diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6f313c6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b927a4 --- /dev/null +++ b/.env.example @@ -0,0 +1,67 @@ +APP_NAME=Piratskibet +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=homestead +DB_USERNAME=homestead +DB_PASSWORD=secret + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null + +MAIL_FROM_ADDRESS=no-reply@codingpirates.dk +MAIL_FROM_NAME=Piratskibet + +MAIL_TO_ADDRESS=piratskibet@codingpirates.dk +MAIL_TO_NAME=Piratskibet + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=eu + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +TWITCH_USER_ID= +TWITCH_CLIENT_ID= +TWITCH_CLIENT_SECRET= + +RECAPTCHA_SITE_KEY= +RECAPTCHA_SITE_SECRET= + +SHUTDOWN_TO=390 +SHUTDOWN_FROM=1290 + +BUGSNAG_API_KEY= +BUGSNAG_NOTIFY_RELEASE_STAGES=production + +INSTAGRAM_LINK=https://www.instagram.com/piratskibet + +TENOR_API_KEY= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..967315d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dae3cb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +/node_modules +/public/hot +/public/storage +/public/js +/public/css +/public/mix-manifest.json +/storage/*.key +/vendor +/.idea +.env +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +*.swp +.vscode +.DS_Store +.rnd diff --git a/README.md b/README.md index bab3208..efcb5ad 100644 --- a/README.md +++ b/README.md @@ -1 +1,432 @@ -# Piratskibs repo +# Pirates :skull: + +## Initial Setup + +1. First install the required packages using composer. + + ``` bash + $ composer install + ``` + +2. Setup a MySQL database. + +3. Copy the `.env.example` file to `.env` and fill out the following required information: + * APP_URL + * DB Credentials + +4. Run the following commands: + ``` bash + $ php artisan system:setup + ``` + + +### Env + +A couple of notes on some of the parameters in the `.env` file: + +##### QUEUE_CONNECTION + +The project has been designed with the `database` queue driver in mind +(with the possibility of upgrading to a `redis` driver later; you can read more here [Laravel Queues][laravel-queues]). +For a development environment the default `sync` driver should be able to be used. +This will however affect the performance of the application. +On a live server the queue system must be setup correctly: [Queues](#queues) + +##### SSL/HTTPS + +For security reasons the project must always use an SSL connection. +To ensure the application always uses a secure connection, we use the following package: [Laravel Https][laravel-https] +However this might not be desirable for a development environment. +To turn this off set the following parameters to false: + +* USE_SSL +* REDIRECT_TO_HTTPS + + +## Development + +For a more indepth look, and for anything not covered in this document, please refer to: + +* The official Laravel documentation [Laravel Documentation][laravel-docs] +* The official React documentation [React Documentation][react-docs] +* The customized boilerplate documentation [Boilerplate Documentation][boilerdocs] + + +### System Commands + +A couple of artisan commands are included to make development and deployment easier. + +* `system:setup` is only used for the initial project setup. + +* `system:build` resets the entire DB and seeds it with data. + +* `system:refresh` is used to reset any cached configurations, and update the persmissions. + This is especially important when developing new Resource endpoints, + or changing permissions for existing ones. + It can also be setup on a live server to run during deployment. + + +### Directory structure + +Here is an overview of some of the non-standard Laravel directory structure used in this project. + +* `App\Models`: These are just Laravel Models, but are grouped in a `Models` dir, + and then further grouped contextually in sub-directories. + +* `App\Resources`: These are the Resource classes from our boilerplate. + These represent traditional Laravel Controllers. + This is where all Api endpoints & page endpoints are defined. + The different sub directories define the Namespace of a Resource, + which is used to generate and reference routes. + +* `App\Operations`: An Operation class is equivalent to a route & Controller method. + While most standard Api routes use the CRUD Operations from [Laravel Resources][laravel-resources], + these project specific Operations usually represent a Model specific action. + +* `App\Support\Enums`: These are our Enum classes from [Laravel Support][laravel-support]. + They simulate Enums in Php, for stricter values and reusability. + We also use these in the React frontend, so the strict values only have to be defined one place. + See more here: [Context](#context) + + +### React + +To work with React you first need to install all the required packages. + +``` bash +$ npm install +``` + +To build files during development use: + +``` bash +$ npm run watch +``` + +Finally before pushing to production, build the optimized files with: + +``` bash +$ npm run prod +``` + +For more info on the custom React boilerplate go here: [Boilerplate Documentation][boilerdocs] + + +### Queues + +To offload a lot of time consuming tasks from user requests, a lot of the system works through Laravel Queues. +The following command is used to run all the project defined queues, in the right order (priority). + +``` bash +$ php artisan queue:work database --sleep=3 --tries=3 --queue=default,forum,emails,notifications,events,sorting,system +``` + +On a server set this up using supervisor following this: [Laravel Queues Supervisor][laravel-queues-supervisor] + + +### Sockets & Pusher + +To achieve real-time communications with the client, +we use websocket connections and the [Pusher][pusher] service. +We do this with Laravel's built-in functionality: [Laravel Broadcasting][laravel-broadcasting]. + +Here are the primary uses of sockets: + +* Notifications: + These are mostly just regular user notification using the default [Laravel Notifications][laravel-notifications]. + There is some custom functionality, which is covered here: [Repeatable Notifications](#notifications) + +* Live chat in the forum: + This forum actually works more like a chat, with live updating content in the frontend + +* User specific events: + There are a couple of user specific events, which use broadcasting. + Specifically the granting of gamification achievements/rewards, + as well as a new user being upgraded to a fully fledged pirate. + +* Some live updates of content: + Other than the chat, some other content is triggered to update through broadcasting. + Content in the moderation section of the admin panel, will update live, + after changes are made by another online user, or after a time consuming queued job executes. + The "shutdown" of the site which happens during night hours, also partially uses broadcasting. + + +### Context + +Context is primarily used to define what data should be provided to the frontend `env`. +It is used to export things like: + +* All routes available to the current user, their parameters and uri's: + This is used for routing in the React application, creating links and more. + +* All Api resources, their endpoints/methods, information about available filters, etc. + These are used for all our Ajax calls. + +* Enums with their values and translated text content: + Mostly used for displaying strict types of data, and select fields with predefined options. + +* User specific data: + This could be the users system permissions, or some of their basic information. + +* Config information required for the frontend: + These could be keys for services like Googles ReCaptcha, or Bugsnag, + or system wide settings like the shutdown times. + +For more information on how to use Context look here: [Boilerplate Context][boilerdocs-context] + + +### Moderation + +#### Features + +The project has an extensive Content Moderation system, +paired with a User Suspensions and Blocking. +The system allows Users to report, or `flag`, undesirable content, which creates a `ModerationCase`. +This in turn, notifies Moderators, who can inspect the case in the admin panel, +and choose to take Action against the flagged content, or even the User responsible for the content. + +The possible `Actions` vary depending on the content (the `Moderateable` Model) in question. +This could be hiding the content of a Forum Message so it is not visible to the public. +All `Moderateable` Models have Actions for suspending or Blocking the User. + +A User suspensions removes their ability to create or edit any content on the site, +until the suspension is lifted. A suspension is set for a specific time frame, +but can be lifted manually by a administrator. + +When a User is Blocked, they are essentially soft deleted, with no ability to login into the system. +If chosen by the Moderator, all content created by the User can also be hidden. +A Blocked User also receives an email, which let's them appeal their case, if they feel unfairly treated. + +To make it easier to handle large amounts of flagged content, +an Automatic Moderation system is also in place. +If enough User have flagged the content before a Moderator handles the case, +an automatic Action will take place. + + +#### Relevant Code + +The system is relatively dynamic, and is mainly composed of relevant Traits, and Actions classes. +The relevant code can be found here: + +* `App\Support\Contracts\Moderateable`: an interface for all content which can be moderated. +* `App\Support\Traits\Moderation\HasModerationRequests`: functionality for flagging `Moderateable` Models. +* `App\Support\Traits\Moderation\HasModerationActions`: used to define Model specific Moderation `Actions`. +* `App\Support\Traits\Moderation\Blockable`: used to hide content, when a User gets Blocked. +* `App\Support\Services\Moderation\*`: all moderation Actions are defined in here. +* `App\Models\Moderation\*`: here are all the Moderation specific Models. + + +#### Flow + +##### 1. Requesting Moderation + +The first step of the process is when a User requests moderation of content. +Currently there are 4 `Moderateable` Models; Models which can be flagged. + +* `App\Models\User\User`: Users can be flagged from their Profile page. +* `App\Models\Projects\Project`: Projects can be flagged from Project pages. +* `App\Models\Forum\Message`: A single Message can be flagged in the Forum. +* `App\Models\Forum\Thread`: An entire Thread can be flagged from the parent Message. + +Before a User can flag something, a permission check is performed. +These are defined in the `Moderateable` Models [Policies][laravel-policies] as `flag`, +however the logic is defined in the `HasModerationRequests` Trait, in the `canUserFlag()` method. +Here it is determined weather a User can flag the Moderateable entity. +A User cannot flag himself, or flag the same entity multiple times before it has been resolved. + +If possible, a User can flag the content by choosing a reason from `App\Support\Enums\ModerationReasons`, +and writing a comment. The request logic is handled in `HasModerationRequests` `flag()` method. +Here a `ModerationRequest` is made for the request, and a `ModerationCase` will be created +(or updated if already exists) for the `Moderateable` Model. + +Upon the creation of the `ModerationRequest`, a notification for admin users is dispatched (`NewModerationRequest`), +and the `AutomaticallyModerateCase` job is scheduled for the case. [Automatic Moderation](#auto_mod) + + +##### 2. Moderation Actions + +After moderation has been requested on a `Moderateble` model, moderators can view the case in the admin panel. +Here they can gain an overview of the moderation requests made against the `Moderateable`, +the content of the Model, the history of Actions performed on the Case, +as well as information on the responsible User, including their other moderation Cases and active suspensions. +Finally they can choose to perform any Actions which are possible for the Model. +Look at `App\Support\Traits\Moderation\HasModerationActions` to see how Actions are bound to a specific Model. + +Moderation Actions consist of 2 parts. + +1. `App\Support\Services\Moderation\Actions`: these are classes which define the Action functionality. +2. `App\Models\Moderation\ModerationAction`: this is a historical log of all Moderation Actions executed for any case. + +There are 3 Action types: + +* `RESOLUTION`: these actions "resolve" the case, meaning they will close the ModerationCase ([note](#auto_mod_resolve)) +* `COMMENT`: these are only for communication purposes +* `SYSTEM`: these are automatic actions performed by the system. Mostly used for opening and closing the case, + as well as sending out User notifications, which need to be recorded in the Action log. + +Most Action classes only define the `perform()` method, but `afterExecute()` and `canPerform()` are also used. +For a more in depth look at how Actions work, refer to the abstract `App\Support\Services\Moderation\Abstracts\Action` class. + + +##### 3. Automatic Moderation + +Upon the creation of a `ModerationRequest`, the system attempts to perform an Automatic Moderation. +This is done in the `App\Jobs\Moderation\AutomaticallyModerateCase` job. +The relevant code is defined in `App\Models\Moderation\ModerationCase` `needsAutomaticResolution()` method. +Here a calculation is made, based on the configuration in `permissions.moderation`. +Moderation Requests made by Users with different Roles, have a different `weight` value. +The sum of this `weight` value, is calculated from all the `ModerationRequests` made for the case. +If the sum is `>=` than the `threshold`, Automatic Moderation will be performed. + +`getCustomAutomaticResolutionActions()` on the individual Model is used to define which Actions should be performed in this case. +The `SuspendUser` action always takes place, to ensure a malicious User can't wreak havoc on the site, +in the time it takes a moderator to inspect the case manually. + +It is important to note, that when an automatic action of type `RESOLUTION` is executed, +it will not close the case. This is done with the assumption that each case needs a human to finalize it. + + +##### Suspensions and Blocking + +When a User has an active suspension (see: `App\Models\User\User::getIsSuspendedAttribute()`) +the vast majority of the sites functionality is locked for them. +This is simply done by including a check for this in Model [Policies][laravel-policies]. + +When a User is Blocked, their User Model is Soft Deleted. +When this happens, a `BlockedUserNotice` mail is sent to them, and their parents. +The mail contains a [signed route][laravel-signed-routes], which leads to an appeal page. +Here the User can submit a form with their appeal, +the content of which will be visible on the Moderation Case in the admin panel. +The Case will also be reopened in this case, so it will appear as not final. + +If the Moderators should wish to unblock the User, this can be done under `Users -> Pirates` +in the admin panel, by sorting for blocked users only, and going to the user in question. +Here an `Unblock` action is available. + + +#### Live update with sockets + +In order to bring the most current data on Moderation Cases to the moderators, +the moderation section in the admin panel, uses a websocket connection, +to force update content in the client, when it changes on the server. +This is done by the broadcasting event `App\Events\Moderation\ModerationCase\Updated`, +which is simply triggered whenever any Case related models update. + + +### Notifications + +##### Repeatable Notifications + +To achieve Notification which can change content or context (f.ex. the count of likes on a Project), +we use a custom Notification [Channel][laravel-notifications-channels]. + +The `App\Channels\RepeatableDatabaseChannel` Channel class, +extends the default Laravel database Channel. +However instead of always creating a new Notification, +it has the ability to find, and update an already existing Notification. +Paired with the `App\Support\Traits\Notifications\Repeatable` trait used on individual Notification classes, +we can change the content of an individual Notification. + +This is an example of how this might work for the `App\Notifications\Forum\MessageReaction` Notification: + +1. Someone likes a users Forum Message + +2. We create and send a new Notification with the content: "User X has liked your message" + +3. Another user likes the same Forum Message + +4. This time instead of creating a new Notification, + we use the specified Identifiers (Notification class, Related Forum Message etc.), + to find the already existing Notification, + and update its content to: "User Y and 1 more, have liked your message" + +This can continue with more likes, +where the content of the Notification might end up being "User Z and 25 others have liked your message". +This gives us the flexibility, to edit or even remove Notifications (f.ex. if somebody "unliked" a message), +without having to create duplicate Notifications for the same entity. + +The keys to creating these Repeatable Notifications are: + +* The `App\Support\Traits\Notifications\Repeatable` trait. + It tells the `RepeatableDatabaseChannel` that this Notification should attempt to be updated, + instead of just created. + +* The `getIdentifiers()` method, which defines something unique to the case, so we can find the Notification to updated. + +* The `toArray()` method, which defines the content of the Notification independently + +In most cases, the Identifiers will be specifically related to a model. +Let's look at an example for the `App\Notifications\Forum\MessageReaction` Notification. +When the `RepetableDatabaseChannel` attempts to find a Notification to update, +it will query the Notifications like this: + +* It will only look for Notifications for the User we are notifying (this is default `DatabaseChannel` behaviour). + +* It looks for Notifications with the "type" of `App\Notifications\Forum\MessageReaction` + +* It will look for the json "data" param, containing matching Identifiers. + In this case it is the `message_id`. + +A Message written by a user, only needs 1 Notification to show how many likes it has received.' +These 3 constrains are enough to find that Notification and update it. +Because the `read_at` attribute is always reset to null, +and we order the Notifications by the `updated_at` param, +whenever a user receives a new like on their Message, +the Notification will go to the top of their list, +and look like a new, unread Notification. +But we we don't have to worry about cleanup of older Notifications, +so we can easily avoid cases where the users Notification tray might fill up with repeated data, +and look something like this: + +* User a, and 4 others have liked your message +* User b, and 3 others have liked your message +* User c, and 2 others have liked your message +* User d, and 2 others have liked your message +* User e, and 1 other has liked your message +* User f has liked your message + +To make this work, there are a couple more things to consider, +when creating the `toArray()` method. +As mentioned earlier it needs to define its data independently of the Notification process. +We do not track how many times a Notification might have been sent. +A simple way too look at it, is to always create data, +that is relevant at the time of execution. +F.ex. the `MessageReaction` Notification is only relevant, +if the Message in question has likes. +Users can remove their likes from Messages. +Therefore, we take a count of the Messages likes, and if it is 0, +we deactivate the Notification, which hides it from the user. + +##### Links in Notifications + +Most Notifications will act as links when pressed on, +to take the User to the relevant place on the site. +To do this simply define the `route` & `params` parameters in the Notification data. +In this example we link to a specific Message in a Forum Thread. + +```php +[ + 'route' => 'app.forum.message', + 'parameters' => ['thread' => $this->thread->id, 'message' => $this->message->id], +]; +``` + + +[laravel-docs]: https://laravel.com/docs/6.x +[laravel-broadcasting]: https://laravel.com/docs/6.x/broadcasting +[laravel-notifications]: https://laravel.com/docs/6.x/notifications +[laravel-notifications-channels]: https://laravel.com/docs/6.x/notifications#custom-channels +[laravel-queues]: https://laravel.com/docs/6.x/queues +[laravel-queues-supervisor]: https://laravel.com/docs/6.x/queues#supervisor-configuration +[laravel-policies]: https://laravel.com/docs/6.x/authorization#creating-policies +[laravel-signed-routes]: https://laravel.com/docs/6.x/urls#signed-urls + +[laravel-resources]: https://github.com/Morning-Train/LaravelResources +[laravel-support]: https://github.com/Morning-Train/LaravelSupport +[laravel-https]: https://github.com/Morning-Train/laravel-https + +[boilerdocs]: http://boilerdocs.backuptrain.dk +[boilerdocs-context]: http://boilerdocs.backuptrain.dk/laravel/context.html + +[react-docs]: https://reactjs.org/ +[pusher]: https://pusher.com/channels diff --git a/app/Channels/RepeatableDatabaseChannel.php b/app/Channels/RepeatableDatabaseChannel.php new file mode 100644 index 0000000..17ee9a0 --- /dev/null +++ b/app/Channels/RepeatableDatabaseChannel.php @@ -0,0 +1,55 @@ +repeatSend($notifiable, $notification); + } + + return parent::send($notifiable, $notification); + } + + protected function repeatSend($notifiable, Notification $notification) + { + $query = $notifiable->routeNotificationFor('database', $notification); + $predecessor = $notification->getPredecessor($notifiable, (clone $query)); + + return $query->updateOrCreate([ + 'id' => optional($predecessor)->id ?? $notification->id, + 'type' => get_class($notification), + ], + $this->buildPayload($notifiable, $notification) + ); + } + + /** + * @param Notification $notification + * @return bool + */ + public static function isRepeatable(Notification $notification) : bool + { + return Arr::has( + class_uses_recursive(get_class($notification)), + Repeatable::class + ); + } +} diff --git a/app/Console/Commands/Events/ScheduleEventReminders.php b/app/Console/Commands/Events/ScheduleEventReminders.php new file mode 100644 index 0000000..edb4075 --- /dev/null +++ b/app/Console/Commands/Events/ScheduleEventReminders.php @@ -0,0 +1,44 @@ +startOf('hour'); + $end = $start->copy()->endOf('hour'); + + EventReminder::query() + ->reminded(false) + ->remindBefore($end) + ->remindAfter($start) + ->with('event:id') + ->get() + ->each + ->scheduleReminder(); + } +} diff --git a/app/Console/Commands/Sorting/CalculateSortingScore.php b/app/Console/Commands/Sorting/CalculateSortingScore.php new file mode 100644 index 0000000..339aa49 --- /dev/null +++ b/app/Console/Commands/Sorting/CalculateSortingScore.php @@ -0,0 +1,33 @@ +confirm('WARNING: This will clear the DATABASE. Are you sure you want to continue?')) { + + $this->call('down'); + + // + // Config + // + + $this->call('cache:clear'); + $this->call('route:clear'); + $this->call('permission:cache-reset'); + + App::environment('local') ? + $this->call('config:clear') : + $this->call('config:cache'); + + // + // Migrations + // + + $this->call('migrate:reset'); + $this->call('migrate'); + + // + // Seed + // + + $this->call('db:seed'); + + $this->call('mt:refresh-permissions'); + $this->call('queue:restart'); + + $this->call('up'); + + $this->info('The system was successfully rebuilt.'); + } + } +} diff --git a/app/Console/Commands/System/Refresh.php b/app/Console/Commands/System/Refresh.php new file mode 100644 index 0000000..593f9d1 --- /dev/null +++ b/app/Console/Commands/System/Refresh.php @@ -0,0 +1,50 @@ +call('cache:clear'); + $this->call('route:clear'); + $this->call('permission:cache-reset'); + + App::environment('local') ? + $this->call('config:clear') : + $this->call('config:cache'); + + + $this->call('mt:refresh-permissions'); + + $this->call('queue:restart'); + + $this->info('The system was successfully refreshed.'); + } +} diff --git a/app/Console/Commands/System/Setup.php b/app/Console/Commands/System/Setup.php new file mode 100644 index 0000000..ab92823 --- /dev/null +++ b/app/Console/Commands/System/Setup.php @@ -0,0 +1,42 @@ +confirm('WARNING: This will clear the DATABASE. Are you sure you want to continue?')) { + + $this->call('key:generate'); + $this->call('passport:keys'); + $this->call('config:cache'); + $this->call('storage:link'); + $this->call('system:build'); + + $this->info('The system was successfully set up'); + + } + } +} diff --git a/app/Console/Commands/Twitch/UpdateStatus.php b/app/Console/Commands/Twitch/UpdateStatus.php new file mode 100644 index 0000000..601c317 --- /dev/null +++ b/app/Console/Commands/Twitch/UpdateStatus.php @@ -0,0 +1,33 @@ +each->updateStatus(); + } +} diff --git a/app/Console/Commands/Users/ClearReservedUsernames.php b/app/Console/Commands/Users/ClearReservedUsernames.php new file mode 100644 index 0000000..a2cb8a1 --- /dev/null +++ b/app/Console/Commands/Users/ClearReservedUsernames.php @@ -0,0 +1,33 @@ +map(static function (stdClass $video) { + return [ + 'title' => $video->title, + 'alt' => $video->title, + 'img' => preg_replace_array('/%{[width|height]+}/', ['160', '90'], $video->thumbnail_url), + 'link' => $video->url, + ]; + }); + + User::query() + ->whereHas('metaAttributes', function ($q) { + $q->where('name', 'notification_settings')->where("value->{$this->key}", true); + }) + ->get() + ->each(static function (User $user) use ($streams, $courses, $projects) { + Mail::to($user)->send(new WeeklyNewsletter($user, $projects, $courses, $streams)); + }); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..7ac2182 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,51 @@ +command('users:clear-reserved-usernames')->daily(); + $schedule->command('users:schedule-reminder-emails')->daily(); + $schedule->command('sorting:calculate-sorting-score')->hourly(); + $schedule->command('schedule:event-reminders')->hourly(); + + $schedule->command('twitch:update-status')->everyMinute(); // TODO - Transfer to queue + + $schedule->command('users:schedule-weekly-newsletter') + ->timezone('Europe/Copenhagen') + ->weeklyOn(Carbon::SATURDAY, '08:00'); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Context/App/AppContext.php b/app/Context/App/AppContext.php new file mode 100644 index 0000000..3a93aad --- /dev/null +++ b/app/Context/App/AppContext.php @@ -0,0 +1,88 @@ + Carbon::now()->format('Y-m-d H:i:s'), + 'debug' => config('app.debug', true), + 'environment' => env('APP_ENV', 'local'), + 'app' => [ + 'locale' => app()->getLocale(), + 'name' => config('app.name'), + 'time' => [ + 'zone' => config('app.timezone', 'UTC'), + 'now' => Carbon::now()->format('Y-m-d H:i:s P'), + ] + ], + 'recaptcha' => [ + 'key' => config('services.google.recaptcha.key'), + ], + 'broadcasting' => [ + 'pusher' => [ + 'key' => config('broadcasting.connections.pusher.key'), + 'cluster' => config('broadcasting.connections.pusher.options.cluster'), + 'encrypted' => config('broadcasting.connections.pusher.options.encrypted'), + ] + ], + 'shutdown' => Shutdown::getEnv(), + 'content' => [ + 'twitch' => [ + 'channels' => TwitchChannel::get(), + ], + 'meeting' => Meeting::export(), + 'helper_bot' => [ + 'app.forum.overview' => 'https://www.youtube.com/watch?v=3m1ZMHQ2ExM', // Piratsnak + 'app.projects.overview' => 'https://www.youtube.com/watch?v=wAn8qPn650Q', // Showcase + 'app.courses.overview' => 'https://www.youtube.com/watch?v=feGXSvtxoPs', // Kodehavet + 'app.tv.index' => 'https://www.youtube.com/watch?v=jnikyLKpWL0', // Coding Pirates TV + ], + ], + 'services' => [ + 'tenor' => [ + 'key' => config('services.tenor.key') + ], + 'instagram' => [ + 'url' => config('services.instagram.url'), + ], + ] + ]; + }); + + Context::env(function () { + return [ + 'user' => \Auth::check() ? \Auth::user()->toArray() : null, + 'notification_settings' => User::editableNotifications(), + ]; + }); + + // Exports user permissions + PermissionsService::export(); + + Context::load(Assets::class); + Context::load(Routes::class); + //Context::load(Services::class); + //Context::load(Translations::class); + Context::load(Components::class); + Context::load(Resources::class); + //Context::load(Menus::class); + Context::load(Enums::class); + + } + +} diff --git a/app/Context/App/Assets.php b/app/Context/App/Assets.php new file mode 100644 index 0000000..a9ba1cc --- /dev/null +++ b/app/Context/App/Assets.php @@ -0,0 +1,29 @@ +manifest)) + ]); + + Context::scripts([ + asset(mix('polyfill.js', 'polyfill')), + asset('polyfill/filepond-polyfill.min.js'), + 'https://player.twitch.tv/js/embed/v1.js', + 'https://meet.jit.si/external_api.js', + asset(mix('js/manifest.js', $this->manifest)), + asset(mix('js/vendor.js', $this->manifest)), + asset(mix('js/app.js', $this->manifest)) + ]); + } + +} diff --git a/app/Context/App/Components.php b/app/Context/App/Components.php new file mode 100644 index 0000000..1d90589 --- /dev/null +++ b/app/Context/App/Components.php @@ -0,0 +1,29 @@ +configureReact(); + } + + protected function configureReact() + { + React::config([ + 'markup' => false, + 'cache' => false, + 'namespace' => 'app', + 'env' => function () { + return Context::env()->data() ['env'] ?? []; + } + ]); + } + +} diff --git a/app/Context/App/Enums.php b/app/Context/App/Enums.php new file mode 100644 index 0000000..d8a8aa0 --- /dev/null +++ b/app/Context/App/Enums.php @@ -0,0 +1,45 @@ +files('Support\Enums')) + ->filter(function ($file) { + // Only use php files + $end = '.php'; + + return substr($file, -strlen($end)) === $end; + + }) + ->map(function ($file) { + $file = rtrim($file, '.php'); + + return '\\App\\Support\\Enums\\' . class_basename($file); + }) + ->unique() + ->filter(function ($class) { + return ( + class_exists($class) && + (get_parent_class($class) === ExportableEnum::class) && + $class::$export + ); + }) + ->mapWithKeys(function ($enum) { + return [$enum::basename() => $enum::export()]; + }); + + Context::env(function () use ($data) { + return [ + 'enums' => $data->toArray(), + ]; + }); + } +} diff --git a/app/Context/App/Resources.php b/app/Context/App/Resources.php new file mode 100644 index 0000000..1a796d4 --- /dev/null +++ b/app/Context/App/Resources.php @@ -0,0 +1,19 @@ +manifest)) + ]); + + Context::scripts([ + asset(mix('polyfill.js', 'polyfill')), + asset('polyfill/filepond-polyfill.min.js'), + asset(mix('js/manifest.js', $this->manifest)), + asset(mix('js/vendor.js', $this->manifest)), + asset(mix('js/backend.js', $this->manifest)) + ]); + } + +} diff --git a/app/Context/Backend/BackendContext.php b/app/Context/Backend/BackendContext.php new file mode 100644 index 0000000..6ce9a30 --- /dev/null +++ b/app/Context/Backend/BackendContext.php @@ -0,0 +1,60 @@ + Carbon::now()->format('Y-m-d H:i:s'), + 'debug' => config('app.debug', true), + 'environment' => env('APP_ENV', 'local'), + 'app' => [ + 'locale' => app()->getLocale(), + 'name' => config('app.name'), + 'time' => [ + 'zone' => config('app.timezone', 'UTC'), + 'now' => Carbon::now()->format('Y-m-d H:i:s P'), + ] + ], + 'broadcasting' => [ + 'pusher' => [ + 'key' => config('broadcasting.connections.pusher.key'), + 'cluster' => config('broadcasting.connections.pusher.options.cluster'), + 'encrypted' => config('broadcasting.connections.pusher.options.encrypted'), + ] + ], + ]; + }); + + Context::env(function () { + return [ + 'user' => \Auth::check() ? \Auth::user()->toArray() : null, + 'notification_settings' => User::editableNotifications(), + ]; + }); + + // Exports user permissions + PermissionsService::export(); + + Context::load(Assets::class); + Context::load(Routes::class); + //Context::load(Services::class); + //Context::load(Translations::class); + Context::load(Components::class); + Context::load(Resources::class); + //Context::load(Menus::class); + Context::load(Enums::class); + + } + +} diff --git a/app/Context/Backend/Components.php b/app/Context/Backend/Components.php new file mode 100644 index 0000000..c50a61c --- /dev/null +++ b/app/Context/Backend/Components.php @@ -0,0 +1,29 @@ +configureReact(); + } + + protected function configureReact() + { + React::config([ + 'markup' => false, + 'cache' => false, + 'namespace' => 'backend', + 'env' => function () { + return Context::env()->data() ['env'] ?? []; + } + ]); + } + +} diff --git a/app/Context/Backend/Enums.php b/app/Context/Backend/Enums.php new file mode 100644 index 0000000..6599f8a --- /dev/null +++ b/app/Context/Backend/Enums.php @@ -0,0 +1,45 @@ +files('Support\Enums')) + ->filter(function ($file) { + // Only use php files + $end = '.php'; + + return substr($file, -strlen($end)) === $end; + + }) + ->map(function ($file) { + $file = rtrim($file, '.php'); + + return '\\App\\Support\\Enums\\' . class_basename($file); + }) + ->unique() + ->filter(function ($class) { + return ( + class_exists($class) && + (get_parent_class($class) === ExportableEnum::class) && + $class::$export + ); + }) + ->mapWithKeys(function ($enum) { + return [$enum::basename() => $enum::export()]; + }); + + Context::env(function () use ($data) { + return [ + 'enums' => $data->toArray(), + ]; + }); + } +} diff --git a/app/Context/Backend/Resources.php b/app/Context/Backend/Resources.php new file mode 100644 index 0000000..e09209a --- /dev/null +++ b/app/Context/Backend/Resources.php @@ -0,0 +1,19 @@ +configureMeta(); + $this->configureReact(); + $this->exportLocalization(); + } + + protected function exportLocalization() + { + Context::env(function () { + return [ + 'authenticated' => Auth::check(), + + 'bugsnag' => [ + 'key' => config('services.bugsnag.key'), + 'notify_stages' => explode(',', config('services.bugsnag.notify_stages')), + ], + + ]; + }); + } + + protected function configureMeta() + { + // The meta is similar to a repository of + // variables that we can use in the views + Context::meta([ + // siteTitle is used in layouts.html to generate the title + 'siteTitle' => config('app.name') + ]); + } + + protected function configureReact() + { + React::config([ + 'host' => config('react.host', 'http://localhost:3000'), + 'markup' => false, + 'namespace' => 'components' + ]); + } +} diff --git a/app/Events/Course/CourseProgressCreated.php b/app/Events/Course/CourseProgressCreated.php new file mode 100644 index 0000000..7dfdc6a --- /dev/null +++ b/app/Events/Course/CourseProgressCreated.php @@ -0,0 +1,23 @@ +courseProgress = $progress; + } +} diff --git a/app/Events/Forum/Thread/MessageCreated.php b/app/Events/Forum/Thread/MessageCreated.php new file mode 100644 index 0000000..cb08b25 --- /dev/null +++ b/app/Events/Forum/Thread/MessageCreated.php @@ -0,0 +1,47 @@ +message = $message; + $this->thread = $message->thread; + } + + public function broadcastOn() + { + return 'forum.thread.' . $this->thread->id . ''; + } + + public function broadcastAs() + { + return 'message.created'; + } + + public function broadcastWith() + { + return [ + 'thread_id' => $this->thread->id, + 'message_id' => $this->message->id, + 'user_id' => $this->message->user_id, + ]; + } + +} diff --git a/app/Events/Forum/Thread/MessageUpdated.php b/app/Events/Forum/Thread/MessageUpdated.php new file mode 100644 index 0000000..a320401 --- /dev/null +++ b/app/Events/Forum/Thread/MessageUpdated.php @@ -0,0 +1,47 @@ +message = $message; + $this->thread = $message->thread; + } + + public function broadcastOn() + { + return 'forum.thread.' . $this->thread->id . ''; + } + + public function broadcastAs() + { + return 'message.' . $this->message->id . '.updated'; + } + + public function broadcastWith() + { + return [ + 'thread_id' => $this->thread->id, + 'message_id' => $this->message->id, + ]; + } + + +} diff --git a/app/Events/Forum/Thread/Updated.php b/app/Events/Forum/Thread/Updated.php new file mode 100644 index 0000000..c9af684 --- /dev/null +++ b/app/Events/Forum/Thread/Updated.php @@ -0,0 +1,50 @@ +thread = $thread; + } + + public function broadcastOn() + { + return 'forum.thread.' . $this->thread->id . ''; + } + + public function broadcastAs() + { + return 'updated'; + } + + public function broadcastWith() + { + return [ + 'thread_id' => $this->thread->id + ]; + } + + + + +} \ No newline at end of file diff --git a/app/Events/Moderation/ModerationCase/Updated.php b/app/Events/Moderation/ModerationCase/Updated.php new file mode 100644 index 0000000..ead9af8 --- /dev/null +++ b/app/Events/Moderation/ModerationCase/Updated.php @@ -0,0 +1,50 @@ +case = $case; + } + + public function broadcastOn() + { + return 'moderation.case.' . $this->case->id . ''; + } + + public function broadcastAs() + { + return 'updated'; + } + + public function broadcastWith() + { + return [ + 'case_id' => $this->case->id + ]; + } + + + + +} diff --git a/app/Events/System/ShutdownEnded.php b/app/Events/System/ShutdownEnded.php new file mode 100644 index 0000000..3ff93e4 --- /dev/null +++ b/app/Events/System/ShutdownEnded.php @@ -0,0 +1,39 @@ + false + ] + ); + } + +} \ No newline at end of file diff --git a/app/Events/System/ShutdownStarted.php b/app/Events/System/ShutdownStarted.php new file mode 100644 index 0000000..ac18988 --- /dev/null +++ b/app/Events/System/ShutdownStarted.php @@ -0,0 +1,39 @@ + true + ] + ); + } + +} \ No newline at end of file diff --git a/app/Events/User/NotificationRead.php b/app/Events/User/NotificationRead.php new file mode 100644 index 0000000..be9f078 --- /dev/null +++ b/app/Events/User/NotificationRead.php @@ -0,0 +1,43 @@ +user = $notification->notifiable; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel("App.Models.User.User.{$this->user->id}"); + } + + public function broadcastAs() + { + return 'notification.read'; + } + +} diff --git a/app/Events/User/UpgradedToPirate.php b/app/Events/User/UpgradedToPirate.php new file mode 100644 index 0000000..d5c6ac9 --- /dev/null +++ b/app/Events/User/UpgradedToPirate.php @@ -0,0 +1,44 @@ +user = $user; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel("App.Models.User.User.{$this->user->id}"); + } + + public function broadcastAs() + { + return 'user.upgradedToPirate'; + } + +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..48b3503 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,67 @@ +route(); + + if ($route instanceof Route && $route->getName() === 'auth.register.verify_email') { + session()->push('flash_messages', [ + 'type' => 'error', + 'message' => trans('auth.email_verification_requires_login') + ]); + } + + return parent::unauthenticated($request, $exception); + } +} diff --git a/app/Exports/UsersExport.php b/app/Exports/UsersExport.php new file mode 100644 index 0000000..555a857 --- /dev/null +++ b/app/Exports/UsersExport.php @@ -0,0 +1,106 @@ +withoutGlobalScope('default_withs') + ->with('roles') + ->with(['metaAttributes' => function ($q) { + $q->where('name', 'zipcode'); + }]) + ->withCount([ + 'rewards', + 'projects', + 'participatingProjects', + 'courseProgresses', + 'moderationAccusations', + 'threads' => function ($q) { + $q->where('is_embedded', false); + }, + 'messages' => function ($q) { + $q->isNormalMessage(); + $q->whereHas('thread', function ($q) { + $q->where('is_embedded', false); + }); + }, + 'messages as project_messages_count' => function ($q) { + $q->whereHas('thread', function ($q) { + $q->has('project'); + }); + }, + ]); + } + + public function headings(): array + { + return [ + 'Navn', + 'Brugernavn', + 'Email', + 'Forældre-email', + 'Alder', + 'Postnummer', + 'Rolle', + 'Dato for oprettelse', + 'Seneste aktivitet', + 'Projekter i Showcase', + 'Kommentarer i Showcase', + 'Chats i Piratsnak', + 'Kommentarer i Piratsnak', + 'Afsluttede forløb i Kodehavet', + 'Achievements', + 'Moderationssager', + ]; + } + + public function map($user): array + { + return [ + $user->name, // 'Navn', + $user->username, // 'Brugernavn', + $user->email, // 'Email', + $user->parent_email, // 'Forældre-email', + $user->age, // 'Alder', + $user->zipcode, // 'Postnummer', + UserRoles::translate($user->role_name), // 'Rolle', + static::formatDate($user->created_at), // 'Dato for oprettelse', + static::formatDate($user->last_activity_at), // 'Seneste aktivitet', + $user->projects_count + $user->participating_projects_count, // 'Antal tilknyttede projekter', + $user->project_messages_count, // 'Kommentarer i Showcase', + $user->threads_count, // 'Antal chats i Piratsnak', + $user->messages_count, // 'Kommentarer i Piratsnak', + $user->course_progresses_count, // 'Antal gennemførte/afsluttede læringsforløb i Kodehavet', + $user->rewards_count, // 'Antal Achievements', + $user->moderation_accusations_count, // 'Antal moderationssager', + ]; + } + + public static function afterSheet(AfterSheet $event) + { + rescue(static function () use ($event) { + $event->sheet->getDelegate()->freezePane('A2'); + }); + } + + public static function formatDate(Carbon $date = null) + { + return optional($date)->format('d/m/Y H:i:s'); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..6a247fe --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,32 @@ +middleware('guest'); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..b2ea669 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,39 @@ +middleware('guest')->except('logout'); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..a23f67d --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,72 @@ +middleware('guest'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return \App\Models\User\User + */ + protected function create(array $data) + { + return User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + ]); + } +} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 0000000..cf726ee --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,39 @@ +middleware('guest'); + } +} diff --git a/app/Http/Controllers/Auth/VerificationController.php b/app/Http/Controllers/Auth/VerificationController.php new file mode 100644 index 0000000..23a43a8 --- /dev/null +++ b/app/Http/Controllers/Auth/VerificationController.php @@ -0,0 +1,41 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..03e02a2 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\AlwaysCreateFreshApiToken::class, + ], + + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, + UpdateLastActivity::class, + ], + + 'api' => [ + //'json', + //'throttle:60,1', + 'bindings', + UpdateLastActivity::class, + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'guard' => \App\Http\Middleware\Guard::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'context' => \MorningTrain\Laravel\Context\Middleware\LoadFeatures::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + //'json' => \App\Http\Middleware\JsonResponseMiddleware::class, + 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, + ]; + + /** + * The priority-sorted list of middleware. + * + * This forces non-global middleware to always be in the given order. + * + * @var array + */ + protected $middlewarePriority = [ + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\Authenticate::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Illuminate\Auth\Middleware\Authorize::class, + UpdateLastActivity::class, + ]; +} diff --git a/app/Http/Middleware/AlwaysCreateFreshApiToken.php b/app/Http/Middleware/AlwaysCreateFreshApiToken.php new file mode 100644 index 0000000..8888645 --- /dev/null +++ b/app/Http/Middleware/AlwaysCreateFreshApiToken.php @@ -0,0 +1,26 @@ +isMethod('GET') && $request->user($this->guard); + /// The reason for this, is that this will now allow us to return the token in API post requests (login route) + /// This middleware should not be used on "normal" routes, as it likely is there for a reason + + return $request->user($this->guard); + } + +} \ No newline at end of file diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..c009a1e --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,27 @@ +expectsJson()) { + + if(Context::is('backend')) { + return route('backend.login.index'); + } + + return route('app.home.index', ['promptLogin' => true]); + } + } +} diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php new file mode 100644 index 0000000..35b9824 --- /dev/null +++ b/app/Http/Middleware/CheckForMaintenanceMode.php @@ -0,0 +1,17 @@ +expectsJson()) { + return route('login'); + } + } + + /** + * Determine if the user is logged in to any of the given guards. + * + * @param \Illuminate\Http\Request $request + * @param array $guards + * @return void + * + * @throws \Illuminate\Auth\AuthenticationException + */ + protected function authenticate($request, array $guards) + { + if (empty($guards)) { + $guards = [null]; + } + + foreach ($guards as $guard) { + if ($this->auth->guard($guard)->check()) { + return $this->auth->shouldUse($guard); + } + } + + foreach ($guards as $guard) { + $this->auth->guard($guard); + return $this->auth->shouldUse($guard); + } + + } + +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..91878e5 --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,31 @@ +check()) { + + if ($request->wantsJson()) { + return abort(403, 'Forbidden'); + } + + return redirect('/'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..5a50e7b --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,18 @@ +refreshLastActivity(); + + return $next($request); + } +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..324a166 --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,24 @@ +getData()['user'] ?? null; + + if (!$user || !$user instanceof User) { + return; + } + + $view->with($this->getViewData( + $this->getProjects($this->getNotifications($user)) + )); + } + + private function getNotifications(User $user): Collection + { + return $user->notifications() + ->whereNull('read_at') + ->where(static function ($q) { + $q->where('data->status', '<>', NotificationStatus::DISABLED); + $q->orWhereNull('data->status'); + }) + ->whereIn('type', [ + ProjectReaction::class, + ]) + ->get() + ->pluck('data'); + } + + private function getProjects(Collection $notifications): Collection + { + return Project::query() + ->whereIn('id', $notifications->pluck('project_id')->filter()) + ->{GenericOrderType::MOST_POPULAR}() + ->get(); + } + + private function getViewData(Collection $projects): array + { + return array_filter([ + 'projectHighlight' => $this->getProjectHighlight($projects->first()), + 'projectSummary' => $this->getProjectsSummary($projects), + ]); + } + + private function getProjectHighlight(?Project $project): ?string + { + if ($project === null) { + return null; + } + + $count = $project->reactions()->endorsement(false)->count() - 1; + $username = $project->reactions() + ->endorsement(false) + ->latest() + ->first()->user->username; + + return trans_choice('emails.weekly_newsletter.projects_highlight', $count, [ + 'user' => $username, + 'project' => $project->title, + 'link' => route('app.projects.project', [ + 'project' => $project->id + ]), + ]); + } + + private function getProjectsSummary(Collection $projects): ?string + { + $count = $projects->count() - 1; + + if ($count < 1) { + return null; + } + + return trans_choice('emails.weekly_newsletter.projects_summary', $count); + } +} diff --git a/app/Http/View/Composers/ThreadActivitySummaryComposer.php b/app/Http/View/Composers/ThreadActivitySummaryComposer.php new file mode 100644 index 0000000..c202fd3 --- /dev/null +++ b/app/Http/View/Composers/ThreadActivitySummaryComposer.php @@ -0,0 +1,139 @@ +getData()['user'] ?? null; + + if (!$user || !$user instanceof User) { + return; + } + + $view->with($this->getViewData( + $this->getThreads($this->getNotifications($user)), + $user + )); + } + + private function getNotifications(User $user): Collection + { + return $user->notifications() + ->whereNull('read_at') + ->where(static function ($q) { + $q->where('data->status', '<>', NotificationStatus::DISABLED); + $q->orWhereNull('data->status'); + }) + ->whereIn('type', [ + MessageReaction::class, + NewMessage::class, + ]) + ->get() + ->groupBy('type'); + } + + private function getThreads(Collection $notifications): Collection + { + return Thread::query() + ->public() + ->where('is_embedded', false) + ->where(static function (Builder $q) use ($notifications) { + $q->whereIn('id', + optional($notifications->get(NewMessage::class))->pluck('data.thread_id') ?? [] + ); + $q->orWhereIn('original_message_id', + optional($notifications->get(MessageReaction::class))->pluck('data.message_id') ?? [] + ); + }) + ->orderBy('sort_score', 'desc') + ->get(); + } + + private function getViewData(Collection $threads, User $user): array + { + return array_filter([ + 'threadHighlight' => $this->getThreadHighlight($threads->first(), $user), + 'threadSummary' => $this->getThreadSummary($threads), + ]); + } + + private function getThreadHighlight(?Thread $thread, User $user): ?string + { + if ($thread === null) { + return null; + } + + $participants = $this->getThreadParticipants($thread, $user); + $reacters = $this->getMessageReacters($thread->originalMessage); + $all = $participants->union($reacters); + + $count = $all->count() - 1; + $username = $all->first(); + $actions = implode(' og ', array_filter([ + $reacters->count() > 0 ? 'liket' : null, + $participants->count() > 0 ? 'kommenteret på' : null, + ])); + + return trans_choice('emails.weekly_newsletter.threads_highlight', $count, [ + 'actions' => $actions, + 'user' => $username, + 'link' => route('app.forum.thread', [ + 'thread' => $thread->id, + ]), + ]); + } + + private function getThreadParticipants(Thread $thread, User $user): Collection + { + return $thread->messages() + ->where('user_id', '<>', $user->id) + ->with('user:id,username') + ->latest() + ->get() + ->pluck('user.username', 'user.id'); + } + + private function getMessageReacters(?Message $message): Collection + { + if (!$message) { + return collect(); + } + + return $message->reactions() + ->endorsement(false) + ->with('user:id,username') + ->latest() + ->get() + ->pluck('user.username', 'user.id'); + } + + private function getThreadSummary(Collection $threads): ?string + { + $count = $threads->count() - 1; + + if ($count < 1) { + return null; + } + + return trans_choice('emails.weekly_newsletter.threads_summary', $count); + } +} diff --git a/app/Jobs/Events/RemindEventUsers.php b/app/Jobs/Events/RemindEventUsers.php new file mode 100644 index 0000000..a999c2e --- /dev/null +++ b/app/Jobs/Events/RemindEventUsers.php @@ -0,0 +1,51 @@ +reminder = $reminder; + $this->event = $event; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if ($this->reminder->reminded) return; + + $this->event + ->relevantUsers + ->each + ->notify(new EventReminderNotification($this->event)); + + $this->reminder->reminded = true; + $this->reminder->save(); + } +} diff --git a/app/Jobs/Forum/UpdateMostPopularAnswer.php b/app/Jobs/Forum/UpdateMostPopularAnswer.php new file mode 100644 index 0000000..136c62c --- /dev/null +++ b/app/Jobs/Forum/UpdateMostPopularAnswer.php @@ -0,0 +1,32 @@ +thread = $thread; + } + + public function handle() + { + if($this->thread !== null) { + $this->thread->updateMostPopularAnswer(); + } + } + +} diff --git a/app/Jobs/Moderation/AutomaticallyModerateCase.php b/app/Jobs/Moderation/AutomaticallyModerateCase.php new file mode 100644 index 0000000..2a30f8e --- /dev/null +++ b/app/Jobs/Moderation/AutomaticallyModerateCase.php @@ -0,0 +1,47 @@ +case = $case; + + self::onQueue('system'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if ($this->case->needsAutomaticResolution()) { + foreach ($this->case->moderateable->getAutomaticResolutionActions($this->case->requestsHaveReachedModerationThreshold()) as $action => $params) { + $this->case->performModeration($action, 'Automatic Moderation', ...$params); + } + + $this->case->performModeration(ModerateAutomatically::class); + } + } +} diff --git a/app/Jobs/Moderation/PerformAsyncModeration.php b/app/Jobs/Moderation/PerformAsyncModeration.php new file mode 100644 index 0000000..8715a35 --- /dev/null +++ b/app/Jobs/Moderation/PerformAsyncModeration.php @@ -0,0 +1,48 @@ +case = $case; + $this->action = $action; + $this->note = $note; + $this->args = $args; + + self::onQueue('system'); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->case->performModeration($this->action, $this->note, ...$this->args); + } +} diff --git a/app/Jobs/Sorting/CalculateSortScore.php b/app/Jobs/Sorting/CalculateSortScore.php new file mode 100644 index 0000000..d9efe43 --- /dev/null +++ b/app/Jobs/Sorting/CalculateSortScore.php @@ -0,0 +1,43 @@ +with('latestMessage') + ->get() + ->each(function ($model) { + CalculateSortScoreForModel::dispatch($model); + }); + + Project::query() + ->with('endorsements') + ->with('thread.latestMessage') + ->get() + ->each(function ($model) { + CalculateSortScoreForModel::dispatch($model); + }); + + } + +} diff --git a/app/Jobs/Sorting/CalculateSortScoreForModel.php b/app/Jobs/Sorting/CalculateSortScoreForModel.php new file mode 100644 index 0000000..5ac25a4 --- /dev/null +++ b/app/Jobs/Sorting/CalculateSortScoreForModel.php @@ -0,0 +1,34 @@ +model = $model; + } + + public function handle() + { + if($this->model && method_exists($this->model, 'calculateSortingScore')) { + $this->model->calculateSortingScore(); + } + } + +} diff --git a/app/Jobs/System/RecordChange.php b/app/Jobs/System/RecordChange.php new file mode 100644 index 0000000..e59c41e --- /dev/null +++ b/app/Jobs/System/RecordChange.php @@ -0,0 +1,48 @@ +data = $data; + + } + + public function handle() + { + + if (empty($this->data)) { + return; + } + + $change = new Change(); + + foreach ($this->data as $attribute => $value) { + if($value instanceof \stdClass || is_array($value)) { + $value = json_encode($value); + } + + $change->{$attribute} = $value; + } + + $change->save(); + + } + +} diff --git a/app/Jobs/Users/SendSignupReminderEmails.php b/app/Jobs/Users/SendSignupReminderEmails.php new file mode 100644 index 0000000..61edd46 --- /dev/null +++ b/app/Jobs/Users/SendSignupReminderEmails.php @@ -0,0 +1,52 @@ +onQueue('system'); + } + + public function handle(): void + { + $this->sendReminder(today()->subDays(config('system.signup_reminders.days_since_first')), 1); + $this->sendReminder(today()->subDays(config('system.signup_reminders.days_since_second')), 2); + } + + private function sendReminder(Carbon $creationDate, int $reminder): void + { + User::query() + ->role(UserRoles::LANDLUBBER) + ->whereDate('created_at', $creationDate) + ->get() + ->each(function (User $user) use ($reminder) { + Mail::to($user->email) + ->later( + today()->timezone('Europe/Copenhagen')->hours(config('system.signup_reminders.send_at_hours')), + new SignupReminder($reminder) + ); + }); + } +} diff --git a/app/Listeners/Forum/ForumEventSubscriber.php b/app/Listeners/Forum/ForumEventSubscriber.php new file mode 100644 index 0000000..eba83ef --- /dev/null +++ b/app/Listeners/Forum/ForumEventSubscriber.php @@ -0,0 +1,69 @@ +listen( + MessageCreated::class, + static::class.'@handleNewMessage' + ); + + $events->listen([ + 'eloquent.created: ' . MessageMention::class, + 'eloquent.deleted: ' . MessageMention::class, + ], static::class . '@notifyMentioned'); + + $events->listen([ + 'eloquent.created: ' . ReactionModel::class, + 'eloquent.deleted: ' . ReactionModel::class, + ], static::class . '@handleMessageReaction'); + } + + public function handleMessageReaction(ReactionModel $reaction) + { + $message = $reaction->message; + $thread = $message->thread; + + if ($reaction->type !== ReactionType::ENDORSEMENT) { + return $message->user->notify(new MessageReaction($message, $thread)); + } + + // Only notify on "first" endorsement + if ($reaction->exists && $message->reactions()->endorsement(true)->count() === 1) { + return $message->user->notify(new NewEndorsement($message, $thread)); + } + } + + public function handleNewMessage($event) + { + // Notify thread creator of new comment + if ($event->thread->created_by !== null && $event->thread->creator->id !== $event->message->user->id) { + $event->thread->creator->notify(new NewMessage($event->thread)); + } + } + + public function notifyMentioned(MessageMention $mention) + { + $message = $mention->message; + $thread = $message->thread; + + $mention->user->notify(new UserMentioned($message, $thread)); + } +} diff --git a/app/Listeners/Gamification/RewardCourseProgression.php b/app/Listeners/Gamification/RewardCourseProgression.php new file mode 100644 index 0000000..cfada58 --- /dev/null +++ b/app/Listeners/Gamification/RewardCourseProgression.php @@ -0,0 +1,31 @@ +courseProgress; + + if (!$progress->completed || !$progress->user || !$progress->course || !$progress->course->achievement) { + return; + } + + $progress->course->achievement->grantAchievement($progress->user); + } +} diff --git a/app/Mail/Contact/NewContactSubmission.php b/app/Mail/Contact/NewContactSubmission.php new file mode 100644 index 0000000..e787d08 --- /dev/null +++ b/app/Mail/Contact/NewContactSubmission.php @@ -0,0 +1,59 @@ +submission = $submission; + + $this->onQueue('emails'); + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->markdown('emails.contact.new_submission') + ->with(['response' => $this->makeResponse()]); + } + + protected function makeResponse() + { + $base = "mailto:{$this->submission->email}"; + $params = http_build_query([ + 'subject' => "Re:{$this->submission->subject}", + 'body' => "\n\n---------------------------------------------------\n\n{$this->submission->message}", + ], + null, + '&', + PHP_QUERY_RFC3986 + ); + + return "{$base}?{$params}"; + } + + +} diff --git a/app/Mail/Moderation/BlockedUserNotice.php b/app/Mail/Moderation/BlockedUserNotice.php new file mode 100644 index 0000000..3db6b18 --- /dev/null +++ b/app/Mail/Moderation/BlockedUserNotice.php @@ -0,0 +1,52 @@ +case = $case; + $this->user = $user; + $this->reason = $reason; + $this->parent = $parent; + $this->greeting = $parent + ? trans('emails.greetings.polite', ['title' => trans('misc.parent')]) + : null; + + $this->onQueue('emails'); + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->markdown('emails.moderation.blocked_notice')->subject('Brugerblokering'); + } + +} diff --git a/app/Mail/User/SignupReminder.php b/app/Mail/User/SignupReminder.php new file mode 100644 index 0000000..7a4c0e7 --- /dev/null +++ b/app/Mail/User/SignupReminder.php @@ -0,0 +1,42 @@ +greeting = trans('emails.signup_reminder.greeting'); + $this->heading = trans_choice('emails.signup_reminder.heading', $reminder); + $this->subject = trans_choice('emails.signup_reminder.subject', $reminder); + + $this->onQueue('emails'); + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->markdown('emails.user.signup-reminder') + ->subject($this->subject); + } +} diff --git a/app/Mail/User/WeeklyNewsletter.php b/app/Mail/User/WeeklyNewsletter.php new file mode 100644 index 0000000..39c9adc --- /dev/null +++ b/app/Mail/User/WeeklyNewsletter.php @@ -0,0 +1,81 @@ +user = $user; + $this->projects = $this->makeTable($projects); + $this->courses = $this->makeTable($courses); + $this->streams = $this->makeTable($streams); + + $this->onQueue('emails'); + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $this->greeting = trans('emails.weekly_newsletter.greeting'); + $this->footerContent = trans('emails.weekly_newsletter.unsubscribe') + . PHP_EOL . PHP_EOL + . trans('emails.weekly_newsletter.questions') + . PHP_EOL . PHP_EOL; + + return $this->markdown('emails.user.weekly-newsletter') + ->subject(__('emails.weekly_newsletter.subject')); + } + + private function makeTable($items) + { + return collect($items) + ->map(static function ($item) { + return [ + 'header' => "[![{$item['alt']}]({$item['img']})]({$item['link']})", + 'split' => ":--------------------------------------------------:", + 'row' => "[{$item['title']}]({$item['link']})", + ]; + }) + ->pipe(static function ($collection) { + return [ + '|' . $collection->implode('header', '|') . '|', + '|' . $collection->implode('split', '|') . '|', + '|' . $collection->implode('row', '|') . '|', + ]; + }); + } +} diff --git a/app/Models/Achievements/Achievement.php b/app/Models/Achievements/Achievement.php new file mode 100644 index 0000000..c1d2cc0 --- /dev/null +++ b/app/Models/Achievements/Achievement.php @@ -0,0 +1,75 @@ +hasMany(AchievementItem::class); + } + + public function userRewards() + { + return $this->hasMany(UserReward::class); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeLockedForUser(Builder $q, $user) + { + if ($user instanceof User) { + $user = $user->id; + } + + return $q->whereDoesntHave('userRewards', function (Builder $q) use ($user) { + $q->where('user_id', $user); + }); + } + + public function scopeGrantedToUser(Builder $q, $user) + { + if ($user instanceof User) { + $user = $user->id; + } + + return $q->whereHas('userRewards', function (Builder $q) use ($user) { + $q->where('user_id', $user); + }); + } + + ////////////////////////////////// + /// Method + ////////////////////////////////// + + public function grantAchievement(User $user) + { + return $user->rewards()->firstOrCreate([ + 'achievement_id' => $this->id, + ], [ + 'name' => $this->name, + 'description' => $this->description, + ]); + } +} diff --git a/app/Models/Achievements/AchievementItem.php b/app/Models/Achievements/AchievementItem.php new file mode 100644 index 0000000..5258f10 --- /dev/null +++ b/app/Models/Achievements/AchievementItem.php @@ -0,0 +1,66 @@ +belongsTo(Achievement::class); + } + + public function item() + { + return $this->morphTo(); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeIsOfType($q, $type) + { + + if (in_array($type, Rewardable::keys())) { + $type = Rewardable::getValue($type); + } + + return $q->where('item_type', '=', $type); + } + + ////////////////////////////////// + /// Methods + ////////////////////////////////// + + /** + * Add $this AchievementItem to the users UserRewardItems, if it didn't exist. + * + * @param User $user + * @return UserRewardItem + */ + public function giveToUser(User $user, UserReward $reward) + { + return $user->rewardItems()->firstOrCreate([ + 'item_id' => $this->item_id, + 'item_type' => $this->item_type, + ], [ + 'user_reward_id' => $reward->id, + ]); + } +} diff --git a/app/Models/Avatar/AvatarItem.php b/app/Models/Avatar/AvatarItem.php new file mode 100644 index 0000000..a72c77a --- /dev/null +++ b/app/Models/Avatar/AvatarItem.php @@ -0,0 +1,177 @@ + 'boolean', + 'is_default' => 'boolean', + 'is_featured' => 'boolean', + 'meta' => 'json', + ]; + + protected $appends = [ + 'svg_viewbox', + 'svg_translate', + ]; + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeCategory(Builder $q, $category) + { + if (!is_string($category) || !AvatarCategory::validate($category)) { + return $q; + } + + return $q->where('category', $category); + } + + public function scopePublished(Builder $q, bool $bool = true) + { + $status = $bool ? + GenericStatus::PUBLISHED : + GenericStatus::DRAFT; + + return $q->where('status', $status); + } + + public function scopePublic(Builder $q, bool $bool = true) + { + return $q->where('is_public', $bool); + } + + public function scopeDefault(Builder $q, bool $bool = true) + { + return $q->where('is_default', $bool); + } + + public function scopeForUser(Builder $q, $user = null) + { + if ($user instanceof User) { + $user = $user->id; + } + else if ($user === null && Auth::check()) { + $user = Auth::id(); + } + + if (is_numeric($user)) { + return $q + ->public() + ->orWhereHas('userRewards', function (Builder $q) use ($user) { + $q->where('user_id', (int)$user); + }); + } + + return $q->public(); + } + + public function scopeOrderByFeatured(Builder $q) + { + return $q->orderBy('is_featured', 'DESC'); + } + + public function scopeOrderFirst(Builder $q, array $ids) + { + if (empty($ids)) { + return $q; + } + + $bindings = join(',', array_fill(0, count($ids), '?')); + + return $q->orderByRaw("FIELD(id, {$bindings}) DESC", $ids); + } + + ////////////////////////////////// + /// Methods + ////////////////////////////////// + + public function userHasItem(User $user = null): bool + { + if ($this->is_public) return true; + + $user = $user ?? Auth::user(); + + if ($user === null) return false; + + return $user->rewardItems() + ->where('item_type', self::class) + ->where('item_id', $this->id) + ->exists(); + } + + ////////////////////////////////// + /// SVG helpers + ////////////////////////////////// + + public function getSvgViewboxAttribute() + { + return $this->buildSvgAttribute('viewBox', [ + 'min-x' => 0, + 'min-y' => 0, + 'width' => 0, + 'height' => 0, + ]); + } + + public function getSvgTranslateAttribute() + { + return $this->buildSvgAttribute('translate', [ + 'x' => 0, + 'y' => 0, + ]); + } + + protected function buildSvgAttribute(string $meta_key, array $defaults) + { + $data = data_get($this->meta, $meta_key, []); + + $values = Arr::only(array_merge($defaults, $data), array_keys($defaults)); + + return implode(' ', $values); + } + + public function getSvgAttribute() + { + return Cache::rememberForever('avatar_item_' . $this->id, function () { + return view('layouts.avatar_item') + ->with([ + 'content' => $this->content, + 'viewBox' => $this->svg_viewbox, + 'translate' => $this->svg_translate, + ]) + ->render(); + }); + } + + /** + * @return Collection + */ + public static function getDefaultItems() + { + return static::query() + ->inRandomOrder() + ->public() + ->default() + ->published() + ->get() + ->keyBy('category'); + } +} diff --git a/app/Models/Avatar/UserAvatar.php b/app/Models/Avatar/UserAvatar.php new file mode 100644 index 0000000..e7e9283 --- /dev/null +++ b/app/Models/Avatar/UserAvatar.php @@ -0,0 +1,177 @@ +id)); + }); + } + + public function user() + { + return $this->hasOne(User::class); + } + + public function userAvatarItems() + { + return $this->hasMany(UserAvatarItem::class); + } + + public function avatarItems() + { + return $this->hasManyThrough( + AvatarItem::class, + UserAvatarItem::class, + 'user_avatar_id', + 'id', + 'id', + 'avatar_item_id' + ); + } + + public function scopeForUser(Builder $q, User $user) + { + $user_id = $user->id; + + return $q->whereHas('user', function (Builder $q) use ($user_id) { + return $q->where('id', $user_id); + }); + } + + public static function getSvgCacheKey(int $id) + { + return 'user_avatar_' . $id; + } + + public function getSvgAttribute() + { + return static::getCachedAvatarSvg(static::getSvgCacheKey($this->id), function () { + return $this->userAvatarItems() + ->with('avatarItem') + ->get() + ->pluck('avatarItem.content', 'category'); + }); + } + + public static function getSvgById($id) + { + if($id === null) { + return null; + } + return static::getCachedAvatarSvg(static::getSvgCacheKey($id), function () use($id) { + return UserAvatar::find($id)->userAvatarItems() + ->with('avatarItem') + ->get() + ->pluck('avatarItem.content', 'category'); + }); + } + + public static function removedAvatarSvg() + { + return static::getCachedAvatarSvg('user_avatar_removed', function () { + return [ + 'body' => '', + 'hat' => '', + 'eyes' => '', + 'mouth' => '', + 'legs' => '', + 'arms' => '', + 'accessories' => '', + ]; + }); + } + + CONST FALLBACK = [ + 'body' => '', + 'hat' => '', + 'eyes' => '', + 'mouth' => '', + 'legs' => '', + 'arms' => '', + 'accessories' => '', + ]; + + public static function getCachedAvatarSvg(string $key, \Closure $rows) + { + return Cache::rememberForever($key, function () use ($rows) { + return view('layouts.avatar') + ->with(['rows' => collect(static::FALLBACK)->merge($rows())]) + ->render(); + }); + } + + public static function setup(User $user) + { + $avatar = static::query()->forUser($user)->firstOrCreate([]); + + $items = AvatarItem::getDefaultItems(); + + foreach (AvatarCategory::values() as $category) { + $item_id = $items->get($category)->id; + + UserAvatarItem::firstOrCreate( + ['user_avatar_id' => $avatar->id, 'category' => $category], + ['avatar_item_id' => $item_id] + ); + } + + return $avatar; + } + + public function setItems(array $items) + { + foreach (AvatarCategory::values() as $category) { + $id = $items[$category] ?? null; + + if ($id === null) { + continue; + } + + $this->setItem($category, $id); + } + } + + public function setItem(string $category, int $value) + { + if (!AvatarCategory::validate($category)) { + throw new \Exception('Invalid AvatarCategory provided.'); + } + + $item = AvatarItem::category($category)->where('id', $value)->firstOrFail(); + + UserAvatarItem::updateOrCreate( + ['user_avatar_id' => $this->id, 'category' => $category], + ['avatar_item_id' => $item->id] + ); + } + + ///////////////////////////// + /// Item getters + ///////////////////////////// + + public function getItemsAttribute() + { + return $this->avatarItems()->get()->keyBy('category'); + } +} diff --git a/app/Models/Avatar/UserAvatarItem.php b/app/Models/Avatar/UserAvatarItem.php new file mode 100644 index 0000000..49641ad --- /dev/null +++ b/app/Models/Avatar/UserAvatarItem.php @@ -0,0 +1,45 @@ +user_avatar_id)); + }); + } + + public function avatarItem() + { + return $this->belongsTo(AvatarItem::class, 'avatar_item_id'); + } + + public function scopeCategory(Builder $q, string $category) + { + if (!AvatarCategory::validate($category)) { + throw new \Exception('Invalid AvatarCategory provided.'); + } + + return $q->where('category', $category); + } +} diff --git a/app/Models/Change.php b/app/Models/Change.php new file mode 100644 index 0000000..2a31ff3 --- /dev/null +++ b/app/Models/Change.php @@ -0,0 +1,89 @@ +exists){ + return; + } + + $dirty = $model->getDirty(); + + if(empty($dirty)) { + return; + } + + foreach($dirty as $column => $value) { + + if(in_array($column, $model->ignoredChangeableAttributes())) { + continue; + } + + $original = $model->getOriginal($column); + + // If the attribute has a get mutator, we will call that then return what + // it returns as the value, which is useful for transforming values on + // retrieval from the model to a form that is more useful for usage. + if ($model->hasGetMutator($column)) { + $original = $model->mutateAttribute($column, $original); + } + + // If the attribute exists within the cast array, we will convert it to + // an appropriate native PHP type dependant upon the associated value + // given with the key in the pair. Dayle made this comment line up. + if ($model->hasCast($column)) { + $original = $model->castAttribute($column, $original); + } + + // If the attribute is listed as a date, we will convert it to a DateTime + // instance on retrieval, which makes it quite convenient to work with + // date fields without having to create a mutator for each property. + if (in_array($column, $model->getDates()) && + ! is_null($original)) { + $original = $model->asDateTime($original); + } + + $current = $model->getAttribute($column); + + RecordChange::dispatch([ + 'changed_at' => Carbon::now(), + 'changed_by' => Auth::id(), + 'changeable_id' => $model->getKey(), + 'changeable_type' => get_class($model), + 'column_name' => $column, + 'from_value' => $original, + 'from_type' => static::getType($original), + 'to_value' => $current, + 'to_type' => static::getType($current), + ]); + + } + + + } + + protected static function getType($value) + { + $type = gettype($value); + + if($type === 'object') { + return get_class($value); + } + + return $type; + } + +} diff --git a/app/Models/ContactSubmission.php b/app/Models/ContactSubmission.php new file mode 100644 index 0000000..f9b6ec4 --- /dev/null +++ b/app/Models/ContactSubmission.php @@ -0,0 +1,46 @@ +score > 0.2) { + $submission->scheduleMail(); + } + + User::query()->permission('backend.contact.index')->get()->each(function ($user) use($submission) { + $user->notify(new \App\Notifications\System\NewContactSubmission($submission)); + }); + + }); + } + + protected function scheduleMail() + { + Mail::to([config('mail.default_to')])->send(new NewContactSubmission($this)); + } + + +} diff --git a/app/Models/Content/AnimatedTickerText.php b/app/Models/Content/AnimatedTickerText.php new file mode 100644 index 0000000..5407f9f --- /dev/null +++ b/app/Models/Content/AnimatedTickerText.php @@ -0,0 +1,24 @@ + 'boolean', + 'meeting_active' => 'boolean', + ]; + + protected static function boot() + { + parent::boot(); + + static::saving(function (Meeting $meeting) { + if ($meeting->banner_active && $meeting->isDirty('banner_active')) { + static::query()->update(['banner_active' => false]); + } + }); + } + + public function scopeActive(Builder $q, bool $active = true) + { + return $q->where('banner_active', $active); + } + + public static function export() + { + $meeting = static::query()->active()->first(); + + if (!$meeting) { + return null; + } + + return [ + 'description' => $meeting->description, + 'meeting_room' => $meeting->meeting_active ? $meeting->meeting_room : null, + 'time' => $meeting->pretty_hours, + ]; + } + + //////////////////////// + /// Time formatting + //////////////////////// + + public function getPrettyHoursAttribute() + { + return + $this->pretty_from + . ' - ' + . $this->pretty_to; + } + + public function getPrettyFromAttribute() + { + return static::formatHour($this->from); + } + + public function getPrettyToAttribute() + { + return static::formatHour($this->to); + } + + private static function formatHour(string $hour) + { + return Carbon::parse($hour)->format('H:i'); + } +} diff --git a/app/Models/Content/Post.php b/app/Models/Content/Post.php new file mode 100644 index 0000000..4f8aa07 --- /dev/null +++ b/app/Models/Content/Post.php @@ -0,0 +1,106 @@ + 'integer' + ]; + + const PURIFIER_CONFIG = 'admin'; + + protected $userGeneratedContent = [ + 'content', + ]; + + protected static function boot() + { + parent::boot(); + + static::saving(function (Post $post) { + + $post->version = $post->version + 1; + + if(empty($post->type)) { + $post->type = PostType::PAGE; + } + }); + + static::saved(function (Post $post) { + if($post->type !== PostType::REVISION) { + $post->storeRevision(); + } + }); + + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeVisible($q) + { + if(Auth::check() === false || Auth::user()->can('api.backend.content.posts.store') === false) { + $q->where('status', '=', GenericStatus::PUBLISHED); + } + + $q->where('type', '=', PostType::PAGE); + + return $q; + } + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + + public function storeRevision() + { + + if(!$this->getOriginal('id')) { + return; + } + + $copy = $this->replicate(); + + foreach ($this->original as $attribute => $value) { + $copy->{$attribute} = $value; + } + + $copy->parent_id = $copy->id; + $copy->version = $copy->version - 1; ///FIX - TODO: This should not be needed? + $copy->id = null; + + $copy->type = PostType::REVISION; + $copy->save(); + } + + public function getOpenGraphMetaAttribute() + { + return array_filter([ + 'title' => $this->title, + 'description' => $this->description, + 'image' => $this->image + ]); + } +} diff --git a/app/Models/Content/TwitchChannel.php b/app/Models/Content/TwitchChannel.php new file mode 100644 index 0000000..049b53d --- /dev/null +++ b/app/Models/Content/TwitchChannel.php @@ -0,0 +1,80 @@ + 'boolean' + ]; + + protected static function boot() + { + parent::boot(); + + static::saving(function (TwitchChannel $channel) { + if ($channel->isDirty(['channel_name'])) { + if ($channel->hasTwitchClientId()) { + $channel->updateStatus(false); + } + } + }); + + } + + public static function getTwitchClientId() + { + return config('services.twitch.client_id'); + } + + public function hasTwitchClientId() + { + return !empty(static::getTwitchClientId()); + } + + protected function resetStatusAttributes() + { + $this->stream_title = ''; + $this->stream_started_at = null; + $this->is_live = false; + } + + public function updateStatus($save = true) + { + $stream = Twitch::getCurrentStreams($this->channel_name)->first(); + + $this->resetStatusAttributes(); + + if ($stream !== null) { + $this->stream_title = $stream->title; + $this->stream_started_at = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $stream->started_at); + $this->is_live = $stream->type === 'live'; + } + + $this->stream_checked_at = Carbon::now(); + + if($save === true) { + $this->save(); + } + + } + +} diff --git a/app/Models/Course/Course.php b/app/Models/Course/Course.php new file mode 100644 index 0000000..d99de0e --- /dev/null +++ b/app/Models/Course/Course.php @@ -0,0 +1,204 @@ +siblings()->get('id')->each->clearCaches(); + }); + + static::deleted(function (Course $course) { + $course->clearCaches(); + }); + } + + + ////////////////////////// + /// Relations + ////////////////////////// + + public function resources() + { + return $this->hasMany(CourseResource::class, 'course_id', 'id'); + } + + public function siblings() + { + return $this->hasMany(static::class, 'category_id', 'category_id'); + } + + public function category() + { + return $this->hasOne(CourseCategory::class, 'id', 'category_id'); + } + + public function achievement() + { + return $this->belongsTo(Achievement::class, 'achievement_id'); + } + + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeCategorySlug(Builder $q, $slug) + { + return $q->whereHas('category', function (Builder $q) use ($slug) { + $q->where('slug', $slug); + }); + } + + public function scopeWithCategoryTitle(Builder $q) + { + return $q->addSelect(['category_title' => CourseCategory::select('title') + ->whereColumn('id', 'courses.category_id') + ->limit(1) + ]); + } + + public function scopeCompleted(Builder $q) + { + return $q->whereHas('progress', function (Builder $q) { + $q->where('status', ResourceProgress::COMPLETED); + }); + } + + + ////////////////////////// + /// Attributes + ////////////////////////// + + public function getLinkAttribute() + { + return [ + 'title' => $this->title, + 'route' => 'app.courses.course', + 'parameters' => [ + 'category' => optional($this->category)->slug, + 'course' => $this->id, + 'course_slug' => $this->slug, + ], + ]; + } + + public function getPrevCourseAttribute() + { + return Cache::remember("{$this->id}_prev_course", 60 * 60 * 24 * 7, function () { + return optional($this->getAdjacentCourse(false))->link; + }); + } + + public function getNextCourseAttribute() + { + return Cache::remember("{$this->id}_next_course", 60 * 60 * 24 * 7, function () { + return optional($this->getAdjacentCourse(true))->link; + }); + } + + /** + * Gets the adjacent course + * + * @param bool $next true = next, false = prev + * @return Builder + */ + public function getAdjacentCourse(bool $next) + { + $operator = $next ? '>' : '<'; + $dir = $next ? 'asc' : 'desc'; + + $sibling = $this->siblings() + ->where('level', (int)$this->level) + ->where('position', $operator, (int)$this->position) + ->orderBy('position', $dir) + ->first(); + + return $sibling ?? $this->siblings() + ->where('level', $operator, (int)$this->level) + ->orderBy('level', $dir) + ->orderBy('position', $dir) + ->first(); + } + + public function clearCaches() + { + Cache::forget("{$this->id}_next_course"); + Cache::forget("{$this->id}_prev_course"); + } + + ////////////////////////// + /// Progress + ////////////////////////// + + public function progress() + { + return $this->hasOne(CourseProgress::class, 'course_id', 'id') + ->where('user_id', Auth::id()); + } + + public function getIsCompletedAttribute() + { + return $this->progress + ? ($this->progress->status === ResourceProgress::COMPLETED) + : false; + } + + ////////////////////////// + /// Misc + ////////////////////////// + + public static function getNewsletterHiglights($count = 3) + { + return static::query() + ->orderBy('created_at', 'desc') + ->with('category:id,slug,logo_id', 'category.logo') + ->limit($count) + ->get() + ->map(static function (Course $course) { + return [ + 'title' => $course->title, + 'alt' => $course->title, + 'img' => optional($course->category)->logo_url, + 'link' => route('app.courses.course', [ + 'category' => optional($course->category)->slug, + 'course' => $course->id, + 'course_slug' => $course->slug, + ]), + ]; + }); + } +} diff --git a/app/Models/Course/CourseCategory.php b/app/Models/Course/CourseCategory.php new file mode 100644 index 0000000..c451e99 --- /dev/null +++ b/app/Models/Course/CourseCategory.php @@ -0,0 +1,91 @@ +removeFiles(); + }); + } + + protected $casts = [ + 'active' => 'boolean' + ]; + + ////////////////////////////////// + /// Relations + ////////////////////////////////// + + public function courses(){ + return $this->hasMany(Course::class, 'category_id', 'id'); + } + + public function logo() + { + return $this->belongsTo(File::class); + } + + public function thumbnail() + { + return $this->belongsTo(File::class); + } + + + ////////////////////////////////// + /// Attributes + ////////////////////////////////// + + public function getLogoUrlAttribute() + { + return optional($this->logo)->url; + } + + public function getThumbnailUrlAttribute() + { + return optional($this->thumbnail)->url; + } + + + ////////////////////////// + /// Misc + ////////////////////////// + + private function removeFiles() + { + // Eager loaded, to trigger "delete" event + // Which removes the actual files + $this->logo->delete(); + $this->thumbnail->delete(); + } + + ////////////////////////// + /// Progress + ////////////////////////// + + public function completedCourses() + { + return $this->courses()->completed(); + } + +} diff --git a/app/Models/Course/CourseProgress.php b/app/Models/Course/CourseProgress.php new file mode 100644 index 0000000..0c0d23a --- /dev/null +++ b/app/Models/Course/CourseProgress.php @@ -0,0 +1,64 @@ + CourseProgressCreated::class, + ]; + + protected $casts = [ + 'meta' => 'json' + ]; + + ////////////////////////// + /// Relationships + ////////////////////////// + + public function courseCategory() + { + return $this->belongsTo(CourseCategory::class, 'course_category_id', 'id'); + } + + public function course() + { + return $this->belongsTo(Course::class, 'course_id', 'id'); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeMine(Builder $q) + { + return $q->where('user_id', '=', Auth::id()); + } + + ////////////////////////// + /// Helpers + ////////////////////////// + + public function getCompletedAttribute() + { + return $this->status === ResourceProgress::COMPLETED; + } +} diff --git a/app/Models/Course/CourseResource.php b/app/Models/Course/CourseResource.php new file mode 100644 index 0000000..d301cda --- /dev/null +++ b/app/Models/Course/CourseResource.php @@ -0,0 +1,97 @@ + 'json' + ]; + + public function course() + { + return $this->belongsTo(Course::class, 'course_id', 'id'); + } + + public function progress() + { + return $this->hasOne(CourseProgress::class, 'course_resource_id', 'id'); + } + + public function my_progress() + { + return $this->progress()->mine(); + } + + public function scopeWhereType(Builder $q, $type) + { + return $q->where('type', $type); + } + + public function scopeWhereCourseSlug(Builder $q, $slug) + { + return $q->whereHas('course', function (Builder $q) use ($slug) { + $q->where('slug', $slug); + }); + } + + public function setMetaAttribute($meta) + { + $key = 'meta'; + + // TODO - Temp. code - update when we get new video types + // Sets default video type + if (is_array($meta) && $this->type === ResourceTypes::VIDEO) { + $meta['type'] = CourseResourceVideoTypes::DEFAULT; + } + + foreach ($meta as $attribute => $value) { + // Clean all content containing HTML + if ($value !== strip_tags($value)) { + $meta[$attribute] = $this->cleanUserContent($value); + } + } + + return $this->attributes['meta'] = $this->castAttributeAsJson($key, $meta); + } + + ////////////////////////// + /// Helpers + ////////////////////////// + + public function checkAnswer(string $question, string $answer): bool + { + if ($this->type !== ResourceTypes::QUESTIONNAIRE) return false; + + $question = collect(optional($this->meta)['questions']) + ->firstWhere('uuid', $question); + + if (empty($question) || !isset($question['answers'])) return false; + + $answer = collect($question['answers'])->firstWhere('uuid', $answer); + + return !empty($answer) + && isset($answer['is_correct']) + && $answer['is_correct']; + } +} diff --git a/app/Models/Events/Event.php b/app/Models/Events/Event.php new file mode 100644 index 0000000..e043d50 --- /dev/null +++ b/app/Models/Events/Event.php @@ -0,0 +1,126 @@ +belongsToMany(Region::class); + } + + public function getRelevantUsersQuery() + { + $regions = $this->regions()->pluck('region_id')->all(); + + return User::query()->forRegions($regions); + } + + /** + * @return Collection + */ + public function getRelevantUsersAttribute() + { + return $this->getRelevantUsersQuery()->get(); + } + + public function scopeForZipcode(Builder $q, $zipcode) + { + return $q->whereHas('regions.zipcodes', function ($q) use ($zipcode) { + return $q->where('zipcode_zipcode', $zipcode); + }); + } + + + ////////////////////////// + /// Reminders + ////////////////////////// + + public function reminders() + { + return $this->hasMany(EventReminder::class); + } + + public function setupDefaultReminders() + { + $fortnight = $this->start_at->copy()->subDays(14)->setTime(18, 30); + $month = $this->start_at->copy()->subMonths(1)->setTime(18, 30); + + $this->reminders()->createMany([ + ['remind_at' => $fortnight], + ['remind_at' => $month], + ]); + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeStatus(Builder $q, $status) + { + return $q->where('status', $status); + } + + public function scopePublished(Builder $q) + { + return $q->where('publish_at', '<', Carbon::now()) + ->status(EventStatus::PUBLISHED); + } + + public function scopeExpired(Builder $q, bool $bool = true) + { + $operator = $bool ? + '<' : '>='; + + return $q->where('end_at', $operator, now()); + } + + ////////////////////////// + /// Attributes + ////////////////////////// + + public function getPrettyStartAttribute() + { + $format = 'D. MMM [kl.] LT'; + + return $this->start_at->calendar(null, [ + 'nextWeek' => $format, + 'sameElse' => $format, + ]); + } + +} diff --git a/app/Models/Events/EventReminder.php b/app/Models/Events/EventReminder.php new file mode 100644 index 0000000..3254754 --- /dev/null +++ b/app/Models/Events/EventReminder.php @@ -0,0 +1,59 @@ + 'boolean']; + + ////////////////////////// + /// Relations + ////////////////////////// + + public function event() + { + return $this->belongsTo(Event::class); + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeReminded(Builder $q, bool $reminded = true) + { + return $q->where('reminded', $reminded); + } + + public function scopeRemindAfter(Builder $q, Carbon $before) + { + return $q->where('remind_at', '>=', $before); + } + + public function scopeRemindBefore(Builder $q, Carbon $before) + { + return $q->where('remind_at', '<=', $before); + } + + ////////////////////////// + /// Helpers + ////////////////////////// + + public function scheduleReminder() + { + RemindEventUsers::dispatch($this, $this->event)->delay($this->remind_at); + } + +} diff --git a/app/Models/Forum/Message.php b/app/Models/Forum/Message.php new file mode 100644 index 0000000..a64386b --- /dev/null +++ b/app/Models/Forum/Message.php @@ -0,0 +1,426 @@ + 'integer', + 'moderated' => 'boolean', + 'created_at' => 'datetime:Y-m-d H:i:s P', + 'updated_at' => 'datetime:Y-m-d H:i:s P', + 'blocked_user' => 'boolean', + ]; + + protected $dates = [ + 'created_at', + 'updated_at' + ]; + + protected $appends = [ + ]; + + protected static function boot() + { + parent::boot(); + + static::created(function (Message $message) { + $message->parseContent(); + }); + + static::updating(function (Message $message) { + if ($message->isDirty('content') && !$message->isDeleted()) { + $message->parseContent(); + $message->recordChange(); + } + }); + + } + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function originalMessageTo() + { + return $this->belongsTo(Thread::class, 'id', 'original_message_id'); + } + + public function thread() + { + return $this->belongsTo(Thread::class); + } + + public function mentions() + { + return $this->hasMany(MessageMention::class); + } + + public function mentionedUsers() + { + return $this->hasManyThrough( + User::class, + MessageMention::class, + 'message_id', // MessageMention->message_id + 'id', // User->id + 'id', // Message->id + 'user_id' // MessageMention->user_id + ); + } + + public function changes() + { + return $this->hasMany(MessageChange::class); + } + + public function reactions() + { + return $this->hasMany(MessageReaction::class, 'message_id'); + } + + public function user() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class, 'user_id', 'id') + ); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeIsOriginalMessage(Builder $builder) + { + return $builder->whereHas('originalMessageTo'); + } + + public function scopeIsNormalMessage(Builder $builder) + { + return $builder->whereDoesntHave('originalMessageTo'); + } + + public function scopePublic(Builder $q, bool $force = false) + { + return $q->whereHas('thread', function ($q) use ($force) { + $q->public($force); + }); + } + + public function scopeForUser(Builder $q, User $user): Builder + { + return $q->where('user_id', $user->id); + } + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + public function getIsOriginalAttribute() + { + return ($this->id !== null) && ($this->id === optional($this->thread)->original_message_id); + } + + public function getModeratedAttribute($moderated) + { + return $moderated || $this->blocked_user; + } + + public function getContentAttribute($content) + { + $removed = __('misc.moderated'); + + return $this->isModerated() ? "[{$removed}]" : $this->correctUserContent($content); + } + + public function isModerated(User $user = null) + { + // Only need to evaluate User, if $moderated === true; + if (!$this->moderated) return false; + + $user = $user ?? Auth::user(); + + return $user === null || !$user->can(CommonPermissions::MODERATE); + } + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + + protected function handleReaction() + { + + CalculateSortScoreForModel::dispatch($this->thread); + + UpdateMostPopularAnswer::dispatch($this->thread) + ->onConnection('database') + ->onQueue('forum'); + + } + + public function syncMentions() + { + + //$regex = "/[\@][\[](?P[^\]]*)[\]][\(](?P[\d]*)/"; + $regex = '/content, $matches); + + $usernames = $matches['username']; + $user_ids = $matches['user_id']; + + $users = User::query()->whereIn('id', $user_ids)->get()->keyBy('id'); + + $mentions = collect(); + + if (!empty($usernames) && !empty($user_ids) && count($usernames) === count($user_ids)) { + foreach ($usernames as $index => $username) { + $user_id = $user_ids[$index]; + + if ($users->has($user_id) && $users->get($user_id)->username === $username) { + $mentions->push($users->get($user_id)); + } + + } + } + + $current_mentions = $this->mentions()->get()->keyBy('user_id'); + + if ($mentions->isNotEmpty()) { + foreach ($mentions as $mention) { + if (!$current_mentions->has($mention->id)) { + /// A mention has been added, create it + $this->mentions()->create(['user_id' => $mention->id]); + } + } + + /// If a mention has been removed, delete it + $current_mentions + ->diffKeys(array_flip($mentions->pluck('id')->all())) + ->each->delete(); + + } else { + $this->mentions()->delete(); + } + + } + + public function parseContent() + { + $this->syncMentions(); + + if ($this->isDirty()) { + //$this->save(); + } + + } + + public function recordChange() + { + + $change = new MessageChange(); + + $change->message_id = $this->id; + $change->content = $this->getOriginal('content'); + $change->user_id = Auth::id(); + + if ($change->user_id === $this->user_id) { + $change->reason = ForumMessageChangeReason::EDIT; + } else { + $change->reason = ForumMessageChangeReason::MODERATION; + } + + $change->save(); + + if ($this->thread) { + $this->broadcastUpdate(); + } + + } + + public function accept() + { + $thread = $this->thread()->first(); + + if($thread !== null) { + + $thread->accepted_answer_id = $this->id; + $thread->save(); + + $thread->broadcastUpdate(); + + } + + return $thread; + } + + ////////////////////////////////// + /// Notification Helpers + ////////////////////////////////// + + public function findPageInThread() + { + + /// This method is used to solve the issue of linking to a specific page in the thread + /// It is relative to the user, as the user might not always have access to all messages + /// We use the Message index operation to utilize the same filtering methods as used on the frontend + /// TODO: Modify operations to make an case like this easier to perform + /// TODO: meaning that we should essentially be able to only apply all the query filters + /// TODO: Maybe be can hook into the pipeline at a certain point and skip the remainging pipes? + + try { + + $operation = (new \App\Resources\Api\Forum\Message('api', 'forum.message'))->operation('index'); + + $found_on_page = null; + $page_to_try = 1; + + while ($found_on_page === null) { + + request()->replace(['thread_id' => $this->thread_id, '$page' => $page_to_try]); + + $response = $operation->execute(); + + $payload = $response->getOriginalContent(); + + $meta = $payload['meta']; + + $page = $meta['filters']['pagination']['page']; + $pages = $meta['filters']['pagination']['pages']; + $has_more_pages = $page < $pages; + + $collection = collect($payload['collection']); + + if ($collection->where('id', '=', $this->id)->isNotEmpty()) { + $found_on_page = $page; + break; + } else { + if ($has_more_pages) { + $page_to_try++; + } else { + break; + } + } + + } + + } catch (\Exception $exception) { + return 1; + } + + return $found_on_page; + } + + ////////////////////////////////// + /// Broadcasting + ////////////////////////////////// + + public function broadcastUpdate() + { + event(new MessageUpdated($this)); + } + + + ////////////////////////// + /// Moderation + ////////////////////////// + + public function moderate() + { + $this->moderated = true; + $this->save(); + } + + public function getResponsibleUserId(): int + { + return $this->user_id; + } + + public function getModerationActionsAttribute(): array + { + return array_merge($this->getCommonModerationActions(), array_filter([ + RemoveMessageContent::class, + $this->removalHasBeenRequested() ? Delete::class : null, + ])); + } + + public function getCustomAutomaticResolutionActions(bool $willSuspendUser): array + { + return [ + ModerationActions::REMOVE_MESSAGE_CONTENT => [], + ]; + } + + public function isDeleted() + { + return $this->user_id === null; + } + + public function delete() + { + $this->content = '[Slettet]'; + $this->user_id = null; + $this->moderated = false; + $this->blocked_user = false; + $this->saveWithoutLogging(); + + $changes = $this->changes()->get('id')->pluck('id')->all(); + $mentions = $this->mentions()->get('id')->pluck('id')->all(); + $this->changes()->delete(); + $this->mentions()->get()->each->delete(); // Trigger delete event, so notifications get updated + + Change::query() + ->where(function (Builder $q) use ($mentions, $changes) { + $q->where(function (Builder $q) { + $q->where('changeable_type', static::class) + ->where('changeable_id', $this->id); + })->orWhere(function (Builder $q) use ($changes) { + $q->where('changeable_type', MessageChange::class) + ->whereIn('changeable_id', $changes); + })->orWhere(function (Builder $q) use ($mentions) { + $q->where('changeable_type', MessageMention::class) + ->whereIn('changeable_id', $mentions); + }); + }) + ->delete(); + } + +} diff --git a/app/Models/Forum/MessageChange.php b/app/Models/Forum/MessageChange.php new file mode 100644 index 0000000..e6d3267 --- /dev/null +++ b/app/Models/Forum/MessageChange.php @@ -0,0 +1,60 @@ + 'datetime:Y-m-d H:i:s P', + 'updated_at' => 'datetime:Y-m-d H:i:s P' + ]; + + protected $dates = [ + 'created_at', + 'updated_at' + ]; + + protected $appends = [ + ]; + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function message() + { + return $this->belongsTo(Message::class); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + public function getContentAttribute($content) + { + $removed = __('misc.moderated'); + + return $this->message->isModerated() ? "[{$removed}]" : $content; + } + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + +} diff --git a/app/Models/Forum/MessageMention.php b/app/Models/Forum/MessageMention.php new file mode 100644 index 0000000..0e441c9 --- /dev/null +++ b/app/Models/Forum/MessageMention.php @@ -0,0 +1,72 @@ + 'datetime:Y-m-d H:i:s P', + 'updated_at' => 'datetime:Y-m-d H:i:s P' + ]; + + protected $dates = [ + 'created_at', + 'updated_at' + ]; + + protected $appends = [ + ]; + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function message() + { + return $this->belongsTo(Message::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeUser(Builder $builder, $user) + { + if($user instanceof User) { + $user = $user->id; + } + + return $builder->where('user_id', '=', $user); + } + + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + +} diff --git a/app/Models/Forum/MessageReaction.php b/app/Models/Forum/MessageReaction.php new file mode 100644 index 0000000..5b97fb2 --- /dev/null +++ b/app/Models/Forum/MessageReaction.php @@ -0,0 +1,59 @@ + 'datetime:Y-m-d H:i:s P', + 'updated_at' => 'datetime:Y-m-d H:i:s P' + ]; + + protected $dates = [ + 'created_at', + 'updated_at' + ]; + + protected $fillable = [ + 'user_id', + 'message_id', + 'type', + ]; + + protected $appends = [ + ]; + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function message() + { + return $this->belongsTo(Message::class, 'message_id'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + +} diff --git a/app/Models/Forum/Thread.php b/app/Models/Forum/Thread.php new file mode 100644 index 0000000..46a7f2c --- /dev/null +++ b/app/Models/Forum/Thread.php @@ -0,0 +1,399 @@ + 'integer', + 'created_by' => 'integer', + 'is_sticky' => 'boolean', + 'is_embedded' => 'boolean', + 'blocked_user' => 'boolean', + 'grownups_can_participate' => 'boolean', + 'created_at' => 'datetime:Y-m-d H:i:s P', + 'updated_at' => 'datetime:Y-m-d H:i:s P', + 'deleted_at' => 'datetime:Y-m-d H:i:s P', + ]; + + protected $dates = [ + 'created_at', + 'updated_at', + 'deleted_at', + ]; + + protected $appends = [ + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function (Thread $thread) { + + if (empty($thread->created_by) && Auth::check()) { + $thread->created_by = Auth::id(); + } + + }); + + } + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function messages() + { + return $this->hasMany(Message::class, 'thread_id'); + } + + public function originalMessage() + { + return $this->belongsTo(Message::class, 'original_message_id'); + } + + public function latestMessage() + { + return $this->hasOne(Message::class, 'thread_id')->orderBy('created_at', 'DESC'); + } + + public function acceptedAnswer() + { + return $this->hasOne(Message::class, 'id', 'accepted_answer_id'); + } + + public function mostPopularMessage() + { + return $this->hasOne(Message::class, 'id', 'most_popular_answer_id'); + } + + public function creator() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class, 'created_by') + ); + } + + public function project() + { + return $this->hasOne(Project::class, 'thread_id'); + } + + + public function topic() + { + return $this->belongsTo(Topic::class, 'topic_id'); + } + + public function ancestralTopics() + { + return $this->hasManyThrough(Topic::class, TopicAncestry::class, 'topic_id', 'id', 'topic_id', 'ancestor_id'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopePublic(Builder $q, bool $force = false) + { + if (User::currentUserCanModerate() && !$force) { + return $q; + } + + return $q->where('blocked_user', false) + ->whereIn('status', [ + SystemStatus::ACTIVE, + SystemStatus::LOCKED, + ]); + } + + public function scopeForUser(Builder $q, User $user): Builder + { + return $q->where('created_by', $user->id); + } + + public function scopeOrderByActivity(Builder $q, string $dir = 'asc') + { + return $q->orderBy( + Message::query() + ->select('created_at') + ->whereColumn('thread_id', 'forum_threads.id') + ->orderBy('created_at', $dir) + ->limit(1), + $dir + ); + } + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + + public function activate() + { + $this->status = SystemStatus::ACTIVE; + $this->save(); + } + + public function isActive() + { + return $this->status === SystemStatus::ACTIVE + && !$this->blocked_user + && (($this->is_embedded) ? $this->project->isActive() : true); + } + + public function lock() + { + $this->status = SystemStatus::LOCKED; + $this->save(); + } + + public function archive() + { + $this->status = SystemStatus::ARCHIVED; + $this->save(); + } + + public function updateMostPopularAnswer($silent = false) + { + $this->most_popular_answer_id = $this->messages() + ->withCount('likes') + ->having('likes_count', '>', 0) + ->orderBy('likes_count', 'desc') + ->take(1) + ->pluck('id') + ->first(); + + $this->save(); + + if(!$silent) { + $this->broadcastUpdate(); + } + } + + public function createMessage($content, $user = null, $attributes = []) + { + + if ($user === null) { + $user = Auth::user(); + } + + $message = new Message(); + $message->thread_id = $this->id; + $message->user_id = ($user !== null) ? $user->id : null; + $message->content = $content; + + if (!empty($attributes) && is_array($attributes)) { + foreach ($attributes as $attribute => $value) { + $message->{$attribute} = $value; + } + } + + $message->save(); + + CalculateSortScoreForModel::dispatch($this); + + return $message; + } + + public function hasMessages() + { + return $this->messages() + ->where('id', '<>', $this->original_message_id) + ->exists(); + } + + ////////////////////////////////// + /// Muting + ////////////////////////////////// + + public function getMutedAttribute() : bool + { + return optional(Auth::user())->threadMuted($this) ?? false; + } + + public function toggleMute() + { + optional(Auth::user())->toggleMuteThread($this); + + return $this; + } + + ////////////////////////////////// + /// Broadcasting + ////////////////////////////////// + + public function broadcastUpdate() + { + event(new Updated($this)); + } + + ////////////////////////////////// + /// Sorting + ////////////////////////////////// + + public function toggleSticky() + { + $this->is_sticky = !$this->is_sticky; + $this->save(); + + return $this; + } + + public function getSortFreshnessAge() + { + $message = $this->latestMessage; + + if ($message !== null) { + return $message->created_at->diffInDays(Carbon::now()); + } + + return $this->created_at->diffInDays(Carbon::now()); + } + + public function getRecentGlobalActivity() + { + $key = get_class($this) . '_get_recent_global_activity'; + + return Cache::driver('array')->rememberForever($key, function () { + $global_reaction_activity = MessageReaction::query()->type('like')->where('created_at', '>=', Carbon::now()->subDays(10))->count(); + $global_message_activity = Message::query()->where('created_at', '>=', Carbon::now()->subDays(10))->count(); + + return $global_reaction_activity * 0.8 + $global_message_activity * 0.2; + }); + } + + public function getContributionToRecentGlobalActivity() + { + $local_reaction_activity = MessageReaction::query() + ->type('like') + ->whereHas('message', function ($q) { + $q->where('thread_id', '=', $this->id); + }) + ->where('created_at', '>=', Carbon::now()->subDays(10)) + ->count(); + + $local_message_activity = $this->messages()->where('forum_messages.created_at', '>=', Carbon::now()->subDays(10))->count(); + + return $local_reaction_activity * 0.8 + $local_message_activity * 0.2; + } + + + ////////////////////////// + /// Moderation + ////////////////////////// + + public function getResponsibleUserId(): int + { + return $this->created_by; + } + + public function getModerationActionsAttribute(): array + { + return array_merge($this->getCommonModerationActions(), array_filter([ + LockModerateable::class, + ArchiveModerateable::class, + ActivateModerateable::class, + RemoveThreadMessageContent::class, + $this->removalHasBeenRequested() ? Delete::class : null, + ])); + } + + public function getCustomAutomaticResolutionActions(bool $willSuspendUser): array + { + $actions = []; + + // Voluntary moderation I.e. Removal request + if (!$willSuspendUser) { + $actions[ModerationActions::REMOVE_THREAD_MESSAGE_CONTENT] = []; + } + + // Regular Auto Moderation OR Voluntary but Empty + if ($willSuspendUser || !$this->hasMessages()) { + $actions[ModerationActions::LOCK_MODERATEABLE] = []; + } + + + return $actions; + } + + public function isDeleted() + { + return $this->created_by === null; + } + + public function delete() + { + optional($this->originalMessage()->first())->delete(); + + $this->subject = '[Slettet]'; + $this->created_by = null; + $this->saveWithoutLogging(); + + Change::query() + ->where(function (Builder $q) { + $q->where('changeable_type', static::class) + ->where('changeable_id', $this->id); + }) + ->where(function (Builder $q) { + $q->where('column_name', 'subject') + ->orWhere('column_name', 'created_by'); + }) + ->delete(); + } + + public function afterDeleteModeration(ModerationCase $case, ModerationAction $log) + { + if ($this->status === SystemStatus::ARCHIVED) { + return; + } + + if (!$this->hasMessages()) { + $case->performAsyncModeration(ArchiveModerateable::class, null, $log); + } + } + +} diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php new file mode 100644 index 0000000..e402fe9 --- /dev/null +++ b/app/Models/Forum/Topic.php @@ -0,0 +1,114 @@ +isDirty('parent_id') || $topic->wasRecentlyCreated) { + + $iterator = $topic; + + $ids = [$topic->id]; + + do { + if ($iterator->parent !== null) { + $iterator = $iterator->parent; + + $ids[] = $iterator->id; + + } + } while ($iterator->parent !== null); + + $topic->ancestors()->sync($ids); + + } + + }); + + } + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function descendantThreads() + { + return $this->hasManyThrough(Thread::class, TopicAncestry::class, 'ancestor_id', 'topic_id', 'id', 'topic_id'); + } + + public function threads() + { + return $this->hasMany(Thread::class, 'topic_id'); + } + + public function parent() + { + return $this->belongsTo(Topic::class, 'parent_id'); + } + + public function ancestors() + { + return $this->belongsToMany(Topic::class, (new TopicAncestry())->getTable(), 'topic_id', 'ancestor_id'); + } + + public function children() + { + return $this->hasMany(Topic::class, 'parent_id'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeWithParentName(Builder $q) { + return $q + ->leftJoin('forum_topics as parent', 'parent.id', '=', 'forum_topics.parent_id') + ->select(['forum_topics.*', 'parent.name as parent_name']); + } + + ////////////////////////////////// + /// Attribute getters + ////////////////////////////////// + + public function getLatestMessageAttribute() + { + return Message::query() + ->orderBy('updated_at', 'DESC') + ->whereHas('thread.ancestralTopics', function ($q) { + $q->where($this->getTable() . '.id', '=', $this->id); + }) + ->first(); + } + + ////////////////////////////////// + /// Attribute setters + ////////////////////////////////// + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + +} diff --git a/app/Models/Forum/TopicAncestry.php b/app/Models/Forum/TopicAncestry.php new file mode 100644 index 0000000..9344363 --- /dev/null +++ b/app/Models/Forum/TopicAncestry.php @@ -0,0 +1,17 @@ +case->performAsyncModeration(AppealAction::class, $appeal->message, $appeal); + }); + } + + /** + * The Moderation Case + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function case() + { + return $this->belongsTo(ModerationCase::class, 'moderation_case_id'); + } + + public function scopeForCase(Builder $q, $case = null) + { + if($case instanceof ModerationCase) { + $case = $case->id; + } + + return $q->where('moderation_case_id', $case); + } + +} diff --git a/app/Models/Moderation/ModerationAction.php b/app/Models/Moderation/ModerationAction.php new file mode 100644 index 0000000..a6b4502 --- /dev/null +++ b/app/Models/Moderation/ModerationAction.php @@ -0,0 +1,80 @@ + 'json', + ]; + + protected static function boot() + { + parent::boot(); + + static::created(function (ModerationAction $action) { + $action->case->triggerUpdatedEvent(); + }); + } + + /** + * The Moderation Case + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function case() + { + return $this->belongsTo(ModerationCase::class, 'moderation_case_id'); + } + + /** + * Author of the ModerationAction + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + public function getLabelAttribute() + { + return ModerationActions::translate($this->action_class, [ + 'moderateable' => $this->case->moderateable_label, + ])['log']; + } + + public function scopeForCase(Builder $q, $case = null) + { + if($case instanceof ModerationCase) { + $case = $case->id; + } + + return $q->where('moderation_case_id', $case); + } + + public function scopeType(Builder $q, string $type) + { + return $q->where('type', $type); + } + + public function scopeNewestFirst(Builder $q) + { + return $q->latest()->orderBy('id', 'desc'); + } + + public function scopeOldestFirst(Builder $q) + { + return $q->oldest()->orderBy('id', 'asc'); + } + +} diff --git a/app/Models/Moderation/ModerationCase.php b/app/Models/Moderation/ModerationCase.php new file mode 100644 index 0000000..557774c --- /dev/null +++ b/app/Models/Moderation/ModerationCase.php @@ -0,0 +1,274 @@ +forUser($case->user_id) + ->get()->each->triggerUpdatedEvent(); + }); + } + + ////////////////////////// + /// Relationships + ////////////////////////// + + /** + * The reported entity + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function moderateable() + { + return User::userRelationFallbackWrap( + $this->morphTo() + ); + } + + /** + * The User blamed for the entity + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class) + ); + } + + /** + * Moderation actions + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function actions() + { + return $this->hasMany(ModerationAction::class); + } + + /** + * Moderation requests + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function requests() + { + return $this->hasMany(ModerationRequest::class); + } + + /** + * Appeals + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function appeals() + { + return $this->hasMany(Appeal::class); + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeRelated(Builder $q, ModerationCase $case) + { + return $q->exclude($case->id)->forUser($case->user_id); + } + + public function scopeExclude(Builder $q, $ids) + { + return $q->whereNotIn('id', (array)$ids); + } + + public function scopeUnresolved(Builder $q) + { + return $q->whereIn('status', [ + ModerationCaseStatus::PENDING, + ModerationCaseStatus::AUTOMATICALLY_MODERATED + ]); + } + + /** + * @param Builder $q + * @param null $user + * @return mixed + */ + public function scopeForUser(Builder $q, $user = null) + { + if($user instanceof User) { + $user = $user->id; + } + + return $q->where('user_id', $user); + } + + public function scopeWithLastRequestedAt(Builder $q) + { + return $q->addSelect(['last_requested_at' => ModerationRequest::select('created_at') + ->whereColumn('moderation_case_id', 'moderation_cases.id') + ->orderBy('created_at', 'desc') + ->limit(1) + ]); + } + + public function scopeOrderByUsername(Builder $q, $dir = 'asc') + { + $userQuery = User::select('username') + ->whereColumn('id', 'moderation_cases.user_id') + ->limit(1); + + // If withTrashed is not applied, it messes up the ordering + User::currentUserCanModerate() ? $userQuery->withTrashed() : $userQuery; + + return $q->orderBy($userQuery, $dir); + } + + ////////////////////////// + /// Accessors + ////////////////////////// + + public function getIsPendingAttribute() + { + return $this->status === ModerationCaseStatus::PENDING; + } + + public function getIsResolvedAttribute() + { + return $this->status === ModerationCaseStatus::MODERATED; + } + + public function getModerateableLabelAttribute() + { + return trans_choice("models.{$this->moderateable_type}.specified", 1); + } + + ////////////////////////// + /// Events + ////////////////////////// + + public function triggerUpdatedEvent() + { + event(new Updated($this)); + } + + ////////////////////////// + /// Actions + ////////////////////////// + + public function getModerationActionsAttribute() + { + return $this->moderateable->moderation_actions; + } + + public function getManualModerationActionsAttribute() + { + return collect($this->moderateable->moderation_actions) + ->filter(function ($action) { + return $action::TYPE !== ModerationActionType::SYSTEM; + }) + ->mapWithKeys(function ($action) { + return [$action => ModerationActions::translate($action, [ + 'moderateable' => $this->moderateable_label, + ])['action'], + ]; + }); + } + + /** + * Perform a moderation action + * + * @param string $action + * @param string|null $note + * @param array $args + * @return mixed + */ + public function performModeration(string $action, string $note = null, ...$args) + { + if (!class_exists($action) || !in_array($action, $this->moderation_actions)) { + throw new ModerationActionException("Provided action ${action} is invalid", 400); + } + + return $action::execute($this, $note, ...$args); + } + + public function performAsyncModeration(string $action, string $note = null, ...$args) + { + PerformAsyncModeration::dispatch($this, $action, $note, ...$args); + } + + ////////////////////////// + /// Appeals + ////////////////////////// + + public function getAppealUrl() + { + return URL::signedRoute('app.appeal.index', [ + 'moderation_case' => $this->id, + 'identifier' => $this->getIdentifier(), + ]); + } + + public function getIdentifier() + { + return hash_hmac('sha256', static::class.'_'.$this->id, config('app.key')); + } + + public function hasValidIdentifier($identifier) + { + return hash_equals($this->getIdentifier(), $identifier); + } + + ////////////////////////// + /// Automatic resolution + ////////////////////////// + + public function needsAutomaticResolution() + { + if (!$this->is_pending) { + return false; + } + + return $this->requestsHaveReachedModerationThreshold() || + $this->requests()->unresolvedRemovalRequestForUserId($this->user_id)->exists(); + } + + public function requestsHaveReachedModerationThreshold() + { + return $this->getRequestsScore() >= config('permissions.moderation.threshold'); + } + + protected function getRequestsScore() + { + return $this->requests() + ->with('reporter.roles') + ->get() + ->pluck('moderation_weight') + ->sum(); + } +} diff --git a/app/Models/Moderation/ModerationRequest.php b/app/Models/Moderation/ModerationRequest.php new file mode 100644 index 0000000..ccc01f5 --- /dev/null +++ b/app/Models/Moderation/ModerationRequest.php @@ -0,0 +1,112 @@ +sendNotification(); + $request->case->triggerUpdatedEvent(); + $request->scheduleAutomaticModeration(); + }); + } + + /** + * The Moderation Case + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function case() + { + return $this->belongsTo(ModerationCase::class, 'moderation_case_id'); + } + + /** + * Author of the ModerationRequest + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function reporter() + { + return $this->belongsTo(User::class); + } + + public function scopeForCase(Builder $q, $case = null) + { + if($case instanceof ModerationCase) { + $case = $case->id; + } + + return $q->where('moderation_case_id', $case); + } + + public function scopeResolved(Builder $q, bool $resolved = true) + { + return $q->whereNull('resolved_at', 'and', $resolved); + } + + public function scopeUnresolvedRemovalRequestForUserId(Builder $q, int $userId) + { + $q->where('reporter_id', $userId) + ->where('reason', ModerationReasons::REMOVAL_REQUEST) + ->resolved(false); + } + + public function scopeOrderByUsername(Builder $q, $dir = 'asc') + { + $userQuery = User::select('username') + ->whereColumn('id', 'moderation_requests.reporter_id') + ->limit(1); + + // If withTrashed is not applied, it messes up the ordering + User::currentUserCanModerate() ? + $userQuery->withTrashed() : + $userQuery; + + return $q->orderBy($userQuery, $dir); + } + + + public function sendNotification() + { + User::query() + ->role('admin') // TODO moderators? + ->get() + ->each + ->notify(new NewModerationRequest($this)); + } + + public function scheduleAutomaticModeration() + { + AutomaticallyModerateCase::dispatch($this->case); + } + + public function getModerationWeightAttribute() + { + // Don't count toward total + if ($this->reason === ModerationReasons::REMOVAL_REQUEST) { + return 0; + } + + return $this->reporter->moderation_weight; + } +} diff --git a/app/Models/Moderation/UserSuspension.php b/app/Models/Moderation/UserSuspension.php new file mode 100644 index 0000000..06c5251 --- /dev/null +++ b/app/Models/Moderation/UserSuspension.php @@ -0,0 +1,124 @@ + 'boolean', + ]; + + protected $dates = [ + 'start_at', + 'end_at', + ]; + + protected $fillable = [ + 'start_at', + 'end_at', + 'issued_by', + ]; + + ////////////////////////// + /// Relations + ////////////////////////// + + public function user() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class, 'user_id') + ); + } + + public function issuer() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class, 'issued_by')->withDefault([ + 'username' => __('misc.automatic'), + ]) + ); + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeActive(Builder $q) + { + $now = now(); + + return $q + ->where('active', true) + ->where('start_at', '<=', $now) + ->where(function (Builder $q) use ($now) { + $q->whereNull('end_at') + ->orWhere('end_at', '>=', $now); + }); + } + + public function scopeForUser(Builder $q, $user) + { + if ($user instanceof User) { + $user = $user->id; + } + + return $q->where('user_id', $user); + } + + public function scopeOrderByUsername(Builder $q, $dir = 'asc') + { + $userQuery = User::select('username') + ->whereColumn('id', 'user_suspensions.user_id') + ->limit(1); + + // If withTrashed is not applied, it messes up the ordering + User::currentUserCanModerate() ? $userQuery->withTrashed() : $userQuery; + + return $q->orderBy($userQuery, $dir); + } + + public function scopeOrderByDuration(Builder $q, $dir = 'asc') + { + return $q->orderByRaw("TIMESTAMPDIFF(SECOND, start_at, end_at) {$dir}"); + } + + ////////////////////////// + /// Methods + ////////////////////////// + + public function deactivate() { + $this->active = false; + + return $this->save(); + } + + ////////////////////////// + /// Attributes + ////////////////////////// + + /** + * Returns number of seconds between start and end date + * + * @return int|string + */ + public function getDurationAttribute() + { + return is_null($this->end_at) ? + __('misc.indefinite') : + $this->start_at->diffInSeconds($this->end_at); + } + +} diff --git a/app/Models/News.php b/app/Models/News.php new file mode 100644 index 0000000..6dbb538 --- /dev/null +++ b/app/Models/News.php @@ -0,0 +1,47 @@ + 'boolean' + ]; + + const PURIFIER_CONFIG = 'admin'; + + protected $userGeneratedContent = [ + 'subtext', + ]; + + protected $hidden = [ + ]; + + protected $appends = [ + ]; + + protected static function boot() + { + parent::boot(); + } + + public function scopeIsPublished(Builder $q) + { + return $q->where( + 'publish_at', + '<', + now() + )->where('status', 'published'); + } + +} diff --git a/app/Models/Projects/Category.php b/app/Models/Projects/Category.php new file mode 100644 index 0000000..90d88dd --- /dev/null +++ b/app/Models/Projects/Category.php @@ -0,0 +1,56 @@ +belongsTo(Category::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(Category::class, 'parent_id'); + } + + public function projects() + { + return $this->belongsToMany(Project::class, 'project_project_categories'); + } + + public function scopeParent(Builder $q, bool $parent = true) + { + return $q->whereNull('parent_id', 'and', !$parent); + } + + public function scopePublic(Builder $q) + { + return $q->where(function (Builder $q) { + $q->parent(false); // Only children... + }) + ->orWhere(function (Builder $q) { + $q->has('children'); // ...or parents with children + }); + } + + public function getDisplayNameAttribute() + { + return join(' : ', array_filter([optional($this->parent)->name, $this->name])); + } + +} diff --git a/app/Models/Projects/Member.php b/app/Models/Projects/Member.php new file mode 100644 index 0000000..a1ade81 --- /dev/null +++ b/app/Models/Projects/Member.php @@ -0,0 +1,55 @@ + 'boolean', + ]; + + protected static function boot() + { + parent::boot(); + + self::created(function (Member $member) { + $member->sendProjectInvitation(); + }); + } + + ////////////////////////////////// + /// Relations + ////////////////////////////////// + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function project() + { + return $this->belongsTo(Project::class, 'project_id'); + } + + ////////////////////////////////// + /// Helpers + ////////////////////////////////// + + public function sendProjectInvitation() + { + if (!$this->accepted) { + $this->user->notify(new ProjectInvitation($this->project)); + } + } +} diff --git a/app/Models/Projects/Project.php b/app/Models/Projects/Project.php new file mode 100644 index 0000000..e3bdbdb --- /dev/null +++ b/app/Models/Projects/Project.php @@ -0,0 +1,513 @@ + 'boolean', + ]; + + protected $dates = [ + 'published_at', + ]; + + protected $userGeneratedContent = [ + 'description', + ]; + + public static $customPermissions = [ + CustomPermissions::MANAGE_PROJECT_MEMBERS, + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function (Project $project) { + if (empty($project->owner_id) && Auth::check()) { + $project->owner_id = Auth::id(); + } + }); + + static::created(function (Project $project) { + $project->seedThread(); + }); + + static::saving(function (Project $project) { + if ($project->published_at === null && $project->status === GenericStatus::PUBLISHED && $project->isDirty('status')) { + $project->published_at = now(); + } + }); + + static::deleting(function (Project $project) { + // We need to load the "files" relation before delete + // Because the pivot table get's delete on DB level (cascade) + $project->load('files'); + }); + static::deleted(function (Project $project) { + // only call cleanup() after final deletion + if ($project->isForceDeleting()) { + $project->cleanup(); + } + }); + } + + public function owner() + { + return User::userRelationFallbackWrap( + $this->belongsTo(User::class, 'owner_id') + ); + } + + public function users() + { + return $this->belongsToMany(User::class) + ->using(Member::class) + ->withPivot('accepted'); + } + + public function members() + { + return $this->belongsToMany(User::class) + ->using(Member::class) + ->withPivot('accepted') + ->wherePivot('accepted', true); + } + + public function categories() + { + return $this->belongsToMany(Category::class, 'project_project_categories'); + } + + public function reactions() + { + return $this->hasMany(ProjectReaction::class, 'project_id'); + } + + public function coverImage() + { + return $this->belongsTo(File::class); + } + + public function thumbnail() + { + return $this->belongsTo(File::class); + } + + public function files() + { + return $this->belongsToMany(File::class, 'project_files'); + } + + public function images() + { + return $this->files()->where('mime', 'LIKE', 'image/%'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopePublished(Builder $q, $published = true) + { + $status = $published ? GenericStatus::PUBLISHED : GenericStatus::DRAFT; + + return $q->where('status', $status); + } + + public function scopePublic(Builder $q, bool $public = true, bool $force = false) + { + if (User::currentUserCanModerate() && !$force) { + return $q; + } + + return $q->where('blocked_user', false) + ->whereIn( + 'system_status', + [SystemStatus::ACTIVE, SystemStatus::LOCKED], + 'and', + !$public + ); + } + + public function scopeForCategories(Builder $q, array $categories) + { + foreach ($categories as $category) { + $q->whereHas('categories', function (Builder $q) use ($category) { + return $q->where('slug', $category); + }); + } + + return $q; + } + + /** + * All user related projects, including: owned, invited & accepted + * + * @param Builder $q + * @param User|int $user + * @param bool $includeInvites If false, we will exclude unaccepted invites + * @return Builder + */ + public function scopeRelatedToUser(Builder $q, $user, bool $includeInvites = true) + { + $id = $user instanceof User ? $user->id : $user; + $relation = $includeInvites ? 'users' : 'members'; + + return $q->ownedBy($id) + ->orWhereHas($relation, function ($q) use ($id) { + return $q->where('user_id', $id); + }); + } + + public function scopeOwnedBy(Builder $q, $user) + { + $id = $user instanceof User ? $user->id : $user; + + return $q->where('owner_id', $id); + } + + public function scopeForUser(Builder $q, User $user): Builder + { + return $q->ownedBy($user); + } + + public function scopeAwaitingResponse(Builder $q, $user) + { + $id = $user instanceof User ? $user->id : $user; + + return $q->whereHas('users', function ($q) use ($id) { + return $q->where('user_id', $id)->where('accepted', false); + }); + } + + public function scopeMostPopular(Builder $q) + { + return $q->orderBy('sort_score', 'desc'); + } + + public function scopeLeastPopular(Builder $q) + { + return $q->orderBy('sort_score', 'asc'); + } + + + ////////////////////////////////// + /// Attributes + ////////////////////////////////// + + public function getIsPublishedAttribute() + { + return $this->status === GenericStatus::PUBLISHED; + } + + public function getCoverImageUrlAttribute() + { + return optional($this->coverImage)->url ?? asset('images/projects/cover.png'); + } + + public function getThumbnailUrlAttribute() + { + return optional($this->thumbnail)->url ?? asset('images/projects/thumbnail.png'); + } + + public function getDescriptionAttribute($description) + { + return $this->correctUserContent($description); + } + + + ////////////////////////////////// + /// Users + ////////////////////////////////// + + public function getAllUsersAttribute() + { + if (!Auth::check() || !Auth::user()->can(CustomPermissions::MANAGE_PROJECT_MEMBERS, $this)) return []; + + return $this->users; + } + + public function userIsOwner(User $user) + { + return $user->id === $this->owner_id; + + } + + public function userIsMember(User $user) + { + return $this->userIsOwner($user) + || $this->members()->where('user_id', $user->id)->exists(); + } + + public function userIsRelated(User $user) + { + return $this->userIsOwner($user) + || $this->users()->where('user_id', $user->id)->exists(); + } + + ////////////////////////////////// + /// Invites + ////////////////////////////////// + + /** + * Sets current users invitation to accepted, or deletes their invitation. + * + * @param bool $accepted + * @return int|null + */ + public function resolveInvitation(bool $accepted = true) + { + if (!Auth::check()) return null; + + return $accepted ? + $this->users()->updateExistingPivot(Auth::id(), ['accepted' => true]) : + $this->users()->detach(Auth::id()); + } + + ////////////////////////////////// + /// Thread + ////////////////////////////////// + + public function thread() + { + return $this->belongsTo(Thread::class, 'thread_id'); + } + + public function threadMessages() + { + return $this->hasManyThrough(Message::class, Thread::class, 'id', 'thread_id', 'thread_id', 'id'); + } + + public function seedThread() + { + + if ($this->thread !== null) { + return; + } + + $thread = new Thread(); + + $thread->is_embedded = true; + $thread->type = ForumThreadType::DISCUSSION; + $thread->status = 'active'; + + $thread->save(); + + $this->thread_id = $thread->id; + $this->save(); + + } + + ////////////////////////////////// + /// Sorting + ////////////////////////////////// + + public function getSortFreshnessAge() + { + + if ($this->thread) { + return $this->thread->getSortFreshnessAge(); + } + + return $this->created_at->diffInDays(Carbon::now()); + } + + public function getRecentGlobalActivity() + { + $key = get_class($this) . '_get_recent_global_activity'; + + return Cache::driver('array')->rememberForever($key, function () { + + $global_reaction_activity = ProjectReaction::query()->type('like')->where('created_at', '>=', Carbon::now()->subDays(10))->count(); + $global_message_reaction_activity = MessageReaction::query()->type('like')->where('created_at', '>=', Carbon::now()->subDays(10))->count(); + $global_message_activity = Message::query()->where('created_at', '>=', Carbon::now()->subDays(10))->count(); + + return $global_reaction_activity * 0.7 + $global_message_reaction_activity * 0.25 + $global_message_activity * 0.05; + }); + } + + public function getContributionToRecentGlobalActivity() + { + $local_reaction_activity = $this->likes() + ->where('created_at', '>=', Carbon::now()->subDays(10)) + ->count(); + + $local_message_reaction_activity = MessageReaction::query() + ->type('like') + ->whereHas('message', function ($q) { + $q->where('thread_id', '=', $this->thread_id); + }) + ->where('created_at', '>=', Carbon::now()->subDays(10)) + ->count(); + + $local_message_activity = $this->threadMessages()->where('forum_messages.created_at', '>=', Carbon::now()->subDays(10))->count(); + + return $local_reaction_activity * 0.7 + $local_message_reaction_activity * 0.25 + $local_message_activity * 0.05; + } + + protected function handleReaction() + { + CalculateSortScoreForModel::dispatch($this); + } + + + ////////////////////////// + /// System Status + ////////////////////////// + + public function isActive() + { + return $this->system_status === SystemStatus::ACTIVE && !$this->blocked_user; + } + + public function isArchived() + { + return $this->system_status === SystemStatus::ARCHIVED || $this->blocked_user; + } + + public function activate() + { + $this->system_status = SystemStatus::ACTIVE; + $this->save(); + } + + public function lock() + { + $this->system_status = SystemStatus::LOCKED; + $this->save(); + } + + public function archive() + { + $this->system_status = SystemStatus::ARCHIVED; + $this->save(); + } + + + ////////////////////////// + /// Moderation + ////////////////////////// + + public function getResponsibleUserId(): int + { + return $this->owner_id; + } + + public function getModerationActionsAttribute(): array + { + return array_merge($this->getCommonModerationActions(), [ + LockModerateable::class, + ArchiveModerateable::class, + ActivateModerateable::class, + ]); + } + + public function getCustomAutomaticResolutionActions(bool $willSuspendUser): array + { + return [ + ModerationActions::LOCK_MODERATEABLE => [], + ]; + } + + + ////////////////////////// + /// Misc + ////////////////////////// + + private function cleanup() + { + + // Eager loaded, to trigger "delete" event + // Which removes the actual files + + if($this->coverImage !== null) { + $this->coverImage->delete(); + } + + if($this->thumbnail !== null) { + $this->thumbnail->delete(); + } + + if($this->files->isNotEmpty()) { + $this->files->each->delete(); + } + + if($this->thread !== null) { + $this->thread->delete(); + } + + } + + public static function getNewsletterHiglights($count = 3) + { + $baseQuery = static::query()->public()->published(); + + $mostPopular = (clone $baseQuery) + ->whereBetween('published_at', [today()->startOfWeek(), today()->endOfWeek()]) + ->mostPopular() + ->limit($count) + ->get(); + + if ($mostPopular->count() < $count) { + $mostPopular = $mostPopular->concat( + (clone $baseQuery) + ->orderBy('published_at', 'desc') + ->whereNotIn('id', $mostPopular->pluck('id')) + ->limit($count - max(0, $mostPopular->count())) + ->get() + ); + } + + return $mostPopular->map(function (Project $project) { + return [ + 'title' => $project->title, + 'alt' => $project->title, + 'img' => $project->thumbnail_url, + 'link' => route('app.projects.project', ['project' => $project->id]), + ]; + }); + } +} diff --git a/app/Models/Projects/ProjectReaction.php b/app/Models/Projects/ProjectReaction.php new file mode 100644 index 0000000..055852d --- /dev/null +++ b/app/Models/Projects/ProjectReaction.php @@ -0,0 +1,72 @@ +notify(); + }); + + static::deleted(function (ProjectReaction $projectReaction) { + $projectReaction->notify(); + }); + } + + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function project() + { + return $this->belongsTo(Project::class, 'project_id'); + } + + ////////////////////////////////// + /// Notification + ////////////////////////////////// + + protected function notifyUsers(Notification $notification) + { + $this->project->members + ->prepend($this->project->owner) + ->each + ->notify($notification); + } + + protected function notify() + { + if ($this->type !== ReactionType::ENDORSEMENT) { + return $this->notifyUsers(new ReactionNotification($this->project)); + } + + // Only notify on "first" endorsement + if ($this->exists && $this->project->reactions()->endorsement(true)->count() === 1) { + return $this->notifyUsers(new EndorsementNotification($this->project)); + } + } +} diff --git a/app/Models/Regions/Region.php b/app/Models/Regions/Region.php new file mode 100644 index 0000000..1f95394 --- /dev/null +++ b/app/Models/Regions/Region.php @@ -0,0 +1,109 @@ +belongsToMany(Zipcode::class)->using(RegionZipcode::class); + } + + /** + * Parses $zipcodes, and calls attach() on the zipcodes relation. + * @see Region::parseZipcodes() + * + * @param array $zipcodes + * @return void + */ + public function attachZipcodes(array $zipcodes) + { + $zipcodes = static::parseZipcodes($zipcodes); + + $this->zipcodes()->attach($zipcodes); + } + + /** + * Parses $zipcodes, and calls detach() on the zipcodes relation. + * @see Region::parseZipcodes() + * + * @param array $zipcodes + * @return int + */ + public function detachZipcodes(array $zipcodes) + { + $zipcodes = static::parseZipcodes($zipcodes); + + return $this->zipcodes()->detach($zipcodes); + } + + /** + * Parses $zipcodes, and calls sync() on the zipcodes relation. + * @see Region::parseZipcodes() + * + * @param array $zipcodes + * @return array + */ + public function syncZipcodes(array $zipcodes) + { + $zipcodes = static::parseZipcodes($zipcodes); + + return $this->zipcodes()->sync($zipcodes); + } + + /** + * Parses an array of zipcodes, and return all existing zipcodes. + * Supports "ranges" of zipcodes like [[5000, 5500]], and converts them to individual zipcodes. + * + * @example input $zipcodes = [5000, 5220, 5999, [1000, 2000]] + * @example result [5000, 5220, 1473, 1799, 1974, 2000] + * + * @param array $zipcodes + * @return array + */ + public static function parseZipcodes(array $zipcodes) + { + $all_zipcodes = Zipcode::getAll(); + $ids = []; + + foreach ($zipcodes as $zipcode) { + if (is_int($zipcode) && $all_zipcodes->contains('zipcode', $zipcode)) { + $ids[] = $zipcode; + continue; + } + + if (is_array($zipcode) && count($zipcode) >= 2) { + $from = array_shift($zipcode); + $to = array_pop($zipcode); + + if (is_int($from) && is_int($to)) { + $ids = array_merge($ids, + $all_zipcodes->whereBetween('zipcode', [$from, $to]) + ->pluck('zipcode') + ->all()); + } + + continue; + } + } + + return $ids; + } +} diff --git a/app/Models/Regions/RegionZipcode.php b/app/Models/Regions/RegionZipcode.php new file mode 100644 index 0000000..33ad76e --- /dev/null +++ b/app/Models/Regions/RegionZipcode.php @@ -0,0 +1,26 @@ +distinct() + ->pluck('zipcode_zipcode'); + } + +} diff --git a/app/Models/Regions/Zipcode.php b/app/Models/Regions/Zipcode.php new file mode 100644 index 0000000..e87dcf2 --- /dev/null +++ b/app/Models/Regions/Zipcode.php @@ -0,0 +1,41 @@ +belongsToMany(Region::class)->using(RegionZipcode::class); + } + + /** + * Returns all Zipcodes + * + * @return Collection + */ + public static function getAll() + { + return Cache::store('array')->rememberForever('all_zipcodes', function(){ + return Zipcode::query()->get(); + }); + } +} diff --git a/app/Models/Rewards/UserReward.php b/app/Models/Rewards/UserReward.php new file mode 100644 index 0000000..51ff637 --- /dev/null +++ b/app/Models/Rewards/UserReward.php @@ -0,0 +1,130 @@ + 'datetime:Y-m-d H:i:s O', + ]; + + protected $userGeneratedContent = [ + 'description', + ]; + + protected static function boot() + { + parent::boot(); + + self::created(function (UserReward $reward) { + $reward->user->notify(new UserRewardGranted($reward)); + }); + } + + ////////////////////////////////// + /// Relationships + ////////////////////////////////// + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function achievement() + { + return $this->belongsTo(Achievement::class, 'achievement_id'); + } + + public function achievementItems() + { + return $this->hasMany(AchievementItem::class, 'achievement_id', 'achievement_id'); + } + + public function rewardItems() + { + return $this->hasMany(UserRewardItem::class, 'user_reward_id'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeOpened(Builder $q, bool $opened = true) + { + return $q->whereNull('opened_at', 'and', $opened); + } + + public function scopeForUser(Builder $q, $user = null) + { + if($user instanceof User) { + $user = $user->id; + } + + if (is_numeric($user)) { + return $q->where('user_id', '=', $user); + } + + return $q->where('user_id', Auth::id()); + } + + public function scopeHasItemType(Builder $q, $type) + { + return $q->whereHas('achievementItems', function ($q) use ($type) { + return $q->isOfType($type); + }); + } + + ////////////////////////////////// + /// Methods + ////////////////////////////////// + + /** + * Mark the UserReward as opened, and grant rewards to the user + * + * @return Collection of \App\Models\Rewards\UserRewardItems + * @throws Exception + */ + public function open() + { + if ($this->opened_at !== null) { + throw new Exception('UserReward has already been opened.'); + } + + return DB::transaction(function () { + $this->opened_at = now(); + $this->save(); + + return $this->achievementItems() + ->get() + ->map + ->giveToUser($this->user, $this) + ->each + ->load('item'); + }); + + } +} diff --git a/app/Models/Rewards/UserRewardItem.php b/app/Models/Rewards/UserRewardItem.php new file mode 100644 index 0000000..2f93ddc --- /dev/null +++ b/app/Models/Rewards/UserRewardItem.php @@ -0,0 +1,70 @@ +belongsTo(User::class, 'user_id'); + } + + public function item() + { + return $this->morphTo(); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeWhereItem(Builder $q, Model $item) + { + return $q->whereItemType(get_class($item))->whereItemId($item->id); + } + + public function scopeIsOfType($q, $type) + { + + if (in_array($type, Rewardable::keys())) { + $type = Rewardable::getValue($type); + } + + return $q->where('item_type', '=', $type); + } + + public function scopeForUser($q, $user = null) + { + if($user instanceof User) { + $user = $user->id; + } + + if (is_numeric($user)) { + return $q->where('user_id', '=', $user); + } + + return $q->where('user_id', Auth::id()); + } +} diff --git a/app/Models/Rewards/UserTitle.php b/app/Models/Rewards/UserTitle.php new file mode 100644 index 0000000..bc2b8c1 --- /dev/null +++ b/app/Models/Rewards/UserTitle.php @@ -0,0 +1,26 @@ +selectTitle($this); + } + + public function getSelectedByUserAttribute() + { + return optional(Auth::user())->title_id === $this->id; + } +} diff --git a/app/Models/User/ReservedUsername.php b/app/Models/User/ReservedUsername.php new file mode 100644 index 0000000..4adbd81 --- /dev/null +++ b/app/Models/User/ReservedUsername.php @@ -0,0 +1,64 @@ +updateOrCreate( + ['identifier' => $identifier], + ['username' => $username] + ); + } + + /** + * Deletes a ReservedUsername based on the identifier + * + * @param string|null $identifier + */ + public static function removeReservation(string $identifier = null): void + { + static::query()->whereIdentifier($identifier)->delete(); + } + + public static function deleteExpired() + { + return static::expired()->delete(); + } + + /////////////////////////// + /// Scopes + /////////////////////////// + + public function scopeExpired(Builder $q) + { + return $q->where( + 'updated_at', + '<', + now()->subMinutes(config('session.lifetime')) + ); + } +} diff --git a/app/Models/User/User.php b/app/Models/User/User.php new file mode 100644 index 0000000..8ccad68 --- /dev/null +++ b/app/Models/User/User.php @@ -0,0 +1,665 @@ + 'date', + ]; + + public $dates = [ + 'email_verified_at', + 'last_activity_at', + ]; + + protected $appends = [ + 'avatar', + 'zipcode', + 'is_blocked', + 'role_name', + 'has_pending_accusations', + 'title', + 'uncompleted_landlubber_requirements', + ]; + + protected $userGeneratedContent = [ + 'description', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function (User $user) { + + $user->email = $user->email ?? $user->parent_email; + + if(!$user->password) { + $user->password = bcrypt(Str::random(20)); + } + + if(!$user->age) { + $user->age = 0; + } + + }); + + static::created(function (User $user) { + $user->userAvatar()->associate(UserAvatar::setup($user)); + $user->setMeta('notification_settings', static::defaultNotificationSettings(), 'json'); + $user->save(); + }); + + static::restored(function (User $user) { + $user->restoreContent(); + }); + + static::addGlobalScope('default_withs', function($q) { + $q->with('userTitle'); + $q->with('metaAttributes'); + }); + + static::saving(function (User $user) { + + if($user->isGrownup()) { + + if($user->isDirty('email')) { + $user->email_verified_at = null; + } + } + + }); + + static::saved(function (User $user) { + + // When user fills out parent_email the first time + if($user->isChild()) { + if ($user->isDirty('parent_email') + && $user->getOriginal('parent_email') === null + && $user->parent_email !== null + && !$user->hasVerifiedEmail()) { + $user->sendEmailVerificationNotification(); + } + } + + if($user->hasRole(UserRoles::LANDLUBBER)) { + $user->attemptToUpgradeLandlubber(); + } + + if($user->isGrownup()) { + if($user->wasChanged(['email'])) { + $user->sendEmailVerificationNotification(); + } + } + + + }); + + } + + ////////////////////////// + /// Avatar + ////////////////////////// + + public function userAvatar() + { + return $this->belongsTo(UserAvatar::class); + } + + public function getAvatarAttribute() + { + return UserAvatar::getSvgById($this->user_avatar_id) ?? UserAvatar::removedAvatarSvg(); + } + + + ////////////////////////// + /// User fallback + ////////////////////////// + + public static function userRelationFallbackWrap(Relation $relation) + { + return static::currentUserCanModerate() ? + $relation->withTrashed() : + $relation->withDefault(static::getRemovedUserData()); + } + + public static function getRemovedUserData() + { + return [ + 'username' => __('misc.unknown'), + ]; + } + + ////////////////////////// + /// Scopes + ////////////////////////// + + public function scopeCurrentUser(Builder $q) + { + return $q->user(); + } + + public function scopeUser(Builder $q, $user = null) + { + if($user instanceof User) { + $user = $user->id; + } + + if (is_int($user)) { + return $q->where('id', '=', $user); + } + + return $q->where('id', Auth::id()); + } + + public function scopeStatus(Builder $q, $status) + { + switch ($status) { + case UserStatus::ACTIVE: + return $q; + case UserStatus::BLOCKED: + return $q->onlyTrashed(); + default: + return $q->withTrashed(); + } + } + + public function sendPasswordResetNotification($token) + { + $this->notify(new ResetPassword($token)); + } + + ////////////////////////// + /// Permissions & Roles + ////////////////////////// + + public function getUserPermissionsAttribute() + { + return PermissionsService::getUserPermissions($this); + } + + public function getModerationWeightAttribute() + { + return config('permissions.moderation.weight.' . optional($this->roles->first())->name, 1); + } + + public function getRoleNameAttribute() + { + return optional($this->roles->first())->name; + } + + public function isChild() + { + return $this->hasRole(UserRoles::children()); + } + + public function isGrownup() + { + return $this->hasRole(UserRoles::grownups()); + } + + ////////////////////////// + /// Zipcode + ////////////////////////// + + public function getZipcodeAttribute() + { + return $this->getMeta('zipcode'); + } + + public function scopeForRegions(Builder $q, array $regions) + { + $zips = RegionZipcode::getZipsForRegions($regions)->all(); + + return $q->forZipcodes($zips); + } + + public function scopeForZipcodes(Builder $q, array $zips) + { + return $q->whereHas('metaAttributes', function ($q) use ($zips) { + return $q->where('name', 'zipcode') + ->whereIn('value', $zips); + }); + } + + + ////////////////////////// + /// Content + ////////////////////////// + + public function getDescriptionAttribute($description) + { + return $this->correctUserContent($description); + } + + + ////////////////////////// + /// Notifications + ////////////////////////// + + /** + * Perform all checks for whether a notification should be sent to the user + * + * @param $instance + * @return bool + */ + public function shouldNotify($instance) : bool + { + if (property_exists($instance, 'type') && $instance::$type !== null) { + // Check general notification settings + if (!$this->canNotify($instance::$type)) { + return false; + } + + // Check for muted forum threads + if (in_array($instance::$type, NotificationType::getForumNotifications()) && isset($instance->thread)) { + if ($this->threadMuted($instance->thread)) { + return false; + } + } + } + + return true; + } + + /** + * Check if the user has disabled the specific type of notification + * + * @param string $type + * @return bool + */ + public function canNotify(string $type) : bool + { + // We have to assume that default behavior is true: + // If new types get added later, they might be ignored, until the settings are updated + return optional($this->getMeta('notification_settings'))->{$type} !== false; + } + + public function toggleMuteThread(Thread $thread) + { + $this->threadMuted($thread) ? $this->unmuteThread($thread) : $this->muteThread($thread); + } + + /** + * Turns all notification off for the specified thread + * + * @param Thread $thread + */ + public function muteThread(Thread $thread) + { + $key = 'muted_threads'; + $existing = $this->getMeta($key, []); + + $this->setMeta($key, array_unique(array_merge($existing, [$thread->id])), 'json'); + } + + /** + * Unmutes notifications for Thread + * + * @param Thread $thread + */ + public function unmuteThread(Thread $thread) + { + $key = 'muted_threads'; + $existing = $this->getMeta($key, []); + + $this->setMeta($key, array_diff($existing, [$thread->id]), 'json'); + } + + /** + * Check if thread is muted by user + * + * @param Thread $thread + * @return bool + */ + public function threadMuted(Thread $thread) : bool + { + return in_array($thread->id, $this->getMeta('muted_threads', [])); + } + + public function getNotificationSettingsAttribute() + { + $this->getResponsibleUserId(); + return $this->getMeta('notification_settings'); + } + + /** + * Returns keys for all notification types which are allowed to be editable + * + * @return array + */ + public static function editableNotifications() + { + return array_keys(static::defaultNotificationSettings()); + } + + /** + * Default user notification settings + * This is also where we essentially define which notifications can be opted out of + * As these serve as the baseline for editable notifications + * + * @return array + */ + public static function defaultNotificationSettings() + { + return [ + NotificationType::WEEKLY_NEWSLETTER => false, + NotificationType::FORUM_REACTION => true, + NotificationType::FORUM_MENTIONED => true, + NotificationType::FORUM_NEW_COMMENT => true, + ]; + } + + ////////////////////////// + /// Moderation + ////////////////////////// + + public function getResponsibleUserId(): int + { + return $this->id; + } + + public function moderationAccusations() + { + return $this->hasMany(ModerationCase::class, 'user_id'); + } + + public static function currentUserCanModerate(): bool + { + return (bool)optional(Auth::user())->can(CommonPermissions::MODERATE); + } + + public function getHasPendingAccusationsAttribute() + { + return static::currentUserCanModerate() ? + $this->moderationAccusations()->unresolved()->exists() : + false; + } + + public function getIsBlockedAttribute() + { + return static::currentUserCanModerate() ? + $this->trashed() : + false; + } + + public function removeAllContent() + { + Project::blockForUser($this); + Message::blockForUser($this); + Thread::blockForUser($this); + } + + public function restoreContent() + { + Project::unblockForUser($this); + Message::unblockForUser($this); + Thread::unblockForUser($this); + } + + ////////////////////////// + /// Suspensions + ////////////////////////// + + public function suspensions() + { + return $this->hasMany(UserSuspension::class, 'user_id'); + } + + public function getIsSuspendedAttribute() + { + return $this->suspensions()->active()->exists(); + } + + /** + * Creates a UserSuspension entry for the supplied period + * + * @param Carbon|null $from If null provided, we use now(). + * @param Carbon $to + * @param User|null $issuer + */ + public function suspend(?Carbon $from, ?Carbon $to, User $issuer = null) + { + $this->suspensions()->create([ + 'start_at' => $from ?? now(), + 'end_at' => $to, + 'issued_by' => optional($issuer)->id, + ]); + } + + ////////////////////////// + /// Achievements / Rewards + ////////////////////////// + + public function rewards() + { + return $this->hasMany(UserReward::class); + } + + public function rewardItems() + { + return $this->hasMany(UserRewardItem::class); + } + + public function userTitle() + { + return $this->belongsTo(UserTitle::class, 'title_id'); + } + + public function selectTitle(UserTitle $title) + { + if ($this->rewardItems()->whereItem($title)->exists()) { + $this->userTitle()->associate($title); + + return $this->save(); + } + + return false; + } + + public function getTitleAttribute() + { + if($this->userTitle !== null) { + return $this->userTitle->title; + } + + return ''; + } + + ////////////////////////// + /// Landlubber logic + ////////////////////////// + + public function attemptToUpgradeLandlubber() + { + if ($this->canUpgradeToPirate()) { + $this->syncRoles(UserRoles::PIRATE); + + UpgradedToPirate::dispatch($this); + } + } + + public function canUpgradeToPirate() + { + if (!$this->hasRole(UserRoles::LANDLUBBER)) { + return false; + } + + return empty($this->uncompleted_landlubber_requirements); + } + + public function getUncompletedLandlubberRequirementsAttribute() + { + $uncompleted = []; + + foreach (LandlubberRequirements::attributes() as $required_attribute) { + if (empty($this->getAttribute($required_attribute))) { + $uncompleted[] = $required_attribute; + } + } + + foreach (LandlubberRequirements::meta() as $required_meta_key) { + if ($this->hasMeta($required_meta_key) === false) { + $uncompleted[] = $required_meta_key; + } + } + + return $uncompleted; + } + + ////////////////////////// + /// Parent Email verification + ////////////////////////// + + public function getEmailForVerification() + { + if($this->isGrownup()) { + return $this->email; + } + + return $this->parent_email; + } + + // \Illuminate\Notifications\RoutesNotifications->routeNotificationFor() + public function routeNotificationForMail($notification) + { + if ($notification instanceof VerifyEmail) { + return $this->getEmailForVerification(); + } + + return $this->email; + } + + public function sendEmailVerificationNotification() + { + $this->notify(new VerifyEmail); + } + + ////////////////////////// + /// Statistics + ////////////////////////// + + public function refreshLastActivity() + { + $now = now(); + + if($this->last_activity_at === null || $this->last_activity_at->diffInSeconds($now) > 30) { + static::query() + ->whereKey($this->id) + ->update(['last_activity_at' => $now]); + } + } + + public function getAgeAttribute($age) + { + if ($this->birthday !== null && $this->birthday instanceof Carbon) { + return $this->birthday->age; + } + + return $age; + } + + public function courseProgresses() + { + return $this->hasMany(CourseProgress::class, 'user_id'); + } + + public function threads() + { + return $this->hasMany(Thread::class, 'created_by'); + } + + public function messages() + { + return $this->hasMany(Message::class, 'user_id'); + } + + public function messageReactions() + { + return $this->hasManyThrough( + MessageReaction::class, + Message::class, + 'user_id', // Foreign key on messages table... + 'message_id', // Foreign key on message_reactions table... + 'id', // Local key on users table... + 'id' // Local key on messages table... + ); + } + + public function projects() + { + return $this->hasMany(Project::class, 'owner_id'); + } + + public function participatingProjects() + { + return $this->belongsToMany(Project::class) + ->using(Member::class) + ->withPivot('accepted') + ->wherePivot('accepted', true); + } + + public function projectReactions() + { + return $this->hasManyThrough( + ProjectReaction::class, + Project::class, + 'owner_id', // Foreign key on projects table... + 'project_id', // Foreign key on project_reactions table... + 'id', // Local key on users table... + 'id' // Local key on projects table... + ); + } + +} diff --git a/app/Models/User/UserMeta.php b/app/Models/User/UserMeta.php new file mode 100644 index 0000000..a9f2c98 --- /dev/null +++ b/app/Models/User/UserMeta.php @@ -0,0 +1,20 @@ +belongsTo(User::class); + } +} diff --git a/app/Notifications/Auth/ResetPassword.php b/app/Notifications/Auth/ResetPassword.php new file mode 100644 index 0000000..c3d81a1 --- /dev/null +++ b/app/Notifications/Auth/ResetPassword.php @@ -0,0 +1,71 @@ +token = $token; + + self::onQueue('notifications'); + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->subject(Lang::get('emails.reset_password.subject')) + ->greeting(Lang::get('emails.greetings.casual')) + ->line(Lang::get('emails.reset_password.line_1')) + ->line(Lang::get('emails.reset_password.line_2')) + ->action(Lang::get('emails.reset_password.action'), url(config('app.url').route('app.password_reset.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false))) + // ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')])) + ->line(Lang::get('emails.reset_password.line_3')) + ->salutation(Lang::get('emails.salutation')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/Auth/VerifyEmail.php b/app/Notifications/Auth/VerifyEmail.php new file mode 100644 index 0000000..366b073 --- /dev/null +++ b/app/Notifications/Auth/VerifyEmail.php @@ -0,0 +1,68 @@ +verificationUrl($notifiable); + + return (new MailMessage) + ->subject(Lang::get('emails.verify_parent_email.subject')) + ->greeting(Lang::get('emails.greetings.polite', ['title' => Lang::get('misc.parent')])) + ->line(Lang::get('emails.verify_parent_email.line_1')) + ->line(Lang::get('emails.verify_parent_email.line_2', ['url' => route('app.pages.about')])) + ->line(Lang::get('emails.verify_parent_email.line_3', ['username' => $notifiable->username, 'url' => route('app.pirate.pirate', ['username' => $notifiable->username])])) + ->line(Lang::get('emails.verify_parent_email.line_4')) + ->line(Lang::get('emails.verify_parent_email.line_5')) + ->line(Lang::get('emails.verify_parent_email.line_6')) + ->action(Lang::get('emails.verify_parent_email.action'), $verificationUrl) + ->salutation(Lang::get('emails.salutation')); + } + + /** + * Get the verification URL for the given notifiable. + * + * @param mixed $notifiable + * @return string + */ + protected function verificationUrl($notifiable) + { + return URL::temporarySignedRoute( + 'auth.register.verify_email', + Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), + [ + 'id' => $notifiable->getKey(), + 'hash' => sha1($notifiable->getEmailForVerification()), + ] + ); + } +} diff --git a/app/Notifications/EventReminderNotification.php b/app/Notifications/EventReminderNotification.php new file mode 100644 index 0000000..a0f548a --- /dev/null +++ b/app/Notifications/EventReminderNotification.php @@ -0,0 +1,57 @@ +event = $event; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'type' => static::$type, + 'event_id' => $this->event->id, + 'status' => NotificationStatus::ACTIVE, + 'title' => trans(static::transKey('title')), + 'msg' => trans(static::transKey('msg'), [ + 'event' => $this->event->title, + 'start' => $this->event->pretty_start, + ]), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'event_id' => $this->event->id, + ]; + } +} diff --git a/app/Notifications/Forum/MessageReaction.php b/app/Notifications/Forum/MessageReaction.php new file mode 100644 index 0000000..fffe84a --- /dev/null +++ b/app/Notifications/Forum/MessageReaction.php @@ -0,0 +1,95 @@ +message = $message; + $this->thread = $thread; // Need this for when entire Thread is muted by user + + self::onQueue('notifications'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + // Count of all OTHER reactions, so don't include this one + $count = $this->message->reactions()->endorsement(false)->count() - 1; + + if ($count < 0) { + return [ + 'message_id' => $this->message->id, + 'status' => NotificationStatus::DISABLED, + ]; + } + + $class = $this->message->is_original ? Thread::class : Message::class; + $username = $this->message->reactions() + ->endorsement(false) + ->latest() + ->first()->user->username; + + $params = [ + 'user' => $username, + 'message' => trans_choice("models.{$class}.unspecified", 1), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'message_id' => $this->message->id, + + // Link + 'route' => 'app.forum.message', + 'parameters' => ['thread' => $this->thread->id, 'message' => $this->message->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans_choice(static::transKey('msg'), $count, $params), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'message_id' => $this->message->id, + ]; + } +} diff --git a/app/Notifications/Forum/NewEndorsement.php b/app/Notifications/Forum/NewEndorsement.php new file mode 100644 index 0000000..f3ac275 --- /dev/null +++ b/app/Notifications/Forum/NewEndorsement.php @@ -0,0 +1,91 @@ +message = $message; + $this->thread = $thread; // Need this for when entire Thread is muted by user + + self::onQueue('notifications'); + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + if (!$notifiable->shouldNotify($this)) { + return []; + } + + return ['broadcast', 'database']; + } + + protected static function transKey(string $key) + { + return 'notifications.' . static::$type . ".{$key}"; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $class = $this->message->is_original ? Thread::class : Message::class; + + $params = [ + 'message' => trans_choice("models.{$class}.unspecified", 1), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + + // Link + 'route' => 'app.forum.thread', + 'parameters' => ['thread' => $this->thread->id, '#thread-message-' . $this->message->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans(static::transKey('msg'), $params), + ]; + } +} diff --git a/app/Notifications/Forum/NewMessage.php b/app/Notifications/Forum/NewMessage.php new file mode 100644 index 0000000..2841d03 --- /dev/null +++ b/app/Notifications/Forum/NewMessage.php @@ -0,0 +1,67 @@ +thread = $thread; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $count = $this->thread->messages() + ->whereDoesntHave('user', function ($q) use ($notifiable) { + return $q->where('id', $notifiable->id); + }) + ->count(); + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'thread_id' => $this->thread->id, + + // Link + 'route' => 'app.forum.thread', + 'parameters' => ['thread' => $this->thread->id], // TODO - link to specific message + + // Message + 'title' => trans(static::transKey('title')), + 'msg' => trans_choice(static::transKey('msg'), $count), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'thread_id' => $this->thread->id, + ]; + } +} diff --git a/app/Notifications/Forum/UserMentioned.php b/app/Notifications/Forum/UserMentioned.php new file mode 100644 index 0000000..8ae8c0a --- /dev/null +++ b/app/Notifications/Forum/UserMentioned.php @@ -0,0 +1,85 @@ +message = $message; + $this->thread = $thread; // Need this for when entire Thread is muted by user + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + if (!$this->message->mentions()->user($notifiable)->exists()) { + return [ + 'status' => NotificationStatus::DISABLED, + 'message_id' => $this->message->id, + ]; + } + + $class = $this->message->is_original ? Thread::class : Message::class; + $params = [ + 'user' => data_get($this->message, 'user.username'), + 'message' => trans_choice("models.{$class}.unspecified", 1), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'message_id' => $this->message->id, + + // Link + 'route' => 'app.forum.thread', + 'parameters' => ['thread' => $this->thread->id, '#thread-message-' . $this->message->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans(static::transKey('msg'), $params), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'message_id' => $this->message->id, + ]; + } +} diff --git a/app/Notifications/Moderation/NewModerationRequest.php b/app/Notifications/Moderation/NewModerationRequest.php new file mode 100644 index 0000000..66162ca --- /dev/null +++ b/app/Notifications/Moderation/NewModerationRequest.php @@ -0,0 +1,76 @@ +request = $request; + $this->case = $request->case; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + // Count of all OTHER requests, so don't include this one + $count = $this->case->requests()->count() - 1; + + $username = $this->request->reporter->username; + $moderateable = __('enums.models.' . $this->case->moderateable_type); + + $params = [ + 'user' => $username, + 'moderateable' => $moderateable, + ]; + + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'case_id' => $this->case->id, + + // Link + 'route' => 'backend.moderation.view_case', + 'parameters' => ['moderation_case'=> $this->case->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans_choice(static::transKey('msg'), $count, $params), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'case_id' => $this->case->id, + ]; + } +} diff --git a/app/Notifications/Projects/NewEndorsement.php b/app/Notifications/Projects/NewEndorsement.php new file mode 100644 index 0000000..56a68b6 --- /dev/null +++ b/app/Notifications/Projects/NewEndorsement.php @@ -0,0 +1,85 @@ +project = $project; + + self::onQueue('notifications'); + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + // TODO / should this be muteable + // if (!$notifiable->shouldNotify($this)) { + // return []; + // } + + return ['broadcast', 'database']; + } + + protected static function transKey(string $key) + { + return 'notifications.' . static::$type . ".{$key}"; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $class = Project::class; + + $params = [ + 'title' => $this->project->title, + 'project' => trans_choice("models.{$class}.unspecified", 1), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + + // Link + 'route' => 'app.projects.project', + 'parameters' => ['project' => $this->project->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans(static::transKey('msg'), $params), + ]; + } +} diff --git a/app/Notifications/Projects/ProjectInvitation.php b/app/Notifications/Projects/ProjectInvitation.php new file mode 100644 index 0000000..6665854 --- /dev/null +++ b/app/Notifications/Projects/ProjectInvitation.php @@ -0,0 +1,84 @@ +project = $project; + + self::onQueue('notifications'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $invitation = $this->project + ->users + ->first(function ($user) use ($notifiable) { + return $user->id === $notifiable->id; + }); + + // If invitation doesn't exist, or user has already accepted, don't notify + if ($invitation === null || $invitation->pivot->accepted) { + return [ + 'project_id' => $this->project->id, + 'status' => NotificationStatus::DISABLED, + ]; + } + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'project_id' => $this->project->id, + + // Link + 'route' => 'app.projects.project', + 'parameters' => ['project' => $this->project->id], + + // Message + 'title' => trans(static::transKey('title')), + 'msg' => trans(static::transKey('msg'), [ + 'owner' => $this->project->owner->username, + 'project' => $this->project->title, + ]), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'project_id' => $this->project->id, + ]; + } +} diff --git a/app/Notifications/Projects/ProjectReaction.php b/app/Notifications/Projects/ProjectReaction.php new file mode 100644 index 0000000..ecc1ea1 --- /dev/null +++ b/app/Notifications/Projects/ProjectReaction.php @@ -0,0 +1,87 @@ +project = $project; + + self::onQueue('notifications'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + // Count of all OTHER reactions, so don't include this one + $count = $this->project->reactions()->endorsement(false)->count() - 1; + + if ($count < 0) { + return [ + 'project_id' => $this->project->id, + 'status' => NotificationStatus::DISABLED, + ]; + } + + $class = Project::class; + $username = $this->project->reactions() + ->endorsement(false) + ->latest() + ->first()->user->username; + + $params = [ + 'user' => $username, + 'project' => trans_choice("models.{$class}.unspecified", 1), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'project_id' => $this->project->id, + + // Link + 'route' => 'app.projects.project', + 'parameters' => ['project' => $this->project->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans_choice(static::transKey('msg'), $count, $params), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'project_id' => $this->project->id, + ]; + } +} diff --git a/app/Notifications/System/NewContactSubmission.php b/app/Notifications/System/NewContactSubmission.php new file mode 100644 index 0000000..15e7910 --- /dev/null +++ b/app/Notifications/System/NewContactSubmission.php @@ -0,0 +1,73 @@ +submission = $submission; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + + $params = [ + 'SubmissionType' => ContactSubmissionType::translate($this->submission->type), + ]; + + return [ + // General + 'type' => static::$type, + 'status' => NotificationStatus::ACTIVE, + 'contact_submission_id' => $this->submission->id, + + // Link + 'route' => 'backend.contact.edit', + 'parameters' => ['contact_submission' => $this->submission->id], + + // Message + 'title' => trans(static::transKey('title'), $params), + 'msg' => trans(static::transKey('msg'), $params), + ]; + } + + private function getIdentifiers(): array + { + return [ + 'contact_submission_id' => $this->submission->id, + ]; + } +} diff --git a/app/Notifications/UserRewardGranted.php b/app/Notifications/UserRewardGranted.php new file mode 100644 index 0000000..2e82a1e --- /dev/null +++ b/app/Notifications/UserRewardGranted.php @@ -0,0 +1,72 @@ +reward = $reward; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $count = $notifiable->rewards()->opened(false)->count(); + + if ($count < 1) { + return [ + 'type' => static::$type, + 'user_id' => $notifiable->id, + 'status' => NotificationStatus::DISABLED, + ]; + } + + return [ + // General + 'type' => static::$type, + 'user_id' => $notifiable->id, + 'status' => NotificationStatus::ACTIVE, + + // Link + 'route' => 'app.pirate.pirate', + 'parameters' => ['username' => $notifiable->username], + + // Message + 'title' => trans(static::transKey('title')), + 'msg' => trans_choice(static::transKey('msg'), $count), + ]; + } + + private function getIdentifiers($notifiable): array + { + return [ + 'user_id' => $notifiable->id, + ]; + } +} diff --git a/app/Operations/Action.php b/app/Operations/Action.php new file mode 100644 index 0000000..cebc07e --- /dev/null +++ b/app/Operations/Action.php @@ -0,0 +1,27 @@ +genericGetSet('trigger', $value); + } + + public function handle($model_or_collection = null) + { + $item = parent::handle($model_or_collection); + + if($item === null || $this->trigger() === null) { + return $item; + } + + return $item->{$this->trigger()}(); + } + +} diff --git a/app/Operations/Avatars/RandomAvatar.php b/app/Operations/Avatars/RandomAvatar.php new file mode 100644 index 0000000..98b90aa --- /dev/null +++ b/app/Operations/Avatars/RandomAvatar.php @@ -0,0 +1,24 @@ +all(), [ + 'user' => 'required|integer|exists:users,id', + ])->validate(); + + $achievement = parent::handle($achievement); + $user = User::findOrFail(request()->input('user')); + + if($achievement === null) { + return abort(404); + } + + /** @var Achievement $achievement */ + return $achievement->grantAchievement($user); + } +} diff --git a/app/Operations/Content/Post.php b/app/Operations/Content/Post.php new file mode 100644 index 0000000..b32a8c1 --- /dev/null +++ b/app/Operations/Content/Post.php @@ -0,0 +1,42 @@ +path('pages/{path}'); + } + + public function handle($model = null) + { + + $path = request()->route()->parameter('path'); + + $post = \App\Models\Content\Post::query()->visible()->wherePath($path)->first(); + + if($post === null) { + return abort(404); + } + + $this->openGraphMeta = $post->openGraphMeta; + + return parent::handle($model); + } + + public function getPageEnvironment() + { + return array_merge( + parent::getPageEnvironment(), + [ + 'og' => $this->openGraphMeta, + ] + ); + } + +} diff --git a/app/Operations/Courses/AnswerQuestion.php b/app/Operations/Courses/AnswerQuestion.php new file mode 100644 index 0000000..5cd0548 --- /dev/null +++ b/app/Operations/Courses/AnswerQuestion.php @@ -0,0 +1,76 @@ +filters([ + Filter::create()->always(function (Builder $q) { + $q->whereType(ResourceTypes::QUESTIONNAIRE); + }), + ]); + + $this->rules([ + 'question' => 'required|string', + 'answer' => 'required|string', + ]); + } + + protected function beforePipes() + { + return array_merge([ + Validates::create()->rules($this->rules), + ], parent::beforePipes()); + } + + public function handle($data) + { + + $answer_was_correct = false; + + if ($data && $data->checkAnswer(request()->question, request()->answer)) { + $answer_was_correct = true; + } + $progress = CourseProgress::query()->mine()->where('course_resource_id', '=', $data->id)->first(); + + if($progress === null) { + $progress = new CourseProgress(); + $progress->user_id = Auth::id(); + $progress->course_resource_id = $data->id; + } + + $progress_meta = array_merge( + (is_array($progress->meta)?$progress->meta:[]), + [ + request()->question => [ + 'answer' => request()->answer, + 'correct' => $answer_was_correct + ] + ] + ); + + $progress->meta = $progress_meta; + $progress->status = ResourceProgress::COMPLETED; + $progress->save(); + + return $data; + } +} diff --git a/app/Operations/Courses/Breadcrumbs.php b/app/Operations/Courses/Breadcrumbs.php new file mode 100644 index 0000000..ec7b1ff --- /dev/null +++ b/app/Operations/Courses/Breadcrumbs.php @@ -0,0 +1,130 @@ +filters([ + Filter::create()->when('category_id', function () {}), + Filter::create()->when('course_id', function () {}), + Filter::create()->when('step_id', function () {}), + ]); + } + + protected function beforePipes() + { + + $request = request(); + + $keyValue = ''; + + if ($request->has('category_id')) { + $this->model(CourseCategory::class); + $keyValue = request()->get('category_id'); + } + + if ($request->has('course_id')) { + $this->model(Course::class); + $keyValue = request()->get('course_id'); + } + + if ($request->has('step_id')) { + $this->model(CourseResource::class); + $keyValue = request()->get('step_id'); + } + + return [ + QueryModel::create()->model($this->model)->filters($this->filters)->operation($this), + QueryToInstance::create()->keyValue($keyValue)->operation($this), + TransformToView::create()->appends($this->appends), + ]; + } + + public function handle($entity = null) + { + + $breadcrumbs = collect(); + + while ($entity !== null) { + + if($entity instanceof CourseResource) { + + $course = optional($entity->course); + + $breadcrumbs->prepend((object)[ + 'label' => optional((object)$entity->meta)->title, + 'route' => 'app.courses.step', + 'parameters' => [ + 'category' => optional($course->category)->slug, + 'step' => $entity->id, + 'course' => $course->id, + 'course_slug' => $course->slug + ], + ]); + + $entity = $entity->course; + } + + if ($entity instanceof Course) { + + $breadcrumbs->prepend((object)[ + 'label' => $entity->title, + 'route' => 'app.courses.course', + 'parameters' => [ + 'category' => optional($entity->category)->slug, + 'course' => $entity->id, + 'course_slug' => $entity->slug + ], + ]); + + $entity = $entity->category; + } + + if ($entity instanceof CourseCategory) { + + $breadcrumbs->prepend((object)[ + 'label' => $entity->title, + 'route' => 'app.courses.courses', + 'parameters' => ['category' => $entity->slug], + ]); + + $breadcrumbs->prepend((object)[ + 'label' => 'Kodehavet', + 'route' => 'app.courses.overview', + ]); + + $entity = null; + } + + } + + if ($breadcrumbs->isNotEmpty()) { + $breadcrumbs->transform(function ($breadcrumb) { + if (isset($breadcrumb->route) && isset($breadcrumb->parameters)) { + $breadcrumb->url = route($breadcrumb->route, $breadcrumb->parameters); + } + return $breadcrumb; + }); + } + + return $breadcrumbs; + } + + +} diff --git a/app/Operations/Courses/MakeProgress.php b/app/Operations/Courses/MakeProgress.php new file mode 100644 index 0000000..b2c980c --- /dev/null +++ b/app/Operations/Courses/MakeProgress.php @@ -0,0 +1,34 @@ +data) { + + $this->data->progress()->where('user_id', Auth::id())->delete(); + + $newProgress = new CourseProgress(); + $newProgress->status = ResourceProgress::COMPLETED; + $newProgress->user_id = Auth::id(); + $newProgress->course_id = $this->data->id; + $newProgress->course_category_id = $this->data->category_id; + $newProgress->save(); + + return $newProgress; + } + } + + +} diff --git a/app/Operations/Excel/DownloadExcelExport.php b/app/Operations/Excel/DownloadExcelExport.php new file mode 100644 index 0000000..238b6c5 --- /dev/null +++ b/app/Operations/Excel/DownloadExcelExport.php @@ -0,0 +1,49 @@ +exportableCallback instanceof Closure, + new Exception('Thats not a Closure') + ); + + $export = $this->exportableCallback->call($this); + + if($export instanceof Responsable) { + return $export; + } + + return Excel::download( + $export, + $this->exportAs + ); + } + + public function exportable($exportable): DownloadExcelExport + { + $this->exportableCallback = $exportable; + + return $this; + } + + public function exportAs(string $exportName) + { + $this->exportAs = $exportName; + + return $this; + } + +} diff --git a/app/Operations/Files/Download.php b/app/Operations/Files/Download.php new file mode 100644 index 0000000..4889045 --- /dev/null +++ b/app/Operations/Files/Download.php @@ -0,0 +1,63 @@ +genericGetSet('from', $value); + } + + public function handle($model_or_collection = null) + { + $item = parent::handle($model_or_collection); + + if($item === null) { + return; + } + + if($this->from) { + $files = $item->{$this->from}; + + if($files instanceof Collection) { + + return response()->streamDownload(function() use ($files) { + + $opt = new Archive(); + + $opt->setContentType('application/octet-stream'); + + $zip = new ZipStream("uploads.zip", $opt); + + + foreach ($files as $file) { + try { + $file_stream = $file->storage->readStream($file->path); + $zip->addFileFromStream($file->filename, $file_stream); + } + catch (\Exception $e) { + Log::error("unable to read the file at storage path: $file->path and output to zip stream. Exception is " . $e->getMessage()); + } + + } + + $zip->finish(); + }, 'uploads.zip'); + + } + + } + + } + +} diff --git a/app/Operations/Forum/Breadcrumbs.php b/app/Operations/Forum/Breadcrumbs.php new file mode 100644 index 0000000..a4977a2 --- /dev/null +++ b/app/Operations/Forum/Breadcrumbs.php @@ -0,0 +1,87 @@ +has('thread_id')) { + $this->model(Thread::class); + $keyValue = request()->get('thread_id'); + } + + if ($request->has('topic_id')) { + $this->model(Topic::class); + $keyValue = request()->get('topic_id'); + } + + return [ + QueryModel::create()->model($this->model)->filters($this->filters)->operation($this), + QueryToInstance::create()->keyValue($keyValue)->operation($this), + TransformToView::create()->appends($this->appends), + ]; + } + + public function handle($entity = null) + { + + $breadcrumbs = collect(); + + while($entity !== null) { + + if($entity instanceof Thread) { + + $breadcrumbs->prepend((object) [ + 'label' => $entity->subject, + 'route' => 'app.forum.thread', + 'parameters' => ['thread' => $entity->id], + 'url' => route('app.forum.thread', ['thread' => $entity->id]) + ]); + + $entity = $entity->topic; + + } + + if($entity instanceof Topic) { + + $breadcrumbs->prepend((object) [ + 'label' => $entity->name, + 'route' => 'app.forum.topic', + 'parameters' => ['topic' => $entity->id, 'topic_slug' => $entity->slug], + 'url' => route('app.forum.topic', ['topic' => $entity->id, 'topic_slug' => $entity->slug]) + ]); + + $entity = $entity->parent; + + } + + } + + $breadcrumbs->prepend((object) [ + 'label' => 'Piratsnak', + 'route' => 'app.forum.overview', + 'parameters' => [], + 'url' => route('app.forum.overview', []) + ]); + + return $breadcrumbs; + } + + +} diff --git a/app/Operations/Forum/RedirectToMessage.php b/app/Operations/Forum/RedirectToMessage.php new file mode 100644 index 0000000..87e1756 --- /dev/null +++ b/app/Operations/Forum/RedirectToMessage.php @@ -0,0 +1,43 @@ +forceRedirect(true); + } + + public function handle($except = null) + { + + + $thread_id = request()->route('thread'); + $message_id = request()->route('message'); + + $message = Message::query() + ->where('thread_id', '=', $thread_id) + ->where('id', '=', $message_id) + ->first(); + + if($message === null) { + abort(400, 'Unable to find message'); + } + + $page = $message->findPageInThread(); + + $url = route('app.forum.thread', ['thread' => $message->thread_id]) . '?$page='.$page.'#thread-message-' . $message->id; + + return response()->redirectTo($url); + } + + +} diff --git a/app/Operations/Forum/SendMessage.php b/app/Operations/Forum/SendMessage.php new file mode 100644 index 0000000..2f37bf5 --- /dev/null +++ b/app/Operations/Forum/SendMessage.php @@ -0,0 +1,53 @@ +model(Thread::class); + $this->rules([ + 'message' => 'required|string|filled_html', + ]); + } + + protected function beforePipes() + { + return array_merge([ + Validates::create()->rules($this->rules), + ], parent::beforePipes()); + } + + public function handle($thread = null) + { + /// Check if the user created another message less than 30 seconds ago - if, then abort + $lastMessage = Message::query()->forUser(Auth::user())->orderBy('created_at', 'desc')->first(); + if ($lastMessage !== null) { + $seconds_since = $lastMessage->created_at->diffInSeconds(now(), true); + if($seconds_since < 30) { + $this->badRequest('Du kan ikke sende flere beskeder så hurtigt efter hinaden. Prøv igen om ' . (30 - $seconds_since) . ' sekunder'); + } + } + + $message = $thread->createMessage(request()->input('message')); + + event(new MessageCreated($message)); + + return $message; + } + + +} diff --git a/app/Operations/Interactions/Reaction.php b/app/Operations/Interactions/Reaction.php new file mode 100644 index 0000000..0c1a4dd --- /dev/null +++ b/app/Operations/Interactions/Reaction.php @@ -0,0 +1,39 @@ +reaction_type = $type; + + return $this; + } + + public function handle($reactable = null) + { + validator(request()->all(), [ + 'react' => 'required|boolean', + ])->validate(); + + $reactable = parent::handle($reactable); + + if($reactable === null) { + return abort(404); + } + + $type = $this->reaction_type; + $react = filter_var(request()->input('react'), FILTER_VALIDATE_BOOLEAN); + + return $reactable->triggerReaction($type, $react); + } + + +} diff --git a/app/Operations/Interactions/Reply.php b/app/Operations/Interactions/Reply.php new file mode 100644 index 0000000..7ada67e --- /dev/null +++ b/app/Operations/Interactions/Reply.php @@ -0,0 +1,37 @@ +genericGetSet('trigger', $value); + } + + public function handle($item = null) + { + + validator(request()->all(), [ + 'reply' => 'required|boolean', + ])->validate(); + + $item = parent::handle($item); + + if($item === null || $this->trigger() === null) { + return $item; + } + + $reply = filter_var(request()->input('reply'), FILTER_VALIDATE_BOOLEAN); + + return $item->{$this->trigger()}($reply); + } + + +} diff --git a/app/Operations/Login.php b/app/Operations/Login.php new file mode 100644 index 0000000..e4d12b5 --- /dev/null +++ b/app/Operations/Login.php @@ -0,0 +1,73 @@ +username; + } + + public function handle($model_or_collection = null) + { + $this->username = $this->findUsername(); + + parent::handle($model_or_collection); + } + + /** + * Attempt to log the user into the application. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function attemptLogin(Request $request) + { + $remember = (bool)$request->input('remember', 0); + + return $this->guard()->attempt( + $this->credentials($request), $remember + ); + } + + protected function authenticated(Request $request, $user) + { + $message = trans('auth.logged_in', ['username' => $user->username]); + $session = $request->session(); + + if ($session && Str::contains($session->get('url.intended', ''), + route('auth.register.verify_email', ['', ''], false))) { + $message = trans('auth.email_verification_post_login'); + } + + $this->setMessage($message); + + return parent::authenticated($request, $user); + } + + /** + * Get the login username to be used by the controller. + * + * @return string + */ + public function findUsername(): string + { + $login = request()->input('login'); + + $fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; + + request()->merge([$fieldType => $login]); + + return $fieldType; + } +} diff --git a/app/Operations/Moderation/Moderate.php b/app/Operations/Moderation/Moderate.php new file mode 100644 index 0000000..b4efe1c --- /dev/null +++ b/app/Operations/Moderation/Moderate.php @@ -0,0 +1,53 @@ +model(ModerationCase::class); + $this->view(['with' => ['moderateable']]); + } + + public function handle($case = null) + { + validator(request()->all(), [ + 'action' => 'required|string|in:' . implode(',', $case->moderation_actions), + 'note' => 'required|string', + 'meta' => 'nullable|array', + ])->validate(); + + try { + $result = $case->performModeration( + request()->action, + request()->note, + request()->except(['action', 'note']) + ); + } + catch (ModerationActionException $e) { + $this->setMessage($e->getMessage()); + $this->setStatusCode($e->getCode()); + + return null; + } + + $this->setMessage($result === false ? null : 'Success'); + $this->setStatusCode(200); + + return $result; + } + + +} diff --git a/app/Operations/Moderation/NextModerationCase.php b/app/Operations/Moderation/NextModerationCase.php new file mode 100644 index 0000000..54561a8 --- /dev/null +++ b/app/Operations/Moderation/NextModerationCase.php @@ -0,0 +1,36 @@ +route('except'); + + $q = ModerationCase::query(); + + if($except !== null) { + $q->exclude([$except]); + } + + $q->unresolved(); + + $case = $q->first(); + + if($case === null) { + return redirect()->back(); + } + + return redirect()->route('backend.moderation.view_case', ['moderation_case' => $case->id]); + } + + +} diff --git a/app/Operations/Moderation/RequestModeration.php b/app/Operations/Moderation/RequestModeration.php new file mode 100644 index 0000000..7b5ae3b --- /dev/null +++ b/app/Operations/Moderation/RequestModeration.php @@ -0,0 +1,34 @@ +all(), array_merge([ + 'reason' => 'required|string|in:' . implode(',', ModerationReasons::values()), + 'comment' => 'required|string', + ], $this->validationRules))->validate(); + + $this->setMessage($this->success_message ?? 'Tak for din henvendelse!'); + + return $moderateable->flag(request()->reason, request()->comment); + } + + public function validationRules(array $rules = []) + { + $this->validationRules = $rules; + + return $this; + } + +} diff --git a/app/Operations/Refresh.php b/app/Operations/Refresh.php new file mode 100644 index 0000000..44265a0 --- /dev/null +++ b/app/Operations/Refresh.php @@ -0,0 +1,36 @@ +model(User::class); + $this->filters([ + ScopeFilter::create()->always()->scope('currentUser'), + ]); + $this->single(); + } + + public function handle($user = null) + { + $permissions = PermissionsService::getUserPermissions($user); + + if ($user !== null) { + $user->unsetRelation('roles'); // Removes duplicate data from response + } + + return [ + 'user' => $user, + 'csrf' => csrf_token(), + 'user_permissions' => $permissions, + ]; + } +} diff --git a/app/Operations/Register.php b/app/Operations/Register.php new file mode 100644 index 0000000..d0f9f3d --- /dev/null +++ b/app/Operations/Register.php @@ -0,0 +1,247 @@ +input('step', ''); + $data = request()->all(); + + switch ($step) { + case 'user': + $this->doUserStep($data); + $next = 'avatar'; + break; + + case 'avatar': + return $this->doAvatarStep($data); + + default: + $this->resetSession(); + $next = 'user'; + } + + $res = Arr::except( + session()->get('register', []), + ['password', 'unique_key'] + ); + $res['step'] = $next; + + return ['model' => $res]; + } + + + ///////////////////////////// + /// Steps + ///////////////////////////// + + protected function doUserStep(array $data) + { + $identifier = session()->get('register.unique_key'); + + validator($data, array_merge( + ['password' => ['required', 'string', 'min:8', 'confirmed']], + static::getUserValidationRules($identifier) + ))->validate(); + + ReservedUsername::reserveUsername($data['username'], $identifier); + + $this->saveStep([ + 'name' => $data['name'], + 'age' => $data['age'], + 'email' => $data['email'], + 'newsletter' => (bool)($data['newsletter'] ?? false), + 'username' => $data['username'], + 'password' => Hash::make($data['password']), + ]); + } + + protected function doAvatarStep(array $data) + { + validator($data, static::getAvatarValidationRules())->validate(); + + // Only saved expected category values + $values = array_intersect_key($data, + array_flip(AvatarCategory::values())); + + $this->saveStep($values); + $this->saveStep([ + 'avatar' => $this->getAvatarItems(), // TODO - If the "shuffle" button get's implemented this could be removed. + ]); + + $identifier = session()->get('register.unique_key'); + + $all_rules = array_merge( + ['password' => ['required', 'string']], + static::getUserValidationRules($identifier), + static::getAvatarValidationRules() + ); + + $user_data = session()->get('register'); + + if (validator($user_data, $all_rules)->fails()) { + abort(400, 'Bad request.'); + } + + // Setup user + $user = $this->createUser($user_data); + $user->assignRole(UserRoles::LANDLUBBER); + + // Setup avatar + $user->userAvatar->setItems($user_data); + $user->userAvatar->save(); + + ReservedUsername::removeReservation($identifier); + session()->forget('register'); + + return $this->registered(request(), $user); + } + + + ///////////////////////////// + /// Validation rules + ///////////////////////////// + + protected static function getUserValidationRules(string $identifier) + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'age' => ['required', 'integer', 'min:3', 'max:150'], + 'email' => ['required', 'string', 'email', 'unique:users,email', 'max:255'], + 'newsletter' => ['nullable', 'boolean'], + 'username' => [ + 'required', + 'string', + 'alpha_dash_dot', + 'max:25', + "unique:reserved_usernames,username,{$identifier},identifier", + 'unique:users,username', + ], + ]; + } + + protected static function getAvatarValidationRules() + { + return collect(AvatarField::create('') + ->getValidationRules((new UserAvatar()))) + ->map(function ($values) { + return array_merge(['required'], $values); + }) + ->toArray(); + } + + + ///////////////////////////// + /// Registration + ///////////////////////////// + + protected function createUser(array $data) + { + $user = new User(); + $user->name = $data['name']; + $user->age = $data['age']; + $user->email = $data['email']; + $user->username = $data['username']; + $user->password = $data['password']; + $user->save(); + + $user->setMeta( + 'notification_settings', + array_merge(User::defaultNotificationSettings(), [ + NotificationType::WEEKLY_NEWSLETTER => $data['newsletter'], + ]), + 'json' + ); + + return $user; + } + + protected function registered(Request $request, $user) + { + event(new Registered($user)); + $this->guard()->login($user); + + $permissions = PermissionsService::getUserPermissions($user); + $user->unsetRelation('roles'); // Removes duplicate data from response + + $this->setMessage(trans('auth.registered', ['username' => $user->username])); + + return [ + 'user' => $user, + 'csrf' => csrf_token(), + 'user_permissions' => $permissions, + ]; + } + + + ///////////////////////////// + /// Helpers + ///////////////////////////// + + protected function resetSession() + { + ReservedUsername::removeReservation( + session()->get('register.unique_key', null) + ); + + session()->forget('register'); + + /// This unique key is only used for "reserving" usernames + /// in the multi step register process + $this->saveStep([ + 'unique_key' => uniqid('', true), + 'avatar' => $this->getAvatarItems(), // TODO - If the "shuffle" button get's implemented this could be removed. + ]); + } + + protected function saveStep(array $data) + { + session()->put('register', + array_merge(session()->get('register', []), $data)); + } + + protected function getAvatarItems() + { + $items = []; + $default = AvatarItem::getDefaultItems()->toArray(); + $categories = AvatarCategory::values(); + + $session = session()->get('register', []); + $ids = array_intersect_key($session, array_flip($categories)); + $saved = AvatarItem::query() + ->whereIn('id', array_values($ids)) + ->get() + ->keyBy('category'); + + foreach ($categories as $category) { + $items[$category] = optional($saved->get($category))->toArray() ?? $default[$category]; + } + + return $items; + } +} diff --git a/app/Operations/ResetPassword.php b/app/Operations/ResetPassword.php new file mode 100644 index 0000000..fa4fd81 --- /dev/null +++ b/app/Operations/ResetPassword.php @@ -0,0 +1,22 @@ +unsetRelation('roles'); // Removes duplicate data from response + + return array_merge(parent::sendResetResponse($request, $response), [ + 'user_permissions' => $permissions, + ]); + } +} diff --git a/app/Policies/ForumMessagePolicy.php b/app/Policies/ForumMessagePolicy.php new file mode 100644 index 0000000..e1bee7f --- /dev/null +++ b/app/Policies/ForumMessagePolicy.php @@ -0,0 +1,67 @@ +is_suspended // User can't be blocked + && !$message->isDeleted() + && $user->id === optional($message->thread)->created_by // Can only choose accepted answer on owned thread + && $user->id !== $message->user_id // Can't choose own answer + && optional($message->thread)->type === ForumThreadType::QUESTION; // The thread has to be a question + } + + public function store(User $user, Message $message) + { + return !$user->is_suspended // User can't be blocked + && !$message->isDeleted() + && $user->id === $message->user_id // Can only edit own comments + && !$message->isModerated($user) // Can't edit moderated comments + && $message->thread->isActive(); // Can't edit comments in locked/archived threads + } + + public function like(User $user, Message $message) + { + return $this->reaction($user, $message); + } + + public function endorse(User $user, Message $message) + { + return $this->reaction($user, $message); + } + + private function reaction(User $user, Message $message) + { + return !$user->is_suspended // User can't be blocked + && !$message->isDeleted() + && $user->id !== $message->user->id // Can't react to own content + && !$message->isModerated($user) // Can't react to moderated comments + && $message->user->exists // Can't react to messages created by blocked users + && $message->thread->isActive(); // Can't react to locked/archived threads + } + + public function flag(User $user, Message $message) + { + return !$user->is_suspended // User can't be blocked + && !$message->isDeleted() + && $message->userCanFlag($user); + } + + public function request_removal(User $user, Message $message) + { + return !$message->isDeleted() + && $message->getResponsibleUserId() === $user->id // Only owner can request deletion + && !$message->userHasUnresolvedModerationRequests($user); + } + +} diff --git a/app/Policies/ForumThreadPolicy.php b/app/Policies/ForumThreadPolicy.php new file mode 100644 index 0000000..bfbb34c --- /dev/null +++ b/app/Policies/ForumThreadPolicy.php @@ -0,0 +1,39 @@ +is_suspended // User can't be blocked + && !$thread->isDeleted() + && $thread->userCanFlag($user); + } + + public function request_removal(User $user, Thread $thread) + { + return !$thread->isDeleted() + && $thread->getResponsibleUserId() === $user->id // Only owner can request deletion + && !$thread->userHasUnresolvedModerationRequests($user); + } + + public function create(User $user) + { + return !$user->is_suspended; // User can't be blocked + } + + public function send(User $user, Thread $thread) + { + return !$user->is_suspended // User can't be blocked + && $thread->isActive(); // Thread has to be open/active + } + +} diff --git a/app/Policies/NotificaionPolicy.php b/app/Policies/NotificaionPolicy.php new file mode 100644 index 0000000..8feb184 --- /dev/null +++ b/app/Policies/NotificaionPolicy.php @@ -0,0 +1,21 @@ +notifiable_type && + $user->id === $notification->notifiable_id + ); + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 0000000..dced8e1 --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,94 @@ +is_suspended; // User can't be blocked + } + + public function store(User $user, Project $project) + { + // User can't be blocked + if ($user->is_suspended) return false; + + // If EDIT call + if ($project->exists) { + // Check if $user is member + // Project can't be locked or archived + return $project->userIsMember($user) && $project->isActive(); + } + + return true; + } + + /** + * @param User|null $user + * @param Project $project + * @return bool + */ + public function read(?User $user, Project $project) + { + if ($project->isArchived()) return false; + if ($project->is_published) return true; + + // If project is not published, only members can see it + return !is_null($user) && $project->userIsRelated($user); + } + + public function delete(User $user, Project $project) + { + return !$user->is_suspended // User can't be blocked + && $project->isActive() // Project can't be locked or archived + && $project->userIsOwner($user); // User has to be owner + } + + public function manage_project_members(User $user, Project $project) + { + return !$user->is_suspended // User can't be blocked + && $project->isActive() // Project can't be locked or archived + && $project->userIsOwner($user); // User has to be owner + } + + public function resolve_invite(User $user, Project $project) + { + return !$user->is_suspended // User can't be blocked + && $project->isActive() // Project can't be locked or archived + && $project->users() // User has to have a unaccepted invite + ->where('user_id', $user->id) + ->wherePivot('accepted', false) + ->exists(); + } + + public function like(User $user, Project $project) + { + return $this->reaction($user, $project); + } + + public function endorse(User $user, Project $project) + { + return $this->reaction($user, $project); + } + + private function reaction(User $user, Project $project) + { + return !$user->is_suspended // User can't be blocked + && $project->isActive() // Project can't be locked or archived + && $project->owner->exists // Can't react to projects created by blocked users + && !$project->userIsMember($user); // Members can't like a project + } + + public function flag(User $user, Project $project) + { + return !$user->is_suspended // User can't be blocked + && $project->userCanFlag($user); + } +} diff --git a/app/Policies/UserAvatarPolicy.php b/app/Policies/UserAvatarPolicy.php new file mode 100644 index 0000000..23259f3 --- /dev/null +++ b/app/Policies/UserAvatarPolicy.php @@ -0,0 +1,23 @@ +user_avatar_id === $avatar->id; + } + + public function store(User $user, UserAvatar $avatar) + { + return $user->user_avatar_id === $avatar->id; + } +} diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..d5d4e02 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,32 @@ +id === $account->id || $user->can('api.backend.users.user.store'); + } + + public function edit_notifications(User $user, User $account) + { + return $user->id === $account->id; + } + + public function flag(User $user, User $account) + { + return $account->userCanFlag($user); + } + + public function accept_pirate_vows(User $user, User $account) + { + return $user->id === $account->id; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..f2aa2c5 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,62 @@ + 0; + }); + + Validator::extend('filled_html', function ($attribute, $value, $parameters, $validator) { + return is_string($value) + && !empty(trim($value)) + && !empty(trim(strip_tags($value))); + }); + + Validator::extend('avatar_item_is_available', function ($attribute, $value, $parameters, $validator) { + return AvatarItem::query()->findOrFail($value)->userHasItem(); + }); + + View::composer( + 'emails.user.weekly-newsletter', + ProjectActivitySummaryComposer::class + ); + + View::composer( + 'emails.user.weekly-newsletter', + ThreadActivitySummaryComposer::class + ); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..8220083 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,56 @@ + UserPolicy::class, + UserAvatar::class => UserAvatarPolicy::class, + DatabaseNotification::class => NotificaionPolicy::class, + Message::class => ForumMessagePolicy::class, + Project::class => ProjectPolicy::class, + Thread::class => ForumThreadPolicy::class, + ]; + + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + + if (Shutdown::isWithinShutdownPeriod()) { + // TODO +// Gate::before(function (User $user, $permission) { +// if (Shutdown::isDeniedAccess($user, $permission)) { +// return false; +// } +// }); + } + + PermissionsService::registerPolicies($this->policies); + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..7abd046 --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,21 @@ + ['auth:api']]); + + require base_path('routes/channels.php'); + } +} diff --git a/app/Providers/ContextServiceProvider.php b/app/Providers/ContextServiceProvider.php new file mode 100644 index 0000000..a5ca990 --- /dev/null +++ b/app/Providers/ContextServiceProvider.php @@ -0,0 +1,52 @@ + BaseContext::class, + 'app' => AppContext::class, + 'backend' => BackendContext::class, + ]; + + /** + * Features to load + * + * @var array + */ + protected $load = [ + 'base', + ]; + +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php new file mode 100644 index 0000000..34c9387 --- /dev/null +++ b/app/Providers/EventServiceProvider.php @@ -0,0 +1,46 @@ + [ + RewardCourseProgression::class, + ], + ]; + + /** + * The subscriber classes to register. + * + * @var array + */ + protected $subscribe = [ + ForumEventSubscriber::class, + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + parent::boot(); + + // + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..c922d34 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,87 @@ +mapApiRoutes(); + $this->mapAppRoutes(); + $this->mapAuthRoutes(); + $this->mapBackendRoutes(); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + * + * @return void + */ + protected function mapAppRoutes() + { + Route::middleware(['web', 'context:app']) + ->namespace($this->namespace . '\\App') + ->group(base_path('routes/app.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + * + * @return void + */ + protected function mapApiRoutes() + { + Route::prefix('api') + ->middleware(['api', 'guard:api']) + ->namespace($this->namespace . '\\Api') + ->group(base_path('routes/api.php')); + } + + protected function mapAuthRoutes() + { + Route::prefix('auth') + ->middleware(['session', 'api']) + ->namespace($this->namespace . '\\Auth') + ->group(base_path('routes/auth.php')); + } + + protected function mapBackendRoutes() + { + Route::middleware(['context:backend', 'web']) + ->namespace($this->namespace . '\\Backend') + ->group(base_path('routes/backend.php')); + } +} diff --git a/app/Resources/Api/AvatarItem.php b/app/Resources/Api/AvatarItem.php new file mode 100644 index 0000000..198de4c --- /dev/null +++ b/app/Resources/Api/AvatarItem.php @@ -0,0 +1,61 @@ +model($this->model)->filters($this->getFilters()), + Read::create()->model($this->model), + ]; + } + + protected function getFields() + { + return []; + } + + protected function getFilters() + { + return [ + + Filter::create()->when('orderFirst')->scope('orderFirst'), + + Filter::create()->always(function (Builder $q) { + $q->published(); + $q->orderByFeatured(); + }), + + Filter::create() + ->when('user') + ->default('user', null) + ->scope('forUser'), + + Filter::create()->when('public') + ->scope('public'), + + Filter::create()->when('category') + ->default('category', AvatarCategory::BODY) + ->placeholder('Kategori', AvatarCategory::all()) + ->scope('category'), + + Order::create() + ->only('is_public') + ->defaultValue('is_public'), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Content/AnimatedTickerText.php b/app/Resources/Api/Backend/Content/AnimatedTickerText.php new file mode 100644 index 0000000..f9a4e55 --- /dev/null +++ b/app/Resources/Api/Backend/Content/AnimatedTickerText.php @@ -0,0 +1,36 @@ +validates('required|string|max:16'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'text', + ]), + + Order::create()->only(['text']), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Content/Event.php b/app/Resources/Api/Backend/Content/Event.php new file mode 100644 index 0000000..c5e35c5 --- /dev/null +++ b/app/Resources/Api/Backend/Content/Event.php @@ -0,0 +1,77 @@ +view(['with' => ['regions', 'reminders']]); + } + + protected function getFields() + { + return [ + Field::create('title')->validates('required|string|max:191'), + Field::create('link')->validates('required|url|max:191'), + Field::create('img')->validates('required|url|max:191'), + Field::create('description') + ->validates('required|string|max:10000'), + + // Enums + EnumField::create('status')->from(EventStatus::class)->required(), + + Field::create('publish_at')->validates('required|date'), + Field::create('start_at')->validates('required|date'), + Field::create('end_at')->validates('required|date'), + + RelationshipField::create('reminders', true)->fields([ + Field::create('remind_at')->validates('required|date', 'reminders.*.remind_at'), + ])->removeMissing(), + PersistenceField::create()->updates(function (Model $model, Request $request) { + if ($model->wasRecentlyCreated) { + $model->setupDefaultReminders(); + } + }, Field::AFTER_SAVE), + + SyncManyField::create('regions') + ->relation('regions') + ->validates('required|integer|exists:regions,id', 'regions.*.id'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'title', + 'description', + ]), + + EnumFilter::create(EventStatus::class, 'status') + ->placeholder('Vælg status', EventStatus::all()), + + Order::create() + ->only(['status', 'title', 'start_at', 'end_at']), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Content/Meeting.php b/app/Resources/Api/Backend/Content/Meeting.php new file mode 100644 index 0000000..b263667 --- /dev/null +++ b/app/Resources/Api/Backend/Content/Meeting.php @@ -0,0 +1,49 @@ +validates('required|string|max:191'), + Field::create('meeting_room')->validates('required|string|max:191'), + Field::create('banner_active')->validates('required|boolean'), + Field::create('meeting_active')->validates('required|boolean'), + Field::create('from')->validates('required|date_format:H:i:s'), + Field::create('to')->validates('required|date_format:H:i:s'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'description', + 'meeting_room', + ]), + Order::create() + ->only([ + 'description', + 'meeting_room', + 'banner_active', + 'meeting_active', + 'from', + 'to', + ]) + ->defaultValue(['is_active' => 'desc']), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Content/News.php b/app/Resources/Api/Backend/Content/News.php new file mode 100644 index 0000000..a306276 --- /dev/null +++ b/app/Resources/Api/Backend/Content/News.php @@ -0,0 +1,68 @@ +validates('required|string|max:191'), + Field::create('img')->validates('required|url|max:191'), + Field::create('link')->validates('required|url|max:191'), + Field::create('subtext')->validates('required|string|max:10000'), + + // Enums + EnumField::create('theme')->from(Theme::class)->required(), + EnumField::create('status')->from(GenericStatus::class)->required(), + + Field::create('featured')->validates('required|boolean'), + Field::create('publish_at')->validates('required|date'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'title', + 'subtext', + ]), + + EnumFilter::create(GenericStatus::class, 'status') + ->placeholder('Vælg status', GenericStatus::values()), + + SelectFilter::create('featured') + ->placeholder('Vælg fremhævet status') + ->options([ + Boolean::YES => 'Vis kun fremhævede', + Boolean::NO => 'Vis ikke fremhævede', + ]), + + Order::create()->only([ + 'title', + 'publish_at', + 'featured', + 'status', + ]), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Content/Posts.php b/app/Resources/Api/Backend/Content/Posts.php new file mode 100644 index 0000000..fb1587f --- /dev/null +++ b/app/Resources/Api/Backend/Content/Posts.php @@ -0,0 +1,47 @@ +validates('required|string|max:191'), + Field::create('title')->validates('required|string|max:191'), + Field::create('description')->validates('nullable|string|max:191'), + Field::create('image')->validates('nullable|string|max:191'), + Field::create('content')->validates('required|string'), + EnumField::create('type')->from(PostType::class), + EnumField::create('status')->from(GenericStatus::class)->required(), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'title', + 'content', + 'path', + ]), + EnumFilter::create(PostType::class, 'type')->default('type', PostType::PAGE)->placeholder('Vælg type', PostType::all()), + EnumFilter::create(GenericStatus::class, 'status')->placeholder('Vælg status'), + Order::create()->only(['type', 'status', 'title', 'updated_at']), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Content/TwitchChannel.php b/app/Resources/Api/Backend/Content/TwitchChannel.php new file mode 100644 index 0000000..210bc11 --- /dev/null +++ b/app/Resources/Api/Backend/Content/TwitchChannel.php @@ -0,0 +1,33 @@ +validates('required|string|max:191'), + Field::create('collection')->validates('required|string|max:191'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'channel_name', + ]), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Courses/Category.php b/app/Resources/Api/Backend/Courses/Category.php new file mode 100644 index 0000000..78e88eb --- /dev/null +++ b/app/Resources/Api/Backend/Courses/Category.php @@ -0,0 +1,59 @@ +validates('required|string|max:191'), + Field::create('slug')->validates('nullable|alpha_dash|max:191') + ->updates(function (Model $category, string $attribute, $value) { + $category->{$attribute} = empty($value) ? Str::slug($category->title, '_') : $value; + }), + Field::create('active')->validates('boolean'), + Field::create('description')->validates('nullable|string|max:500000'), + Field::create('color')->validates('nullable|string'), + + FilesField::create('logo')->validates('file|image'), + FilesField::create('thumbnail')->validates('file|image'), + ]; + } + + public function configureReadOperation(Read $operation) + { + $operation->filters([ + Filter::create()->always(function (Builder $q) { + $q->with('logo'); + $q->with('thumbnail'); + }), + ]); + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'title', + 'slug', + ]), + Order::create()->only(['title', 'slug']), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Courses/Course.php b/app/Resources/Api/Backend/Courses/Course.php new file mode 100644 index 0000000..2e7d7e2 --- /dev/null +++ b/app/Resources/Api/Backend/Courses/Course.php @@ -0,0 +1,99 @@ +model($this->model)->filters($this->getFilters()), + Read::create()->model($this->model)->filters($this->getReadFilters()), + Store::create()->model($this->model)->fields($this->getFields()), + Delete::create()->model($this->model) + ]; + } + + protected function getFields() + { + return [ + Field::create('title')->validates('required|string|max:191'), + Field::create('slug')->validates('nullable|alpha_dash|max:191') + ->updates(function (Model $category, string $attribute, $value) { + $category->{$attribute} = empty($value) ? Str::slug($category->title, '_') : $value; + }), + + Field::create('category_id')->validates('nullable|int|exists:course_categories,id'), + Field::create('achievement_id')->validates('nullable|int|exists:achievements,id'), + Field::create('description')->validates('required|string|max:10000'), + + EnumField::create('level')->from(DifficultyLevels::class), + Field::create('position')->validates('required|int'), + + + RelationshipField::create('resources', true)->fields([ + EnumField::create('type')->from(BasicResourceTypes::class)->required()->validatorName('resources.*.type'), + Field::create('meta'), + ])->removeMissing()->setOrderTo('position'), + ]; + } + + protected function getReadFilters() + { + return [ + Filter::create()->always(function($q) { + $q->with(['resources' => function($q) { + $q->orderBy('position'); + }]); + }) + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function ($q) { + $q->withCategoryTitle(); + }), + + Search::create()->search([ + 'title', + 'slug', + ]), + + RelationshipFilter::create('category') + ->placeholder('Vælg kategori') + ->source(CourseCategory::query()->whereHas('courses'), 'title'), + + EnumFilter::create(DifficultyLevels::class, 'level') + ->placeholder('Vælg sværhedsgrad', DifficultyLevels::values()), + + Order::create() + ->only(['title', 'slug', 'position', 'level', 'category_title']), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Forum/Topic.php b/app/Resources/Api/Backend/Forum/Topic.php new file mode 100644 index 0000000..b29537d --- /dev/null +++ b/app/Resources/Api/Backend/Forum/Topic.php @@ -0,0 +1,76 @@ +view(['with' => ['children']]); + } + + protected function getFields() + { + return [ + Field::create('name')->validates('required|string|max:191'), + Field::create('slug')->validates('nullable|alpha_dash|max:191') + ->updates(function (Model $topic, string $attribute, $value) { + $topic->{$attribute} = empty($value) ? Str::slug($topic->name, '_') : $value; + }), + + Field::create('description')->validates('nullable|string|max:255'), + // TODO enum status field + + RelationshipField::create('children', true)->fields([ + Field::create('name')->validates('required|string|max:191', 'children.*.name'), + Field::create('slug')->validates('nullable|alpha_dash|max:191', 'children.*.slug') + ->updates(function (Model $topic, string $attribute, $value) { + $topic->{$attribute} = empty($value) ? Str::slug($topic->name, '_') : $value; + }), + + Field::create('description')->validates('nullable|string|max:255', 'children.*.description'), + ])->removeMissing(), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->withParentName(); + }), + Search::create()->search([ + 'name', + 'slug', + 'parent' => ['name', 'slug'], + ]), + + RelationshipFilter::create('parent') + ->placeholder('Vælg overemmne') + ->source(Model::query()->whereHas('children'), 'name'), + + Order::create()->only([ + 'name', + 'slug', + 'parent_name', + ]), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Gamification/Achievement.php b/app/Resources/Api/Backend/Gamification/Achievement.php new file mode 100644 index 0000000..229de95 --- /dev/null +++ b/app/Resources/Api/Backend/Gamification/Achievement.php @@ -0,0 +1,68 @@ + GrantAchievement::create()->model($this->model), + ]); + } + + protected function configureReadOperation(Read $read) + { + return $read->filters([ + Filter::create()->always(function (Builder $q) { + $q->with('achievementItems'); + }), + ]); + } + + protected function getFields() + { + return [ + Field::create('name')->validates('required|string|max:191'), + Field::create('description')->validates('nullable|string|max:191'), + + RelationshipField::create('achievement_items', true)->removeMissing()->fields([ + Field::create('item_id')->validates('required|integer', 'achievement_items.*.item_id'), + EnumField::create('item_type') + ->validatorName('achievement_items.*.item_type') + ->from(Rewardable::class) + ->required(), + ]), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'name', + 'description', + ]), + + Filter::create()->when('lockedForUser')->scope('lockedForUser'), + Filter::create()->when('grantedToUser')->scope('grantedToUser'), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Gamification/AvatarItem.php b/app/Resources/Api/Backend/Gamification/AvatarItem.php new file mode 100644 index 0000000..06f99b1 --- /dev/null +++ b/app/Resources/Api/Backend/Gamification/AvatarItem.php @@ -0,0 +1,70 @@ +validates('required|string|max:191'), + EnumField::create('category')->from(AvatarCategory::class)->required(), + EnumField::create('status')->from(GenericStatus::class)->required(), + Field::create('meta'), + Field::create('content'), + Field::create('is_public'), + Field::create('is_default'), + Field::create('is_featured'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'name', + ]), + + Filter::create()->when('published')->scope('published'), + Filter::create()->when('public')->scope('public'), + + EnumFilter::create(SelectableAvatarCategory::class, 'category') + ->placeholder('Vælg kategori') + ->scope('category'), + + Order::create()->only([ + 'name', + 'category', + ]), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Gamification/User.php b/app/Resources/Api/Backend/Gamification/User.php new file mode 100644 index 0000000..75236af --- /dev/null +++ b/app/Resources/Api/Backend/Gamification/User.php @@ -0,0 +1,133 @@ +model($this->model)->filters($this->getIndexFilters())->appends([]), + Read::create()->model($this->model)->filters($this->getReadFilters())->appends([]), + ]; + } + + protected function getReadFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->withoutGlobalScope('default_withs'); + }) + ]; + } + + protected function getIndexFilters() + { + return [ + Search::create()->search([ + 'name', + 'email', + 'parent_email', + 'username', + ]), + + Filter::create()->always(function (Builder $q) { + $q->select('id', 'username'); + $q->status(UserStatus::ACTIVE); + $q->withoutGlobalScope('default_withs'); + + // Total threads created + $q->withCount(['threads' => function (Builder $q) { + $q->public(true); + }]); + + // Total messages written + $q->withCount(['messages' => function (Builder $q) { + $q->where('moderated', false); + $q->where('blocked_user', false); + $q->public(true); + }]); + + // Total likes on messages + $q->withCount(['messageReactions as message_likes' => function (Builder $q) { + $q->where('type', ReactionType::LIKE); + $q->whereHas('message', function (Builder $q) { + $q->where('moderated', false); + $q->where('blocked_user', false); + $q->public(true); + }); + }]); + + // Total endorsements on messages + $q->withCount(['messageReactions as message_endorsements' => function (Builder $q) { + $q->where('type', ReactionType::ENDORSEMENT); + $q->whereHas('message', function (Builder $q) { + $q->where('moderated', false); + $q->where('blocked_user', false); + $q->public(true); + }); + }]); + + // Total projects created + $q->withCount(['projects' => function (Builder $q) { + $q->public(true, true); + $q->published(); + }]); + + // Total projects participated + $q->withCount(['participatingProjects' => function (Builder $q) { + $q->public(true, true); + $q->published(); + }]); + + // Total likes on owned projects + $q->withCount(['projectReactions as project_likes' => function (Builder $q) { + $q->where('type', ReactionType::LIKE); + $q->whereHas('project', function (Builder $q) { + $q->public(true, true); + $q->published(); + }); + }]); + + // Total endorsements on owned projects + $q->withCount(['projectReactions as project_endorsements' => function (Builder $q) { + $q->where('type', ReactionType::ENDORSEMENT); + $q->whereHas('project', function (Builder $q) { + $q->public(true, true); + $q->published(); + }); + }]); + }), + + Order::create()->only([ + 'username', + 'threads_count', + 'messages_count', + 'message_likes', + 'message_endorsements', + 'projects_count', + 'participating_projects_count', + 'project_likes', + 'project_endorsements', + ])->defaultValue(['project_likes' => 'desc']), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Gamification/UserTitle.php b/app/Resources/Api/Backend/Gamification/UserTitle.php new file mode 100644 index 0000000..397eda8 --- /dev/null +++ b/app/Resources/Api/Backend/Gamification/UserTitle.php @@ -0,0 +1,32 @@ +validates('required|string|max:191'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'title', + ]), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Moderation/ModerationAction.php b/app/Resources/Api/Backend/Moderation/ModerationAction.php new file mode 100644 index 0000000..b82fcc7 --- /dev/null +++ b/app/Resources/Api/Backend/Moderation/ModerationAction.php @@ -0,0 +1,43 @@ +model($this->model) + ->filters($this->getFilters()) + ->appends(['label']) + ->view(['with' => ['user:id,username', 'case:id,moderateable_type']]), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->when('moderation_case', function (Builder $q, $case) { + return $q->forCase($case); + }), + + Order::create() + ->scopes(['newestFirst', 'oldestFirst']) + ->defaultValue('newestFirst'), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Moderation/ModerationCase.php b/app/Resources/Api/Backend/Moderation/ModerationCase.php new file mode 100644 index 0000000..1eb3230 --- /dev/null +++ b/app/Resources/Api/Backend/Moderation/ModerationCase.php @@ -0,0 +1,76 @@ +model($this->model) + ->filters($this->getFilters()) + ->view(['with' => ['user:id,username']]), + + Read::create() + ->model($this->model) + ->view(['with' => ['user', 'moderateable']]) + ->appends(['manual_moderation_actions']), + + Moderate::create(), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->withCount('requests'); + $q->withLastRequestedAt(); + }), + + Search::create()->search([ + 'user' => ['username', 'name', 'email', 'parent_email'], + ]), + + Filter::create()->when('user', function (Builder $q, $user) { + return $q->forUser($user); + }), + + Filter::create()->when('exclude', function (Builder $q, $ids) { + return $q->exclude($ids); + }), + + EnumFilter::create(Models::class, 'moderateable_type') + ->placeholder('Vælg type', Models::all()), + + EnumFilter::create(ModerationCaseStatus::class, 'status') + ->default('status', ModerationCaseStatus::PENDING) + ->placeholder('Vælg status', ModerationCaseStatus::all()), + + Order::create() + ->only(['last_requested_at', 'status', 'moderateable_type', 'requests_count']) + ->scopes(['orderByUsername']) + ->defaultValue(['last_requested_at' => 'desc']), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Moderation/ModerationComment.php b/app/Resources/Api/Backend/Moderation/ModerationComment.php new file mode 100644 index 0000000..085816f --- /dev/null +++ b/app/Resources/Api/Backend/Moderation/ModerationComment.php @@ -0,0 +1,48 @@ +model($this->model) + ->filters($this->getFilters()) + ->view(['with' => ['user']]), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + return $q->type(ModerationActionType::COMMENT); + }), + + Filter::create()->when('moderation_case', function (Builder $q, $case) { + return $q->forCase($case); + }), + + Order::create() + ->scopes(['latest', 'oldest']) + ->defaultValue('latest'), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Moderation/ModerationRequest.php b/app/Resources/Api/Backend/Moderation/ModerationRequest.php new file mode 100644 index 0000000..d5d066c --- /dev/null +++ b/app/Resources/Api/Backend/Moderation/ModerationRequest.php @@ -0,0 +1,46 @@ +model($this->model) + ->filters($this->getFilters()) + ->view(['with' => ['reporter:id,username']]), + + Read::create()->model($this->model), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->when('moderation_case', function (Builder $q, $case) { + return $q->forCase($case); + }), + + Order::create() + ->only(['created_at', 'reason']) + ->scopes(['orderByUsername']) + ->defaultValue(['created_at' => 'desc']), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Moderation/UserSuspension.php b/app/Resources/Api/Backend/Moderation/UserSuspension.php new file mode 100644 index 0000000..4acda42 --- /dev/null +++ b/app/Resources/Api/Backend/Moderation/UserSuspension.php @@ -0,0 +1,64 @@ +model($this->model) + ->filters($this->getFilters()) + ->view(['with' => ['user:id,username', 'issuer:id,username']]), + + Read::create() + ->model($this->model) + ->view(['with' => ['user', 'issuer:id,username']]), + + 'deactivate' => Action::create() + ->model($this->model) + ->trigger('deactivate'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'user' => ['username', 'name', 'email', 'parent_email'], + ]), + + Filter::create()->when('user', + function (Builder $q, $user) { + return $q->forUser($user); + }), + + Filter::create()->when('active', + function (Builder $q) { + return $q->active(); + }), + + Order::create() + ->only(['created_at', 'start_at', 'end_at']) + ->scopes(['orderByUsername', 'orderByDuration']) + ->defaultValue(['created_at' => 'desc']), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Projects/Category.php b/app/Resources/Api/Backend/Projects/Category.php new file mode 100644 index 0000000..ef95903 --- /dev/null +++ b/app/Resources/Api/Backend/Projects/Category.php @@ -0,0 +1,78 @@ +filters([ + Filter::create()->always(function (Builder $q) { + $q->with(['children' => function ($q) { + $q->orderBy('priority', 'asc'); + }]); + }) + ]); + } + + protected function getFields() + { + return [ + Field::create('name')->validates('required|string|max:191'), + Field::create('slug')->validates('nullable|alpha_dash|max:191') + ->updates(function (Model $topic, string $attribute, $value) { + $topic->{$attribute} = empty($value) ? Str::slug($topic->name, '_') : $value; + }), + EnumField::create('status')->from(VisibleStatus::class)->required(), + + RelationshipField::create('children', true)->fields([ + Field::create('name')->validates('required|string|max:191', 'children.*.name'), + Field::create('slug')->validates('nullable|alpha_dash|max:191', 'children.*.slug') + ->updates(function (Model $topic, string $attribute, $value) { + $topic->{$attribute} = empty($value) ? Str::slug($topic->name, '_') : $value; + }), + EnumField::create('status') + ->validatorName('children.*.status') + ->from(VisibleStatus::class) + ->required(), + ])->removeMissing()->setOrderTo('priority'), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function ($q) { + return $q->parent(); + }), + + Search::create()->search([ + 'name', + 'slug', + ]), + + Order::create()->only([ + 'name', + 'slug', + ]), + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Backend/Regions/Region.php b/app/Resources/Api/Backend/Regions/Region.php new file mode 100644 index 0000000..9190970 --- /dev/null +++ b/app/Resources/Api/Backend/Regions/Region.php @@ -0,0 +1,20 @@ +model($this->model)->filters($this->getFilters()), + ]; + } +} diff --git a/app/Resources/Api/Backend/Users/BackendUser.php b/app/Resources/Api/Backend/Users/BackendUser.php new file mode 100644 index 0000000..87b44a7 --- /dev/null +++ b/app/Resources/Api/Backend/Users/BackendUser.php @@ -0,0 +1,45 @@ +validates('required|string|max:255'), + + Field::create('username')->validates(function (Model $user) { + return join('|', [ + 'required', + 'string', + 'alpha_dash_dot', + 'max:25', + 'unique:reserved_usernames,username', + "unique:users,username,{$user->id}", + ]); + }), + + Field::create('email')->validates(function (Model $user) { + return join('|', [ + 'required', + 'string', + 'email', + 'max:255', + "unique:users,email,{$user->id}", + ]); + }), + + Field::create('role_name') + ->updates(function ($user, $attribute, $value) { + $user->syncRoles($value); + }), + ]; + } + +} diff --git a/app/Resources/Api/Backend/Users/Contact.php b/app/Resources/Api/Backend/Users/Contact.php new file mode 100644 index 0000000..fb20373 --- /dev/null +++ b/app/Resources/Api/Backend/Users/Contact.php @@ -0,0 +1,41 @@ +search([ + 'name', + 'email', + 'subject', + 'phone', + 'message', + ]), + + EnumFilter::create(ContactSubmissionType::class, 'type') + ->placeholder('Vælg type', ContactSubmissionType::all()), + + Order::create() + ->only(['created_at', 'score', 'type', 'subject', 'name', 'email', 'phone']) + ->defaultValue(['created_at' => 'desc']), + ]; + } +} diff --git a/app/Resources/Api/Backend/Users/User.php b/app/Resources/Api/Backend/Users/User.php new file mode 100644 index 0000000..d61ea19 --- /dev/null +++ b/app/Resources/Api/Backend/Users/User.php @@ -0,0 +1,120 @@ + Action::create() + ->model($this->model) + ->filters($this->withTrashedFilter()) + ->trigger('restore'), + ]); + } + + protected function getFields() + { + return [ + Field::create('name')->validates('required|string|max:255'), + Field::create('username')->validates(function (Model $user) { + return join('|', [ + 'required', + 'string', + 'alpha_dash_dot', + 'max:25', + 'unique:reserved_usernames,username', + "unique:users,username,{$user->id}", + ]); + }), + Field::create('email')->validates(function (Model $user) { + return join('|', [ + 'required', + 'string', + 'email', + 'max:255', + 'different:parent_email', + "unique:users,email,{$user->id}", + ]); + }), + + Field::create('parent_email')->validates('required|string|email|different:email|max:255'), + Field::create('birthday')->validates('required|date|before:today'), + Field::create('zipcode') + ->validates('required|integer|exists:zipcodes,zipcode') + ->updates(function ($user, $attribute, $value) { + $user->setMeta('zipcode', $value, 'int'); + }), + Field::create('role_name') + ->updates(function ($user, $attribute, $value) { + $user->syncRoles($value); + }), + ]; + } + + public function configureReadOperation(Read $operation) + { + $operation->filters($this->withTrashedFilter()); + } + + public function configureStoreOperation(Store $operation) + { + $operation->filters($this->withTrashedFilter()); + } + + protected function withTrashedFilter() + { + return [ + Filter::create()->always()->scope('withTrashed'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search([ + 'name', + 'email', + 'parent_email', + 'username', + ]), + + EnumFilter::create(UserStatus::class, 'status') + ->default('status', UserStatus::ACTIVE) + ->placeholder('Alle', UserStatus::all()) + ->scope('status'), + + EnumFilter::create(UserRoles::class, 'role') + ->placeholder('Vælg rolle', UserRoles::all()) + ->scope('role'), + + Order::create()->only([ + 'name', + 'email', + 'parent_email', + 'username', + 'birthday', + ]), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Contact.php b/app/Resources/Api/Contact.php new file mode 100644 index 0000000..1ede2a6 --- /dev/null +++ b/app/Resources/Api/Contact.php @@ -0,0 +1,50 @@ + Store::create() + ->model($this->model) + ->fields($this->getFields()) + ->successMessage('Tak for din henvendelse, vi vender tilbage hurtigst muligt.'), + ]; + } + + protected function getFields() + { + return [ + Field::create('subject')->validates('required|string'), + Field::create('message')->validates('required|string'), + Field::create('name')->validates('nullable|string'), + Field::create('phone')->validates('nullable|string'), + Field::create('email')->validates('required|email'), + EnumField::create('type')->from(ContactSubmissionType::class), + + Field::create('token') + ->validates('required|string') + ->updates(function (Model $submission, string $attr, $value, Field $field) { + $response = ReCaptcha::verify($value, 'contact_form'); + + if ($response !== false && is_object($response)) { + $submission->score = $response->score; + $submission->recaptcha_at = \Carbon\Carbon::parse($response->challenge_ts); + } + }), + ]; + } + +} diff --git a/app/Resources/Api/Content/Posts.php b/app/Resources/Api/Content/Posts.php new file mode 100644 index 0000000..b646ad5 --- /dev/null +++ b/app/Resources/Api/Content/Posts.php @@ -0,0 +1,37 @@ + Index::create() + ->model($this->model) + ->single() + ->filters($this->getFilters()), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + return $q->visible(); + }), + Filter::create()->when('path', function (Builder $q, $path) { + return $q->wherePath($path); + }), + ]; + } + +} diff --git a/app/Resources/Api/Courses/Category.php b/app/Resources/Api/Courses/Category.php new file mode 100644 index 0000000..f3e1652 --- /dev/null +++ b/app/Resources/Api/Courses/Category.php @@ -0,0 +1,69 @@ +model($this->model) + ->filters($this->getFilters()) + ->appends(['logo_url', 'thumbnail_url']), + + Breadcrumbs::create(), + 'newest' => Index::create()->model($this->model)->appends(['logo_url']), + 'read' => Index::create()->model($this->model)->filters($this->getFilters())->single(), + ]; + } + + public function configureNewestOperation(Index $operation) + { + $operation->filters([ + Filter::create()->always(function (Builder $q) { + $q->where('active', '=', true); + }), + + Order::create() + ->only('created_at') + ->defaultValue(['created_at' => 'DESC']), + + Pagination::create()->shows(3), + ]); + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->withCount('completedCourses'); + $q->withCount('courses'); + return $q->where('active', '=', true); + }), + + Filter::create()->when('slug', function (Builder $q, $value) { + return $q->where('slug', '=', $value); + }), + + Search::create()->search([ + 'title', + 'slug', + ]), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Courses/CourseResource.php b/app/Resources/Api/Courses/CourseResource.php new file mode 100644 index 0000000..391b34f --- /dev/null +++ b/app/Resources/Api/Courses/CourseResource.php @@ -0,0 +1,46 @@ +model($this->model) + ->filters($this->getFilters()), + 'read' => Index::create() + ->model($this->model) + ->filters($this->getFilters()) + ->single(), + 'answer' => AnswerQuestion::create()->model($this->model), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->when('course_slug', function (Builder $q, $value) { + return $q->whereCourseSlug($value); + }), + + Filter::create()->always(function (Builder $q) { + + $q->with('my_progress'); + + return $q->orderBy('position'); + }), + ]; + } + +} diff --git a/app/Resources/Api/Courses/Courses.php b/app/Resources/Api/Courses/Courses.php new file mode 100644 index 0000000..fd8efd9 --- /dev/null +++ b/app/Resources/Api/Courses/Courses.php @@ -0,0 +1,51 @@ +model($this->model) + ->filters($this->getFilters()) + ->appends(['is_completed']), + Read::create() + ->model($this->model) + ->filters($this->getFilters()) + ->appends(['prev_course', 'next_course', 'is_completed']), + Breadcrumbs::create(), + MakeProgress::create()->model($this->model), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->withCount('resources'); + $q->orderBy('level'); + $q->orderBy('position'); + }), + Filter::create()->when('category_slug', function (Builder $q, $value) { + return $q->categorySlug($value); + }), + Filter::create()->when('course', function (Builder $q, $value) { + return $q->where('slug', $value); + }), + ]; + } + +} diff --git a/app/Resources/Api/Event.php b/app/Resources/Api/Event.php new file mode 100644 index 0000000..3048d6a --- /dev/null +++ b/app/Resources/Api/Event.php @@ -0,0 +1,45 @@ +model($this->model)->filters($this->getFilters()), + Read::create()->model($this->model), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + return $q->published() + ->orderBy('start_at', 'asc'); + }), + + Filter::create()->when('expired', function (Builder $q, $expired) { + return $q->expired((bool)$expired); + }), + + Filter::create()->when('zipcode', function (Builder $q, $zip) { + return $q->forZipcode($zip); + }), + + Pagination::create(), + ]; + } + +} diff --git a/app/Resources/Api/Forum/Message.php b/app/Resources/Api/Forum/Message.php new file mode 100644 index 0000000..aa52f92 --- /dev/null +++ b/app/Resources/Api/Forum/Message.php @@ -0,0 +1,91 @@ +model($this->model)->filters(array_merge( + [ + Filter::create()->always(function ($q) { + $q->isNormalMessage(); + }), + ], + $this->getFilters(), + [ + Pagination::create()->default('$per_page', 5)->default('$page', request()->get('$page', 1)), + ] + )), + Read::create()->model($this->model)->filters($this->getFilters()), + Store::create()->model($this->model)->fields($this->getFields()), + Delete::create()->model($this->model), + 'changes' => Index::create()->model(MessageChange::class)->filters([ + Filter::create()->always(function ($q) { + $q->orderBy('created_at', 'ASC'); + }), + Filter::create()->when('message_id', function ($q, $message_id) { + $q->where('message_id', $message_id); + }), + ]), + 'accept' => Action::create()->model($this->model)->trigger('accept'), + 'like' => Reaction::create()->type(ReactionType::LIKE)->model($this->model), + 'endorse' => Reaction::create()->type(ReactionType::ENDORSEMENT)->model($this->model), + 'flag' => RequestModeration::create()->model($this->model), + 'request_removal' => RequestModeration::create()->model($this->model) + ->successMessage('Dit indlæg er nu skjult og sendt til moderator.') + ->validationRules([ + 'reason' => 'required|string|in:' . ModerationReasons::REMOVAL_REQUEST, + 'comment' => 'nullable|string', + ]), + ]; + } + + public function getFilters() + { + return [ + Filter::create()->when('thread_id', function (Builder $builder, $thread_id) { + $builder->where('thread_id', '=', $thread_id); + }), + Filter::create()->always(function (Builder $builder) { + $builder->with('thread:id,created_by,type,status'); // Need 'status' for permission checks + $builder->public(); + $builder->orderBy('created_at', 'ASC'); + $builder->withCount('likes'); + $builder->withCount('my_likes'); + $builder->withCount('changes'); + $builder->with('user:id,username,user_avatar_id,title_id,deleted_at'); // Need 'deleted_at', for showing blocked users + }), + Search::create()->search(['message' => ['content']]), + ]; + } + + public function getFields() + { + return [ + Field::create('content')->validates('required|string|filled_html'), + ]; + } + +} diff --git a/app/Resources/Api/Forum/Thread.php b/app/Resources/Api/Forum/Thread.php new file mode 100644 index 0000000..c90ef25 --- /dev/null +++ b/app/Resources/Api/Forum/Thread.php @@ -0,0 +1,93 @@ +model($this->model)->filters([ + Filter::create()->when('topic_id', function ($q, $topic_id) { + $q->where('topic_id', '=', $topic_id); + $q->orderBy('is_sticky', 'DESC'); + }), + Filter::create()->always(function ($builder) { + $builder->public(); + $builder->where('is_embedded', false); + $builder->withCount('messages'); + $builder->with('latestMessage'); + $builder->with('topic'); + }), + Order::create() + ->only(['sort_score', 'created_at']) + ->scopes(['orderByActivity']) + ->defaultValue(['sort_score' => 'DESC']), + Search::create()->search(['subject', 'originalMessage' => ['content']]), + Pagination::create()->default('$per_page', 15) + ]), + Read::create()->model($this->model)->appends(['muted'])->filters([ + Filter::create()->always(function ($builder) { + $builder->public(); + }), + ]), + 'newest' => Index::create()->model($this->model), + 'create' => Store::create() + ->model($this->model) + ->fields([ + Field::create('topic_id')->validates('integer|exists:forum_topics,id'), + Field::create('subject')->validates('required|string'), + Field::create('grownups_can_participate')->validates('boolean'), + Field::create('type')->validates('required|in:discussion,question'), + Field::create('message')->validates('required|string|filled_html')->updatesAt(Field::AFTER_SAVE)->updates(function ($thread, $attribute, $content) { + $message = $thread->createMessage($content); + + if ($message !== null) { + $thread->original_message_id = $message->id; + $thread->save(); + } + + }), + ]), + 'send' => SendMessage::create(), + 'toggle_mute' => Action::create()->model($this->model)->trigger('toggleMute'), + 'toggle_sticky' => Action::create()->model($this->model)->trigger('toggleSticky'), + 'flag' => RequestModeration::create()->model($this->model), + 'request_removal' => RequestModeration::create()->model($this->model) + ->successMessage('Dit indlæg er nu skjult og sendt til moderator.') + ->validationRules([ + 'reason' => 'required|string|in:' . ModerationReasons::REMOVAL_REQUEST, + 'comment' => 'nullable|string', + ]), + ]; + } + + public function configureNewestOperation(Index $operation) + { + $operation->filters([ + Filter::create()->always(function ($builder) { + $builder->public(); + $builder->where('is_embedded', false); + $builder->orderBy('created_at', 'DESC'); + }), + Pagination::create()->default('$per_page', 3) + ]); + } +} diff --git a/app/Resources/Api/Forum/Topics.php b/app/Resources/Api/Forum/Topics.php new file mode 100644 index 0000000..0b4fd12 --- /dev/null +++ b/app/Resources/Api/Forum/Topics.php @@ -0,0 +1,44 @@ + Index::create()->model($this->model)->filters($this->getFilters())->appends(['latestMessage']), + 'read' => Read::create()->model($this->model), + 'breadcrumbs' => Breadcrumbs::create()->filters([ + Filter::create()->when('thread_id', function(){}), + Filter::create()->when('topic_id', function(){}), + ]), + ]; + } + + public function getFilters() + { + return [ + Filter::create()->always(function ($builder) { + /// Eager load a count of all thread immediately related to this topic. It is used in the overview + $builder->withCount(['descendantThreads' => function (Builder $q) { + $q->public(); + }]); + }), + Filter::create()->when('parent_id', function ($builder, $parent_id) { + return $builder->where('parent_id', '=', $parent_id); + }), + ]; + } + +} diff --git a/app/Resources/Api/Moderation/Appeal.php b/app/Resources/Api/Moderation/Appeal.php new file mode 100644 index 0000000..b8adafb --- /dev/null +++ b/app/Resources/Api/Moderation/Appeal.php @@ -0,0 +1,47 @@ + Store::create() + ->model($this->model) + ->fields($this->getFields()) + ->successMessage('Tak for din henvendelse, vi vender tilbage hurtigst muligt.'), + ]; + } + + protected function getFields() + { + return [ + Field::create('name')->validates('nullable|string'), + Field::create('email')->validates('required|email'), + Field::create('phone')->validates('nullable|string'), + Field::create('message')->validates('required|string'), + Field::create('moderation_case_id')->validates(['moderation_case_id' => [ + 'bail', + 'integer', + 'required', + 'exists:moderation_cases,id', + function ($attribute, $value, $fail) { + if (!ModerationCase::findOrFail($value)->hasValidIdentifier(request()->input('identifier', ''))) { + $fail('Invalid signature.'); + } + }, + ]]), + + ]; + } + +} diff --git a/app/Resources/Api/News.php b/app/Resources/Api/News.php new file mode 100644 index 0000000..c19d15d --- /dev/null +++ b/app/Resources/Api/News.php @@ -0,0 +1,41 @@ +model($this->model) + ->filters($this->getFilters()), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + return $q->isPublished(); + }), + + Order::create() + ->only(['featured', 'publish_at']) + ->defaultValue(['featured' => 'DESC', 'publish_at' => 'DESC']), + + Pagination::create()->shows(5), + ]; + } + +} diff --git a/app/Resources/Api/Notification.php b/app/Resources/Api/Notification.php new file mode 100644 index 0000000..0c4c457 --- /dev/null +++ b/app/Resources/Api/Notification.php @@ -0,0 +1,58 @@ +model($this->model)->filters($this->getFilters()), + 'count' => Count::create()->model($this->model)->filters($this->getFilters()), + 'markAsRead' => Action::create() + ->model($this->model) + ->trigger(function (Model $notification) { + $notification->markAsRead(); + event(new NotificationRead($notification)); + }), + ]; + } + + public function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $user = Auth::user(); + + $q->orderBy('updated_at', 'DESC'); + $q->where('notifiable_type', get_class($user)); + $q->where('notifiable_id', $user->id); + $q->where(function ($q) { + return $q->where('data->status', '<>', NotificationStatus::DISABLED) + ->orWhereNull('data->status'); + }); + }), + + Filter::create()->when('unread', function (Builder $q, $unred) { + return $q->whereNull('read_at'); + }), + + Pagination::create(), + ]; + } +} diff --git a/app/Resources/Api/Projects/Category.php b/app/Resources/Api/Projects/Category.php new file mode 100644 index 0000000..a5812dd --- /dev/null +++ b/app/Resources/Api/Projects/Category.php @@ -0,0 +1,44 @@ +model($this->model)->filters($this->getFilters())->appends(['display_name']), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->public(); + $q->with('parent:id,name'); + $q->orderBy('parent_id', 'asc'); + $q->orderBy('priority', 'asc'); + }), + Filter::create()->when('project', function (Builder $q, $project) { + $q->where('status', VisibleStatus::VISIBLE); + $q->orWhereHas('projects', function ($q) use ($project) { + $q->whereId($project); + }); + }), + Filter::create()->when('children', function (Builder $q) { + $q->parent(false); + }), + ]; + } +} diff --git a/app/Resources/Api/Projects/Project.php b/app/Resources/Api/Projects/Project.php new file mode 100644 index 0000000..16051a3 --- /dev/null +++ b/app/Resources/Api/Projects/Project.php @@ -0,0 +1,202 @@ +model($this->model)->filters($this->getFilters())->appends(['thumbnail_url']), + Read::create()->model($this->model)->filters($this->getReadFilters())->appends(['all_users', 'cover_image_url']), + Delete::create()->model($this->model), + + 'newest' => Index::create()->model($this->model)->filters($this->getNewestFilters())->appends(['thumbnail_url']), + + 'store' => Store::create()->model($this->model)->fields($this->getFields())->successMessage('Ændringerne på dit projekt er blevet gemt!'), + 'create' => Store::create()->model($this->model)->fields($this->getFields()), + + 'like' => Reaction::create()->type(ReactionType::LIKE)->model($this->model), + 'endorse' => Reaction::create()->type(ReactionType::ENDORSEMENT)->model($this->model), + + 'resolve_invite' => Reply::create()->model($this->model)->trigger('resolveInvitation'), + 'flag' => RequestModeration::create()->model($this->model), + ]; + } + + protected function getFields() + { + return [ + // TODO - more specific validation & max file sizes + FilesField::create('cover_image') + ->validates('file|mimes:jpeg,png') + ->manipulates(function (string $path) { + try { + Image::make($path)->fit(1300, 500, function ($constraint) { + $constraint->upsize(); + })->save(); + } catch (NotReadableException $e) { + } + }), + + FilesField::create('thumbnail') + ->validates('file|mimes:jpeg,png') + ->manipulates(function (string $path) { + try { + Image::make($path)->fit(300, 300, function ($constraint) { + $constraint->upsize(); + })->save(); + } catch (NotReadableException $e) { + } + }), + + FilesField::create('files') + ->validates('file|mimes:jpeg,png,pdf') + ->manipulates(function (string $path) { + try { + Image::make($path)->resize(1000, null, function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + })->save(); + } catch (NotReadableException $e) { + } + }), + + Field::create('title')->validates('required|string|max:191'), + Field::create('description')->validates('nullable|string|max:500000'), + EnumField::create('status')->from(GenericStatus::class)->required(), + + SyncManyField::create('categories') + ->relation('categories') + ->validates('required|integer|exists:project_categories,id', 'categories.*.id'), + + SyncManyField::create('users') + ->can(CustomPermissions::MANAGE_PROJECT_MEMBERS) + ->relation('users') + ->validates('required|integer|exists:users,id', 'users.*.id'), + ]; + } + + protected function getBaseFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->public(); + $q->withCount('likes'); + $q->withCount('my_likes'); + $q->withCount('threadMessages'); + if(Gate::allows(CommonPermissions::ENDORSE_PROJECTS)) { + $q->withCount('endorsements'); + $q->withCount('my_endorsements'); + } + }), + ]; + } + + protected function getReadFilters() + { + return array_merge($this->getBaseFilters(), [ + Filter::create()->always(function (Builder $q) { + $q->with('categories.parent'); + $q->with('owner:id,username,user_avatar_id,title_id'); + $q->with('members:id,username,user_avatar_id,title_id'); + + $q->with('images'); + $q->with('files'); + $q->with('coverImage'); + $q->with('thumbnail'); + }), + ]); + } + + protected function getFilters() + { + return array_merge($this->getBaseFilters(), [ + + // Always limit projects to PUBLISHED or User Related + Filter::create()->always(function (Builder $q) { + $q->where(function ($q) { + $q->published(); + + if (Auth::check()) { + $q->orWhere(function ($q) { + return $q->relatedToUser(Auth::id()); + }); + } + }); + }), + + + Search::create()->search([ + 'title', + 'description', + 'owner' => ['username'], + 'members' => ['username'], + ]), + + Filter::create()->when('user', function (Builder $q, $user) { + return $q->relatedToUser($user, false); + }), + + Filter::create()->when('invitations', function (Builder $q) { + $q->awaitingResponse(Auth::id()); + }), + + Filter::create()->when('categories', function (Builder $q, $categories) { + return $q->forCategories((array)$categories); + }), + + Order::create() + ->scopes(GenericOrderType::values()) + ->defaultValue(GenericOrderType::MOST_POPULAR), + + Pagination::create()->default('$per_page', 18) + ]); + } + + protected function getNewestFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->public(); + $q->published(); + }), + + Order::create() + ->only('published_at') + ->defaultValue(['published_at' => 'DESC']), + + Pagination::create()->shows(3), + ]; + } +} diff --git a/app/Resources/Api/Rewards/UserReward.php b/app/Resources/Api/Rewards/UserReward.php new file mode 100644 index 0000000..ed7a8ab --- /dev/null +++ b/app/Resources/Api/Rewards/UserReward.php @@ -0,0 +1,66 @@ +model($this->model) + ->filters($this->getFilters()), + + 'opened' => Index::create() + ->model($this->model) + ->filters([ + Filter::create()->when('user')->scope('forUser'), + Filter::create()->when('type')->scope('hasItemType'), + Filter::create()->always(function (Builder $q) { + $q->opened(true); + $q->with([ + 'rewardItems' => function($q){ + $q->with('item'); + $q->forUser(request()->get('user', Auth::id())); + if(request()->has('type')) { + $q->isOfType(request()->get('type')); + } + } + ]); + $q->whereHas('rewardItems', function ($q) { + $q->forUser(request()->get('user', Auth::id())); + if(request()->has('type')) { + $q->isOfType(request()->get('type')); + } + }); + $q->orderBy('created_at', 'DESC'); + }), + ]), + + 'open' => Action::create() + ->model($this->model) + ->filters($this->getFilters()) + ->trigger('open'), + ]; + } + + protected function getFilters() + { + return [ + Filter::create()->always(function (Builder $q) { + $q->forUser(); // <- If you remove this, add a Model Policy instead + $q->opened(false); + }), + ]; + } +} diff --git a/app/Resources/Api/Rewards/UserTitle.php b/app/Resources/Api/Rewards/UserTitle.php new file mode 100644 index 0000000..f0d981e --- /dev/null +++ b/app/Resources/Api/Rewards/UserTitle.php @@ -0,0 +1,41 @@ +model($this->model) + ->filters($this->getFilters()) + ->appends(['selected_by_user']), + + 'select' => Action::create() + ->model($this->model) + ->filters([ + Filter::create()->always()->scope('forUser'), + ]) + ->trigger('selectForUser'), + ]; + } + + protected function getFilters() + { + return [ + Filter::create() + ->when('user') + ->default('user', null) + ->scope('forUser'), + ]; + } +} diff --git a/app/Resources/Api/User.php b/app/Resources/Api/User.php new file mode 100644 index 0000000..16d7740 --- /dev/null +++ b/app/Resources/Api/User.php @@ -0,0 +1,209 @@ + Store::create() + ->model($this->model) + ->fields($this->getNotificationFields()), + + Validate::create() + ->model($this->model) + ->fields($this->getFields()), + + 'mentionables' => Index::create() + ->model($this->model) + ->filters(($this->getMentionableFilters())), + + 'flag' => RequestModeration::create() + ->model($this->model), + + 'pirate' => Index::create() + ->model($this->model) + ->filters([ + Filter::create() + ->when('username', function ($q, $username) { + return $q->whereUsername($username); + }) + ->missing('username', function () { + return abort(404); + }) + ]) + ->single(), + + 'accept_pirate_vows' => Store::create() + ->model($this->model) + ->fields($this->getVowFields()), + ]); + } + + public function configureReadOperation(Read $operation) + { + $operation->appends(['notification_settings', 'zipcode']); + } + + protected function getNotificationFields() + { + return [ + Field::create('notification_settings') + ->validates('required|array') + ->updates(function ($user, $attribute, $value) { + $settings = Arr::only($value, Model::editableNotifications()); + $settings = array_map(function ($i) { + return (bool)$i; + }, $settings); + + $user->setMeta('notification_settings', $settings, 'json'); + }), + ]; + } + + protected function getFields() + { + return [ + Field::create('name')->validates('required|string|max:255'), + Field::create('email')->validates(function (Model $user) { + return join('|', [ + 'required', + 'string', + 'email', + 'max:255', + 'different:parent_email', + "unique:users,email,{$user->id}", + ]); + }), + + Field::create('parent_email') + ->validates(function (Model $user) { + return $user->hasVerifiedEmail() + ? 'in:'.$user->parent_email + : 'required|string|email|different:email|max:255'; + }), + + Field::create('description')->validates('nullable|string'), + Field::create('birthday') + ->validates(function (Model $user) { + return ['birthday' => join('|', array_filter([ + !empty($user->birthday) ? 'required' : 'nullable', + 'date', + 'before:today', + ]))]; + }), + + Field::create('zipcode') + ->validates(function (Model $user) { + return ['zipcode' => join('|', array_filter([ + !empty($user->zipcode) ? 'required' : 'nullable', + 'integer', + 'exists:zipcodes,zipcode', + ]))]; + }) + ->updates(function ($user, $attribute, $value) { + if (!empty($value)) { + $user->setMeta('zipcode', $value, 'int'); + } + }), + + Field::create('password') + ->validates('nullable|string|min:8|confirmed') + ->updates(function ($user, $attribute, $value) { + if (!empty($value)) { + $user->password = Hash::make($value); + } + }), + + Field::create('current_password') + ->validates(['current_password' => [ + 'nullable', + 'required_with:password', + function ($attribute, $value, $fail) { + if (!Hash::check($value, Auth::user()->password)) { + $fail(trans('validation.invalid', [$attribute])); + } + }, + ]]) + ->updates(function () {}), + ]; + } + + protected function getMentionableFilters() + { + return [ + Filter::create()->always(function ($builder) { + + /// Only allow mentioning of pirates + $builder->role(UserRoles::active()); + + /// Only display public user info + $builder->select(['username', 'id', 'user_avatar_id']); + + /// Do not allow mention of yourself + $builder->where('id', '<>', Auth::id()); + + }), + + Filter::create()->when('query', function ($builder, $query) { + $builder->where('username', 'like', '%' . $query . '%'); + }), + + Filter::create()->when('exclude', function ($q, $ids) { + return $q->whereNotIn('id', (array)$ids); + }), + + ]; + } + + protected function getCurrentFilters() + { + return [ + ScopeFilter::create()->always()->scope('currentUser'), + ]; + } + + protected function getFilters() + { + return [ + Search::create()->search(['name', 'email', 'parent_email', 'username']), + Pagination::create() + ]; + } + + protected function getVowFields() + { + return [ + Field::create('vows') + ->validates([ + 'vows' => 'required|array|size:6', + 'vows.*' => 'required|accepted', + ]) + ->updates(function ($user, $attribute, $value) { + $user->setMeta('accepted_pirate_vows', $value, 'bool'); + }), + ]; + } + +} diff --git a/app/Resources/Api/UserAvatar.php b/app/Resources/Api/UserAvatar.php new file mode 100644 index 0000000..314346a --- /dev/null +++ b/app/Resources/Api/UserAvatar.php @@ -0,0 +1,60 @@ +model($this->model)->filters($this->getFilters()), + Read::create()->model($this->model), + Store::create()->model($this->model)->fields($this->getFields())->successMessage('Din pirat er blevet gemt!'), + //Delete::create()->model($this->model), + 'fetch' => Index::create()->model($this->model)->filters($this->getFetchFilters()) + ]; + } + + protected function getFields() + { + return [ + AvatarField::create(''), + ]; + } + + protected function getFilters() + { + return [ + ]; + } + + protected function getFetchFilters() + { + return [ + Filter::create()->always(function ($builder) { + $builder->with([ + 'user' => function ($builder) { + $builder->select('id', 'user_avatar_id'); + } + ]); + }), + Filter::create()->when('ids', function ($builder, $ids) { + $builder->whereHas('user', function ($builder) use ($ids) { + $builder->whereIn('id', $ids); + }); + }) + ]; + } + +} diff --git a/app/Resources/App/Appeal.php b/app/Resources/App/Appeal.php new file mode 100644 index 0000000..927f81e --- /dev/null +++ b/app/Resources/App/Appeal.php @@ -0,0 +1,22 @@ + React::create() + ->component('Appeal') + ->path('/appeal/{moderation_case}') + ->middlewares(['signed']), + ]; + } + +} diff --git a/app/Resources/App/Courses.php b/app/Resources/App/Courses.php new file mode 100644 index 0000000..da285f0 --- /dev/null +++ b/app/Resources/App/Courses.php @@ -0,0 +1,29 @@ + React::create() + ->component('Courses.Overview') + ->path('kodehavet'), + 'courses' => React::create() + ->component('Courses.Courses') + ->path('kodehavet/{category}'), + 'course' => React::create() + ->component('Courses.Course') + ->path('kodehavet/{category}/{course}/{course_slug}'), + 'step' => React::create() + ->component('Courses.Step') + ->path('kodehavet/{category}/{course}/{course_slug}/{step}') + ]; + } + +} diff --git a/app/Resources/App/Forum.php b/app/Resources/App/Forum.php new file mode 100644 index 0000000..dbbaff0 --- /dev/null +++ b/app/Resources/App/Forum.php @@ -0,0 +1,29 @@ + React::create() + ->component('Forum.Overview') + ->path('piratsnak') + ->title('Piratsnak'), + 'topic' => React::create() + ->component('Forum.Topic') + ->path('piratsnak/emner/{topic}/{topic_slug}'), + 'thread' => React::create() + ->component('Forum.Thread') + ->path('piratsnak/snakke/{thread}'), + 'message' => RedirectToMessage::create()->path('piratsnak/snakke/{thread}/besked/{message}') + ]; + } + +} diff --git a/app/Resources/App/Home.php b/app/Resources/App/Home.php new file mode 100644 index 0000000..39b1c32 --- /dev/null +++ b/app/Resources/App/Home.php @@ -0,0 +1,19 @@ + React::create()->component('Welcome')->path('/') + ]; + } + +} diff --git a/app/Resources/App/Pages.php b/app/Resources/App/Pages.php new file mode 100644 index 0000000..72c2952 --- /dev/null +++ b/app/Resources/App/Pages.php @@ -0,0 +1,24 @@ + Post::create()->component('Content.Pages.Post'), + 'about' => React::create()->component('Content.Pages.About')->path('om-piratskibet'), + 'rules' => React::create()->component('Content.Pages.Rules')->path('vi-skal-vaere-gode-ved-hinanden'), + 'user_introduction' => React::create()->component('Content.Pages.User_introduction')->path('bruger-introduktion'), + 'parent_introduction' => React::create()->component('Content.Pages.Parent_introduction')->path('foraeldre-introduktion'), + 'contact' => React::create()->component('Content.Pages.Contact')->path('kontakt'), + ]; + } + +} diff --git a/app/Resources/App/PasswordReset.php b/app/Resources/App/PasswordReset.php new file mode 100644 index 0000000..9dc1916 --- /dev/null +++ b/app/Resources/App/PasswordReset.php @@ -0,0 +1,22 @@ + React::create() + ->component('PasswordReset') + ->path('/reset-password/{token}') + ->middlewares(['guest']), + ]; + } + +} diff --git a/app/Resources/App/Pirate.php b/app/Resources/App/Pirate.php new file mode 100644 index 0000000..391d3e7 --- /dev/null +++ b/app/Resources/App/Pirate.php @@ -0,0 +1,20 @@ + React::create() + ->component('Pirate') + ->path('pirat/{username}'), + ]; + } +} diff --git a/app/Resources/App/Projects.php b/app/Resources/App/Projects.php new file mode 100644 index 0000000..394cbc4 --- /dev/null +++ b/app/Resources/App/Projects.php @@ -0,0 +1,43 @@ + React::create() + ->component('Projects.Overview') + ->path('showcase') + ->title('Showcases'), + + 'create' => React::create() + ->component('Projects.Edit') + ->path('showcase/projekt/opret') + ->title('Opret Showcase') + ->parent('app.projects.overview'), + + 'edit' => React::create() + ->component('Projects.Edit') + ->path('showcase/projekt/rediger/{project}') + ->title('Rediger Showcase') + ->parent('app.projects.overview'), + + 'project' => React::create() + ->component('Projects.Project') + ->path('showcase/projekt/{project}') + ->title('Showcase') + ->parent('app.projects.overview'), + + Download::create()->model(Project::class)->from('files') + ]; + } + +} diff --git a/app/Resources/App/Submissions.php b/app/Resources/App/Submissions.php new file mode 100644 index 0000000..d131451 --- /dev/null +++ b/app/Resources/App/Submissions.php @@ -0,0 +1,20 @@ + React::create() + ->component('Submissions.AvatarItem') + ->path('indsend-pirat-avatar-element'), + ]; + } + +} diff --git a/app/Resources/App/Test.php b/app/Resources/App/Test.php new file mode 100644 index 0000000..b4eed58 --- /dev/null +++ b/app/Resources/App/Test.php @@ -0,0 +1,22 @@ +component('Test'); + $operation->path('test'); + } + +} diff --git a/app/Resources/App/Tv.php b/app/Resources/App/Tv.php new file mode 100644 index 0000000..be5ebf0 --- /dev/null +++ b/app/Resources/App/Tv.php @@ -0,0 +1,20 @@ + React::create() + ->component('Tv.Index') + ->path('coding-pirates-tv'), + ]; + } + +} diff --git a/app/Resources/Auth/Login.php b/app/Resources/Auth/Login.php new file mode 100644 index 0000000..4f9a425 --- /dev/null +++ b/app/Resources/Auth/Login.php @@ -0,0 +1,21 @@ +successMessage(trans('auth.verification_link_sent')) + ->errorMessage(trans('auth.verification_link_error')), + + VerifyEmail::create() + ->successMessage(trans('auth.email_verified')) + ->errorMessage(trans('auth.email_verification_error')) + ->setRedirectTo('/'), + ]; + } +} diff --git a/app/Resources/Backend/Contact.php b/app/Resources/Backend/Contact.php new file mode 100644 index 0000000..122c79b --- /dev/null +++ b/app/Resources/Backend/Contact.php @@ -0,0 +1,29 @@ + React::create() + ->component('Contact.Index') + ->path('contact_submissions') + ->title('Kontakthenvendelser'), + + 'edit' => React::create() + ->component('Contact.Edit') + ->path("contact_submissions/edit/{contact_submission?}") + ->title('Kontakthenvendelse') + ->parent('backend.contact_submissions.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/AnimatedTickerTexts.php b/app/Resources/Backend/Content/AnimatedTickerTexts.php new file mode 100644 index 0000000..a726eeb --- /dev/null +++ b/app/Resources/Backend/Content/AnimatedTickerTexts.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.AnimatedTickerTexts.Index') + ->path('animated_ticker_texts') + ->title('Animeret ticker tekst'), + + 'create' => React::create() + ->component('Content.AnimatedTickerTexts.Edit') + ->path("animated_ticker_texts/create/{animated_ticker_text?}") + ->title('Opret ticker tekst') + ->parent('backend.content.animated_ticker_texts.index'), + + 'edit' => React::create() + ->component('Content.AnimatedTickerTexts.Edit') + ->path("animated_ticker_texts/edit/{animated_ticker_text?}") + ->title('Redigér ticker tekst') + ->parent('backend.content.animated_ticker_texts.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/Events.php b/app/Resources/Backend/Content/Events.php new file mode 100644 index 0000000..815d094 --- /dev/null +++ b/app/Resources/Backend/Content/Events.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.Events.Index') + ->path('events/event') + ->title('Begivenheder'), + + 'create' => React::create() + ->component('Content.Events.Edit') + ->path("events/event/create/{event?}") + ->title('Opret begivenhed') + ->parent('backend.content.events.index'), + + 'edit' => React::create() + ->component('Content.Events.Edit') + ->path("events/event/edit/{event?}") + ->title('Redigér begivenhed') + ->parent('backend.content.events.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/Meetings.php b/app/Resources/Backend/Content/Meetings.php new file mode 100644 index 0000000..398d5ff --- /dev/null +++ b/app/Resources/Backend/Content/Meetings.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.Meetings.Index') + ->path('meetings') + ->title('Møder'), + + 'create' => React::create() + ->component('Content.Meetings.Edit') + ->path("meetings/create/{meeting?}") + ->title('Opret Møde') + ->parent('backend.content.meetings.index'), + + 'edit' => React::create() + ->component('Content.Meetings.Edit') + ->path("meetings/edit/{meeting?}") + ->title('Redigér Møde') + ->parent('backend.content.meetings.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/News.php b/app/Resources/Backend/Content/News.php new file mode 100644 index 0000000..55b0ed7 --- /dev/null +++ b/app/Resources/Backend/Content/News.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.News.Index') + ->path('news') + ->title('Nyheder'), + + 'create' => React::create() + ->component('Content.News.Edit') + ->path("news/create/{news?}") + ->title('Opret nyhed') + ->parent('backend.content.news.index'), + + 'edit' => React::create() + ->component('Content.News.Edit') + ->path("news/edit/{news?}") + ->title('Redigér nyhed') + ->parent('backend.content.news.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/Posts.php b/app/Resources/Backend/Content/Posts.php new file mode 100644 index 0000000..fa32061 --- /dev/null +++ b/app/Resources/Backend/Content/Posts.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.Posts.Index') + ->path('posts') + ->title('Indholdssider'), + + 'create' => React::create() + ->component('Content.Posts.Edit') + ->path("posts/create/{post?}") + ->title('Opret side') + ->parent('backend.content.posts.index'), + + 'edit' => React::create() + ->component('Content.Posts.Edit') + ->path("posts/edit/{post?}") + ->title('Redigér side') + ->parent('backend.content.posts.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Content/TwitchChannels.php b/app/Resources/Backend/Content/TwitchChannels.php new file mode 100644 index 0000000..a1c7510 --- /dev/null +++ b/app/Resources/Backend/Content/TwitchChannels.php @@ -0,0 +1,35 @@ + React::create() + ->component('Content.TwitchChannels.Index') + ->path('twitch_channels') + ->title('Twitch kanaler'), + + 'create' => React::create() + ->component('Content.TwitchChannels.Edit') + ->path("twitch_channels/create/{twitch_channel?}") + ->title('Opret Twitch kanal') + ->parent('backend.content.twitch_channels.index'), + + 'edit' => React::create() + ->component('Content.TwitchChannels.Edit') + ->path("twitch_channels/edit/{twitch_channel?}") + ->title('Redigér Twitch kanal') + ->parent('backend.content.twitch_channels.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Courses.php b/app/Resources/Backend/Courses.php new file mode 100644 index 0000000..938beed --- /dev/null +++ b/app/Resources/Backend/Courses.php @@ -0,0 +1,51 @@ + React::create() + ->component('Courses.Category.Index') + ->path('courses/categories') + ->title('Kategorier'), + + 'create_category' => React::create() + ->component('Courses.Category.Edit') + ->path("courses/categories/create/{course_category?}") + ->title('Opret kategori') + ->parent('backend.courses.index_categories'), + + 'edit_category' => React::create() + ->component('Courses.Category.Edit') + ->path("courses/categories/edit/{course_category?}") + ->title('Redigér kategori') + ->parent('backend.courses.index_categories'), + + 'index_courses' => React::create() + ->component('Courses.Course.Index') + ->path('courses/courses') + ->title('Læringsforløb'), + + 'create_course' => React::create() + ->component('Courses.Course.Edit') + ->path("courses/courses/create/{course?}") + ->title('Opret læringsforløb') + ->parent('backend.courses.index_courses'), + + 'edit_course' => React::create() + ->component('Courses.Course.Edit') + ->path("courses/courses/edit/{course?}") + ->title('Redigér læringsforløb') + ->parent('backend.courses.index_courses'), + + ]; + } +} diff --git a/app/Resources/Backend/Forum.php b/app/Resources/Backend/Forum.php new file mode 100644 index 0000000..96a794a --- /dev/null +++ b/app/Resources/Backend/Forum.php @@ -0,0 +1,34 @@ + React::create() + ->component('Forum.Topic.Index') + ->path('forum/topics') + ->title('Emner'), + + 'create_topic' => React::create() + ->component('Forum.Topic.Edit') + ->path("forum/topics/create/{topic?}") + ->title('Opret emne') + ->parent('backend.forum.index_topics'), + + 'edit_topic' => React::create() + ->component('Forum.Topic.Edit') + ->path("forum/topics/edit/{topic?}") + ->title('Redigér emme') + ->parent('backend.forum.index_topics'), + + ]; + } +} diff --git a/app/Resources/Backend/Gamification/Achievements.php b/app/Resources/Backend/Gamification/Achievements.php new file mode 100644 index 0000000..b88990b --- /dev/null +++ b/app/Resources/Backend/Gamification/Achievements.php @@ -0,0 +1,34 @@ + React::create() + ->component('Gamification.Achievements.Index') + ->path('gamification/achievements') + ->title('Achievements'), + + 'create' => React::create() + ->component('Gamification.Achievements.Edit') + ->path("gamification/achievements/create/{achievement?}") + ->title('Opret achievement') + ->parent('backend.gamification.achievements.index'), + + 'edit' => React::create() + ->component('Gamification.Achievements.Edit') + ->path("gamification/achievements/edit/{achievement?}") + ->title('Redigér achievement') + ->parent('backend.gamification.achievements.index'), + + ]; + } +} diff --git a/app/Resources/Backend/Gamification/AvatarItems.php b/app/Resources/Backend/Gamification/AvatarItems.php new file mode 100644 index 0000000..f37eaf6 --- /dev/null +++ b/app/Resources/Backend/Gamification/AvatarItems.php @@ -0,0 +1,34 @@ + React::create() + ->component('Gamification.AvatarItems.Index') + ->path('gamification/avatar_items') + ->title('Avatar elementer'), + + 'create' => React::create() + ->component('Gamification.AvatarItems.Edit') + ->path("gamification/avatar_items/create/{avatar_item?}") + ->title('Opret avatar element') + ->parent('backend.gamification.avatar_items.index'), + + 'edit' => React::create() + ->component('Gamification.AvatarItems.Edit') + ->path("gamification/avatar_items/edit/{avatar_item?}") + ->title('Redigér avatar element') + ->parent('backend.gamification.avatar_items.index'), + + ]; + } +} diff --git a/app/Resources/Backend/Gamification/UserTitles.php b/app/Resources/Backend/Gamification/UserTitles.php new file mode 100644 index 0000000..7705d0a --- /dev/null +++ b/app/Resources/Backend/Gamification/UserTitles.php @@ -0,0 +1,34 @@ + React::create() + ->component('Gamification.UserTitles.Index') + ->path('gamification/user_titles') + ->title('Bruger titler'), + + 'create' => React::create() + ->component('Gamification.UserTitles.Edit') + ->path("gamification/user_titles/create/{user_title?}") + ->title('Opret bruger titel') + ->parent('backend.gamification.user_titles.index'), + + 'edit' => React::create() + ->component('Gamification.UserTitles.Edit') + ->path("gamification/user_titles/edit/{user_title?}") + ->title('Redigér bruger titel') + ->parent('backend.gamification.user_titles.index'), + + ]; + } +} diff --git a/app/Resources/Backend/Gamification/Users.php b/app/Resources/Backend/Gamification/Users.php new file mode 100644 index 0000000..18aebc1 --- /dev/null +++ b/app/Resources/Backend/Gamification/Users.php @@ -0,0 +1,28 @@ + React::create() + ->component('Gamification.Users.Index') + ->path('gamification/users') + ->title('Bruger'), + + 'view' => React::create() + ->component('Gamification.Users.View') + ->path("gamification/users/view/{user?}") + ->title('Se bruger') + ->parent('backend.gamification.users.index'), + + ]; + } +} diff --git a/app/Resources/Backend/Home.php b/app/Resources/Backend/Home.php new file mode 100644 index 0000000..044fdb1 --- /dev/null +++ b/app/Resources/Backend/Home.php @@ -0,0 +1,22 @@ +component('Home'); + $operation->path('/'); + } + +} diff --git a/app/Resources/Backend/Login.php b/app/Resources/Backend/Login.php new file mode 100644 index 0000000..e2cf23a --- /dev/null +++ b/app/Resources/Backend/Login.php @@ -0,0 +1,19 @@ + React::create()->path('login')->component('Login') + ]; + } + +} diff --git a/app/Resources/Backend/Moderation.php b/app/Resources/Backend/Moderation.php new file mode 100644 index 0000000..b8877de --- /dev/null +++ b/app/Resources/Backend/Moderation.php @@ -0,0 +1,42 @@ + React::create() + ->component('Moderation.Case.Index') + ->path('moderation/cases') + ->title('Sager'), + + 'next_case' => NextModerationCase::create()->path('moderation/cases/next/{except?}'), + + 'view_case' => React::create() + ->component('Moderation.Case.View') + ->path("moderation/case/view/{moderation_case}") + ->title('Sag') + ->parent('backend.moderation.index_cases'), + + 'index_suspensions' => React::create() + ->component('Moderation.Suspension.Index') + ->path('moderation/suspensions') + ->title('Suspenderinger'), + + 'edit_suspension' => React::create() + ->component('Moderation.Suspension.Edit') + ->path("moderation/suspension/edit/{user_suspension}") + ->title('Suspenderinger') + ->parent('backend.moderation.index_suspensions'), + ]; + } + +} diff --git a/app/Resources/Backend/Projects.php b/app/Resources/Backend/Projects.php new file mode 100644 index 0000000..75b8cfc --- /dev/null +++ b/app/Resources/Backend/Projects.php @@ -0,0 +1,34 @@ + React::create() + ->component('Projects.Categories.Index') + ->path('projects/categories') + ->title('Kategorier'), + + 'create_category' => React::create() + ->component('Projects.Categories.Edit') + ->path("projects/categories/create/{category?}") + ->title('Opret kategori') + ->parent('backend.projects.index_categories'), + + 'edit_category' => React::create() + ->component('Projects.Categories.Edit') + ->path("projects/categories/edit/{category?}") + ->title('Redigér kategori') + ->parent('backend.projects.index_categories'), + + ]; + } +} diff --git a/app/Resources/Backend/Users/Backend.php b/app/Resources/Backend/Users/Backend.php new file mode 100644 index 0000000..bbc4989 --- /dev/null +++ b/app/Resources/Backend/Users/Backend.php @@ -0,0 +1,35 @@ + React::create() + ->component('Users.Backend.Index') + ->path('users/backend') + ->title('Backend brugere'), + + 'create' => React::create() + ->component('Users.Backend.Edit') + ->path("users/backend/create/{user?}") + ->title('Opret backend bruger') + ->parent('backend.users.backend.index'), + + 'edit' => React::create() + ->component('Users.Backend.Edit') + ->path("users/backend/edit/{user?}") + ->title('Redigér backend bruger') + ->parent('backend.users.backend.index'), + + ]; + } + +} diff --git a/app/Resources/Backend/Users/Pirates.php b/app/Resources/Backend/Users/Pirates.php new file mode 100644 index 0000000..1638e35 --- /dev/null +++ b/app/Resources/Backend/Users/Pirates.php @@ -0,0 +1,42 @@ + React::create() + ->component('Users.Pirates.Index') + ->path('users/pirates') + ->title('Pirater'), + + 'create' => React::create() + ->component('Users.Pirates.Edit') + ->path("users/pirates/create/{user?}") + ->title('Opret pirat') + ->parent('backend.users.pirates.index'), + + 'edit' => React::create() + ->component('Users.Pirates.Edit') + ->path("users/pirates/edit/{user?}") + ->title('Redigér pirat') + ->parent('backend.users.pirates.index'), + + 'download' => DownloadExcelExport::create() + ->exportAs('BrugerStatistik.xlsx') + ->exportable(function () { + return new UsersExport(); + }), + ]; + } + +} diff --git a/app/Support/Abstracts/Reaction.php b/app/Support/Abstracts/Reaction.php new file mode 100644 index 0000000..ec6c243 --- /dev/null +++ b/app/Support/Abstracts/Reaction.php @@ -0,0 +1,60 @@ +belongsTo(User::class, 'user_id'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeUser(Builder $builder, $user = null) + { + if ($user instanceof User) { + $user = $user->id; + } + + if ($user !== null) { + return $builder->where('user_id', '=', $user); + } + + return $builder->whereNull('user_id'); + } + + public function scopeMine(Builder $builder) + { + return $builder->user(Auth::user()); + } + + public function scopeType(Builder $builder, $type) + { + return $builder->where('type', '=', $type); + } + + public function scopeEndorsement(Builder $q, bool $endorsement = true) + { + return $q->where( + 'type', + $endorsement ? '=' : '<>', + ReactionType::ENDORSEMENT + ); + } + +} diff --git a/app/Support/Contracts/Moderateable.php b/app/Support/Contracts/Moderateable.php new file mode 100644 index 0000000..3e36e3d --- /dev/null +++ b/app/Support/Contracts/Moderateable.php @@ -0,0 +1,37 @@ +setMetaAttribute() + const YOUTUBE = 'youtube'; + +} diff --git a/app/Support/Enums/CustomPermissions.php b/app/Support/Enums/CustomPermissions.php new file mode 100644 index 0000000..87faa61 --- /dev/null +++ b/app/Support/Enums/CustomPermissions.php @@ -0,0 +1,13 @@ + static::ADMIN, 'MODERATOR' => static::MODERATOR, 'MENTOR' => static::MENTOR]; + } + + public static function portal() + { + return ['LANDLUBBER' => static::LANDLUBBER, 'PIRATE' => static::PIRATE]; + } + + public static function grownups() + { + return [static::ADMIN, static::MODERATOR]; + } + + public static function children() + { + return [static::LANDLUBBER, static::PIRATE, static::MENTOR]; + } + +} diff --git a/app/Support/Enums/UserStatus.php b/app/Support/Enums/UserStatus.php new file mode 100644 index 0000000..a5c7bc0 --- /dev/null +++ b/app/Support/Enums/UserStatus.php @@ -0,0 +1,12 @@ +categories = AvatarCategory::values(); + $this->setCollection($this->getFields()); + } + + protected function getFields() + { + return collect($this->categories)->map(function ($category) { + return Field::create($category) + ->validates($this->getCategoryValidation($category)) + ->updates(function (UserAvatar $avatar, $category, $value) { + if (!is_null($value)) { + $avatar->setItem($category, $value); + } + }); + })->all(); + } + + protected function getCategoryValidation(string $category) + { + return [ + $category => [ + 'bail', + 'nullable', + 'int', + Rule::exists('avatar_items', 'id') + ->where(function (Builder $q) use ($category) { + $q->where('category', $category); + }), + 'avatar_item_is_available', + ], + ]; + } + + protected function checkRequest(Request $request) + { + return $request->hasAny($this->categories); + } +} diff --git a/app/Support/Fields/PersistenceField.php b/app/Support/Fields/PersistenceField.php new file mode 100644 index 0000000..fe40efc --- /dev/null +++ b/app/Support/Fields/PersistenceField.php @@ -0,0 +1,36 @@ +update ?? null; + } + + protected function performUpdate(Model $model, Request $request) + { + $update = $this->getUpdateMethod(); + + return ($update !== null) ? + $update($model, $request, $this) : + null; + } +} diff --git a/app/Support/Services/Moderation/Abstracts/Action.php b/app/Support/Services/Moderation/Abstracts/Action.php new file mode 100644 index 0000000..a2391c8 --- /dev/null +++ b/app/Support/Services/Moderation/Abstracts/Action.php @@ -0,0 +1,111 @@ +case = $case; + $this->moderateable = $case->moderateable; + } + + /** + * Execute the action and log it to DB + * + * @param ModerationCase $case + * @param string|null $note + * @param array $args + * @return mixed + * @throws Exception + */ + public static function execute(ModerationCase $case, string $note = null, ...$args) + { + $action = new static($case); + + if (!$action->canPerform()) return false; + + $result = $action->perform(...$args); + + if ($log = $action->logAction($note)) { + $action->afterExecute($log); + } + + return $result; + } + + protected function afterExecute(ModerationAction $log) + { + if ( + static::TYPE === ModerationActionType::RESOLUTION + && !$this->case->is_resolved + && $log->user_id !== null // If moderation wasn't executed by a user, it was probably automatic. + ) { + $this->case->performAsyncModeration(Resolve::class, null, $log); + } + } + + protected function canPerform(): bool + { + return true; + } + + /** + * Perform the specific action + * + * @param array $args + * @return mixed + */ + protected abstract function perform(...$args); + + /** + * Creates a \App\Models\Moderation\ModerationAction and saves to DB + * + * @param string|null $note + * @return \Illuminate\Database\Eloquent\Model|false + */ + protected function logAction(string $note = null) + { + return $this->case->actions()->save( + $this->makeAction($note) + ); + } + + /** + * Make the action log instance + * + * @param string|null $note + * @return ModerationAction + */ + protected function makeAction(string $note = null): ModerationAction + { + $action = new ModerationAction(); + $action->type = static::TYPE; + $action->action_class = static::class; + $action->note = $note; + $action->meta = $this->meta; + $action->user_id = optional(Auth::user())->id; + + return $action; + } +} diff --git a/app/Support/Services/Moderation/Actions/ActivateModerateable.php b/app/Support/Services/Moderation/Actions/ActivateModerateable.php new file mode 100644 index 0000000..de5e5de --- /dev/null +++ b/app/Support/Services/Moderation/Actions/ActivateModerateable.php @@ -0,0 +1,23 @@ +moderateable->activate(); + } +} diff --git a/app/Support/Services/Moderation/Actions/Appeal.php b/app/Support/Services/Moderation/Actions/Appeal.php new file mode 100644 index 0000000..3b0b5a0 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/Appeal.php @@ -0,0 +1,39 @@ +meta = [ + 'appeal_id' => $appeal->id, + ]; + } + + } + + protected function afterExecute(ModerationAction $log) + { + if (!$this->case->is_pending) { + $this->case->performAsyncModeration(Open::class, null, $log); + } + + parent::afterExecute($log); + } +} diff --git a/app/Support/Services/Moderation/Actions/ArchiveModerateable.php b/app/Support/Services/Moderation/Actions/ArchiveModerateable.php new file mode 100644 index 0000000..cc17f8f --- /dev/null +++ b/app/Support/Services/Moderation/Actions/ArchiveModerateable.php @@ -0,0 +1,23 @@ +moderateable->archive(); + } +} diff --git a/app/Support/Services/Moderation/Actions/BlockUser.php b/app/Support/Services/Moderation/Actions/BlockUser.php new file mode 100644 index 0000000..3eca4c5 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/BlockUser.php @@ -0,0 +1,46 @@ +case->user; + + if (isset($args[0]) && isset($args[0]['recursive'])) { + $recursive = filter_var($args[0]['recursive'], FILTER_VALIDATE_BOOLEAN); + } + + if ($recursive) { + $user->removeAllContent(); + } + + $this->meta = [ + 'recursive' => $recursive, + ]; + + $user->delete(); + } + + protected function afterExecute(ModerationAction $log) + { + $this->case->performAsyncModeration(NotifyBlockedUser::class, null, $log); + + parent::afterExecute($log); + } +} diff --git a/app/Support/Services/Moderation/Actions/Comment.php b/app/Support/Services/Moderation/Actions/Comment.php new file mode 100644 index 0000000..3b6fcda --- /dev/null +++ b/app/Support/Services/Moderation/Actions/Comment.php @@ -0,0 +1,23 @@ +moderateable->delete(); + } + + protected function afterExecute(ModerationAction $log) + { + if (method_exists($this->moderateable, 'afterDeleteModeration')) { + $this->moderateable->afterDeleteModeration($this->case, $log); + } + + parent::afterExecute($log); + } +} diff --git a/app/Support/Services/Moderation/Actions/LockModerateable.php b/app/Support/Services/Moderation/Actions/LockModerateable.php new file mode 100644 index 0000000..84c6d5e --- /dev/null +++ b/app/Support/Services/Moderation/Actions/LockModerateable.php @@ -0,0 +1,23 @@ +moderateable->lock(); + } +} diff --git a/app/Support/Services/Moderation/Actions/ModerateAutomatically.php b/app/Support/Services/Moderation/Actions/ModerateAutomatically.php new file mode 100644 index 0000000..5bad72e --- /dev/null +++ b/app/Support/Services/Moderation/Actions/ModerateAutomatically.php @@ -0,0 +1,44 @@ +case->status = ModerationCaseStatus::AUTOMATICALLY_MODERATED; + $this->case->save(); + + if (isset($args[0]) && ($moderation_action = $args[0]) instanceof ModerationAction) { + $this->meta = [ + 'moderation_action_id' => $moderation_action->id, + ]; + } + } + + protected function canPerform(): bool + { + return $this->case->is_pending && parent::canPerform(); + } + + protected function makeAction(string $note = null): ModerationAction + { + return tap(parent::makeAction($note), function ($action) { + $action->user_id = null; + }); + } +} diff --git a/app/Support/Services/Moderation/Actions/NotifyBlockedUser.php b/app/Support/Services/Moderation/Actions/NotifyBlockedUser.php new file mode 100644 index 0000000..f31ed92 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/NotifyBlockedUser.php @@ -0,0 +1,42 @@ +meta = [ + 'moderation_action_id' => $moderation_action->id, + ]; + } + + $user = $this->case->user()->withTrashed()->firstOrFail(); + + Mail::to($user->email)->send(new BlockedUserNotice($this->case, $user, optional($moderation_action)->note, false)); + + if ($user->parent_email !== $user->email && $user->parent_email !== null) { + Mail::to($user->parent_email)->send(new BlockedUserNotice($this->case, $user, optional($moderation_action)->note, true)); + } + } +} diff --git a/app/Support/Services/Moderation/Actions/Open.php b/app/Support/Services/Moderation/Actions/Open.php new file mode 100644 index 0000000..7654c9d --- /dev/null +++ b/app/Support/Services/Moderation/Actions/Open.php @@ -0,0 +1,50 @@ +meta = [ + 'moderation_request_id' => $moderation_request->id, + ]; + } + + $this->case->status = ModerationCaseStatus::PENDING; + $this->case->save(); + } + + protected function canPerform(): bool + { + return parent::canPerform() && !in_array($this->case->status, [ + ModerationCaseStatus::PENDING, + ModerationCaseStatus::AUTOMATICALLY_MODERATED, + ]); + } + + protected function makeAction(string $note = null): ModerationAction + { + return tap(parent::makeAction($note), function ($action) { + $action->user_id = null; + }); + } +} diff --git a/app/Support/Services/Moderation/Actions/Reject.php b/app/Support/Services/Moderation/Actions/Reject.php new file mode 100644 index 0000000..8c665e9 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/Reject.php @@ -0,0 +1,30 @@ +case->status = ModerationCaseStatus::REJECTED; + $this->case->save(); + + $this->case->requests() + ->resolved(false) + ->update(['resolved_at' => now()]); + } +} diff --git a/app/Support/Services/Moderation/Actions/RemoveMessageContent.php b/app/Support/Services/Moderation/Actions/RemoveMessageContent.php new file mode 100644 index 0000000..c8cb923 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/RemoveMessageContent.php @@ -0,0 +1,24 @@ +moderateable->moderate(); + } +} diff --git a/app/Support/Services/Moderation/Actions/RemoveThreadMessageContent.php b/app/Support/Services/Moderation/Actions/RemoveThreadMessageContent.php new file mode 100644 index 0000000..c15f23c --- /dev/null +++ b/app/Support/Services/Moderation/Actions/RemoveThreadMessageContent.php @@ -0,0 +1,18 @@ +moderateable->originalMessage)->moderate(); + } +} diff --git a/app/Support/Services/Moderation/Actions/Resolve.php b/app/Support/Services/Moderation/Actions/Resolve.php new file mode 100644 index 0000000..685b579 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/Resolve.php @@ -0,0 +1,48 @@ +case->status = ModerationCaseStatus::MODERATED; + $this->case->save(); + + $this->case->requests() + ->resolved(false) + ->update(['resolved_at' => now()]); + + if (isset($args[0]) && ($moderation_action = $args[0]) instanceof ModerationAction) { + $this->meta = [ + 'moderation_action_id' => $moderation_action->id, + ]; + } + } + + protected function canPerform(): bool + { + return !$this->case->is_resolved && parent::canPerform(); + } + + protected function makeAction(string $note = null): ModerationAction + { + return tap(parent::makeAction($note), function ($action) { + $action->user_id = null; + }); + } +} diff --git a/app/Support/Services/Moderation/Actions/SuspendUser.php b/app/Support/Services/Moderation/Actions/SuspendUser.php new file mode 100644 index 0000000..a55b717 --- /dev/null +++ b/app/Support/Services/Moderation/Actions/SuspendUser.php @@ -0,0 +1,63 @@ +meta = [ + 'from' => $from, + 'to' => $to, + ]; + + $this->case->user->suspend($from, $to, Auth::user()); + } + + /** + * @param $date + * @return Carbon + */ + public static function getCarbon($date) + { + if (is_null($date) || $date instanceof Carbon) return $date; + + try { + $date = Carbon::parse($date); + + } catch (Exception $e) { + static::throwMessage('Invalid date formats provided for User suspension.'); + } + + return $date; + } + + public static function throwMessage(string $message) + { + throw new ModerationActionException($message, 400); + } + +} diff --git a/app/Support/Services/Moderation/ModerationActionException.php b/app/Support/Services/Moderation/ModerationActionException.php new file mode 100644 index 0000000..72c6491 --- /dev/null +++ b/app/Support/Services/Moderation/ModerationActionException.php @@ -0,0 +1,10 @@ +client = new Client([ + 'base_uri' => config($this->config . 'base_uri'), + ]); + } + + /** + * Send a verify request to google, and return the response body if successfully verified + * + * @param string $token + * @param string $action + * @return array|bool|mixed + */ + public function verify(string $token, string $action) + { + $body = [ + 'form_params' => [ + 'response' => $token, + 'remoteip' => $_SERVER['REMOTE_ADDR'], + 'secret' => config($this->config . 'secret'), + ], + ]; + + try { + + $response = $this->client->post('siteverify', $body); + $body = json_decode($response->getBody()->getContents()); + + } catch (Exception $e) { + return false; + } + + if ($response->getStatusCode() !== 200 + || $body->success !== true + || $body->action !== $action) { + return false; + } + + return $body; + } + +} diff --git a/app/Support/Services/Shutdown.php b/app/Support/Services/Shutdown.php new file mode 100644 index 0000000..3519abf --- /dev/null +++ b/app/Support/Services/Shutdown.php @@ -0,0 +1,140 @@ +getStartsAt() <= $now && $now <= $this->getEndsAt(); + } + + public function isDeniedAccess(User $user, $permission_to_check) + { + $roles = config('auth.shutdown.roles', []); + $permissions = config('auth.shutdown.permissions', []); + + if ($user->hasAnyRole($roles)) { + if (!empty($permissions)) { + + if (in_array($permission_to_check, $permissions)) { + return true; + } + + foreach ($permissions as $permission) { + if (preg_match('/' . $permission . '/', $permission_to_check)) { + return true; + } + } + } + } + + return false; + } + + ///////////////////////////// + /// Start and end helpers + ///////////////////////////// + + public function getStartsAt() + { + return $this->getStartOfToday()->setSeconds($this->getFromInSeconds()); + } + + public function getEndsAt() + { + + if($this->getFromInSeconds() > $this->getToInSeconds()) { + return $this->getStartsAt()->copy()->addDay()->startOfDay()->setSeconds($this->getToInSeconds()); + } + + return $this->getStartsAt()->copy()->startOfDay()->setSeconds($this->getToInSeconds()); + } + + ///////////////////////////// + /// Config helpers + ///////////////////////////// + + public function getFromInSeconds() + { + /// Default -> 21:30 + return config('auth.shutdown.from', 1290) * 60; + } + + public function getToInSeconds() + { + // Default -> 6:30 + return config('auth.shutdown.to', 390) * 60; + } + + ///////////////////////////// + /// Carbon helpers + ///////////////////////////// + + public function getStartOfToday() + { + return Carbon::now() + ->setTimezone('Europe/Copenhagen') + ->startOfDay(); + } + + public function getStartOfTomorrow() + { + return $this->getStartOfToday()->copy()->addDay(); + } + + public function getCurrentSecondInDay() + { + return Carbon::now() + ->setTimezone('Europe/Copenhagen') + ->diffInSeconds( + Carbon::now() + ->setTimezone('Europe/Copenhagen') + ->startOfDay() + ); + } + + ///////////////////////////// + /// ENV Helpers + ///////////////////////////// + + public function getEnv() + { + return [ + 'is_active' => $this->isWithinShutdownPeriod(), + 'starts_at' => $this->getStartsAt()->format('Y-m-d H:i:s'), + 'ends_at' => $this->getEndsAt()->format('Y-m-d H:i:s'), + 'warning_delay' => 30 + ]; + } + + ///////////////////////////// + /// Broadcasting helpers + ///////////////////////////// + + public function broadcastStart() + { + event(new ShutdownStarted()); + } + + public function broadcastEnd() + { + event(new ShutdownEnded()); + } + +} \ No newline at end of file diff --git a/app/Support/Services/Twitch.php b/app/Support/Services/Twitch.php new file mode 100644 index 0000000..c10156b --- /dev/null +++ b/app/Support/Services/Twitch.php @@ -0,0 +1,211 @@ +clientId = config('services.twitch.client_id'); + $accessToken = $this->getAccessToken(); + + $this->client = new Client([ + 'base_uri' => 'https://api.twitch.tv/helix/', + 'headers' => [ + 'Authorization' => "Bearer {$accessToken}", + 'Client-ID' => $this->clientId, + ], + ]); + } + + ///////////////////////// + /// Public Methods + ///////////////////////// + + /** + * Gets current streams for the provided channel name + * + * @param string $channelName + * + * @return Collection + */ + public function getCurrentStreams(string $channelName) + { + return $this->makeRequest( + 'GET', + 'streams', + ['query' => ['user_login' => $channelName]] + ); + } + + /** + * @param int $userId + * + * @return Collection + */ + public function getLatestVideos(int $userId, int $limit = 3) + { + $limit = max(1, $limit); + + return $this->makeRequest( + 'GET', + 'videos', + ['query' => [ + 'user_id' => $userId, + 'first' => $limit, + ]] + ); + } + + ///////////////////////// + /// Access Token + ///////////////////////// + + /** + * Gets a new access token, or a cached one if valid + * + * @return string A valid Access Token + * @throws Exception + */ + private function getAccessToken(): string + { + $token = cache(static::CACHE_KEY, null); + + if (!$token || !$this->tokenIsValid($token)) { + $token = $this->fetchNewAccessToken(); + } + + return $token ?? ''; + } + + /** + * Checks if token is valid, for at least another 10 minutes + * + * @param string $token + * @return bool + */ + private function tokenIsValid(string $token): bool + { + if (blank($token)) { + return false; + } + + try { + $body = $this->getSuccessfulJsonBody((new Client)->request( + 'GET', + 'https://id.twitch.tv/oauth2/validate', + ['headers' => ['Authorization' => "OAuth {$token}"]] + )); + + if (!is_object($body) || !isset($body->expires_in, $body->client_id)) { + return false; + } + + // Token is valid at least another 10 minutes + if ($body->client_id === $this->clientId && $body->expires_in > 600) { + return true; + } + + } catch (Exception $e) { + logger()->error($e->getMessage()); + } + + return false; + } + + /** + * Fetches and caches new access token + * + * @return string|null + */ + private function fetchNewAccessToken(): ?string + { + try { + $body = $this->getSuccessfulJsonBody((new Client)->request( + 'POST', + 'https://id.twitch.tv/oauth2/token', + [ + 'json' => [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->clientId, + 'client_secret' => config('services.twitch.client_secret'), + ], + ] + )); + + if (is_object($body) && isset($body->access_token, $body->expires_in)) { + cache([static::CACHE_KEY => $body->access_token], $body->expires_in); + + return $body->access_token; + } + + } catch (Exception $e) { + logger()->error($e->getMessage()); + } + + return null; + } + + ///////////////////////// + /// Helpers + ///////////////////////// + + /** + * If a Response with status 200 is provided, returns a body Object. Otherwise null. + * + * @param ResponseInterface $response + * + * @return object|null + */ + protected function getSuccessfulJsonBody(ResponseInterface $response): ?object + { + $status_code = $response->getStatusCode(); + + if ($status_code === 200) { + $content = $response->getBody()->getContents(); + + if (is_string($content)) { + $data = json_decode($content); + + if ($data && is_object($data)) { + return $data; + } + } + } + + return null; + } + + /** + * Makes a request with provided client arguments + * + * @param mixed ...$args + * @return Collection + */ + protected function makeRequest(...$args) + { + try { + + $body = $this->getSuccessfulJsonBody($this->client->request(...$args)); + + if ($body !== null && isset($body->data) && is_array($body->data) && !empty($body->data)) { + return collect($body->data); + } + + } catch (Exception $e) { + logger()->error($e->getMessage()); + } + + return collect(); + } +} diff --git a/app/Support/Traits/CachesAttributes.php b/app/Support/Traits/CachesAttributes.php new file mode 100644 index 0000000..c989218 --- /dev/null +++ b/app/Support/Traits/CachesAttributes.php @@ -0,0 +1,50 @@ +runningInConsole()) { + return $getter(); + } + + $key = $attribute; + if(is_callable([$this, 'getKey'])){ + $key = $attribute.'_'.$this->getKey().'_'.static::class; + } + return static::$cached_attributes[$key] ?? (static::$cached_attributes[$key] = $getter()); + } + + protected function cacheClear(string $attribute = null) + { + $key = $attribute; + if (is_string($key)) { + if(is_callable([$this, 'getKey'])){ + $key = $attribute.'_'.$this->getKey().'_'.static::class; + } + unset(static::$cached_attributes[$key]); + } else { + static::$cached_attributes = []; + } + + return $this; + } + + protected function cacheSet(string $attribute, $value) + { + $key = $attribute; + if(is_callable([$this, 'getKey'])){ + $key = $attribute.'_'.$this->getKey().'_'.static::class; + } + static::$cached_attributes[$key] = $value; + return $this; + } + +} diff --git a/app/Support/Traits/Changeable.php b/app/Support/Traits/Changeable.php new file mode 100644 index 0000000..134bd44 --- /dev/null +++ b/app/Support/Traits/Changeable.php @@ -0,0 +1,35 @@ +logChanges) { + Change::record($model); + } + }); + } + + public function ignoredChangeableAttributes() + { + return ['created_at', 'updated_at']; + } + + public function saveWithoutLogging() + { + try { + $this->logChanges = false; + + return $this->save(); + } finally { + $this->logChanges = true; + } + } +} diff --git a/app/Support/Traits/HasSortableScore.php b/app/Support/Traits/HasSortableScore.php new file mode 100644 index 0000000..d4d24c1 --- /dev/null +++ b/app/Support/Traits/HasSortableScore.php @@ -0,0 +1,175 @@ +sort_score = array_sum($this->getSortScoreFactors()); + //dump(get_class($this), $this->id, $this->sort_score, $this->getSortScoreFactors()); + $this->save(); + } + + public function getSortScoreFactors() + { + return [ + 'popularity' => $this->getSortPopularityScore(), + 'quality' => $this->getSortQualityScore(), + 'freshness' => $this->getSortFreshnessScore(), + ]; + } + + ////////////////////////////////// + /// Popularity + ////////////////////////////////// + + public function getSortPopularityWeight() + { + /// How much should the popularity score contribute to the final score? + return 2; + } + + public function getSortPopularityScore() + { + return ($this->getContributionToRecentGlobalActivity() / ($this->getRecentGlobalActivity() ?: 1))* 100 * $this->getSortPopularityWeight(); + } + + public function getRecentGlobalActivity() + { + return 1; + } + + public function getContributionToRecentGlobalActivity() + { + return 1; + } + + ////////////////////////////////// + /// Freshness + ////////////////////////////////// + + public function getSortFreshnessAge() + { + return 0; + } + + public function getSortFreshnessWeight() + { + return 0.4; + } + + public function getSortFreshnessDecayFactor() + { + return 10; + } + + public function getSortFreshnessScore() + { + + $age_in_days = $this->getSortFreshnessAge(); + $decay_factor = $this->getSortFreshnessDecayFactor(); + + $decay = $age_in_days * $decay_factor; + + $score = 100 - $decay; + + return max(0, $score) * $this->getSortFreshnessWeight(); + } + + ////////////////////////////////// + /// Quality + ////////////////////////////////// + + public function getSortQualityContributors() + { + + /// Expected format: collect([ + /// [ + /// "date" => Carbon, + /// "amount" => Integer + /// ] + /// ]) + + if (method_exists($this, 'endorsements')) { + $endorsements = $this->endorsements; + + return $endorsements + ->groupBy(function ($endorsement) { + return $endorsement->updated_at->format('Y-m-d'); + }) + ->transform(function ($endorsements) { + return [ + 'date' => $endorsements->first()->updated_at->copy(), + 'amount' => $endorsements->count() + ]; + }); + + } + + return collect(); + } + + public function getSortQualityWeight() + { + /// How much should the quality score contribute to the final score? + return 0.4; + } + + public function getSortQualityDecayFactor() + { + /// How much will the score decay with for each day? + return 0.4; + } + + public function getSortQualityContributionMultiplier() + { + return 300; + } + + public function getSortQualityScore() + { + + $contributors = $this->getSortQualityContributors(); + + if (!($contributors instanceof Collection) || $contributors->isEmpty()) { + return 0; + } + + return $contributors->sum(function ($contributor) { + + if (!isset($contributor['amount']) || !isset($contributor['date']) || !($contributor['date'] instanceof Carbon)) { + return 0; + } + + $contribution = ($contributor['amount'] * $this->getSortQualityContributionMultiplier()); + $decay = ($this->getSortQualityDecayFactor() * Carbon::today()->diffInDays($contributor['date'])); + + if(empty($decay)) { + $decay = 1; + } + + return ($contribution / $decay) * $this->getSortQualityWeight(); + }); + } + +} diff --git a/app/Support/Traits/HasUserGeneratedContent.php b/app/Support/Traits/HasUserGeneratedContent.php new file mode 100644 index 0000000..5aea877 --- /dev/null +++ b/app/Support/Traits/HasUserGeneratedContent.php @@ -0,0 +1,45 @@ +cleanAllUserContent(); + }); + } + + protected function cleanAllUserContent() + { + foreach ($this->userGeneratedColumns() as $attribute) { + if (isset($this->attributes[$attribute])) { + $this->attributes[$attribute] = $this->cleanUserContent($this->attributes[$attribute]); + } + } + } + + public function cleanUserContent(string $content = null): string + { + return clean($content, $this->purifierConfig()); + } + + public function correctUserContent(string $content = null): string { + return preg_replace('/(<]*)>/i', '$1 rel="noopener noreferrer" target="_blank">', $content); + } + + public function userGeneratedColumns(): array + { + return $this->userGeneratedContent ?? []; + } + + public function purifierConfig(): string + { + return defined('static::PURIFIER_CONFIG') ? static::PURIFIER_CONFIG : 'default'; + } + +} diff --git a/app/Support/Traits/Meta/HasMetaAttributes.php b/app/Support/Traits/Meta/HasMetaAttributes.php new file mode 100644 index 0000000..1e5cc96 --- /dev/null +++ b/app/Support/Traits/Meta/HasMetaAttributes.php @@ -0,0 +1,232 @@ +metaModel)) { + return $this->metaModel; + } + + return static::class . 'Meta'; + } + + private function getMetaForeignKey() + { + return strtolower(Str::snake((new \ReflectionClass($this))->getShortName())) . '_' . $this->getKeyName(); + } + + private function getMetaType($name) + { + return isset($this->metaTypes, $this->metaTypes[$name]) ? $this->metaTypes[$name] : null; + } + + private function getMetaCast($name) + { + $type = $this->getMetaType($name); + + switch ($type) { + case 'int': + case 'integer': + return 'SIGNED'; + case 'bool': + case 'boolean': + return 'UNSIGNED'; + } + } + + //////////////////////////////// + // Relationships + //////////////////////////////// + + public function metaAttributes() + { + $model = $this->getMetaModel(); + + return $this->hasMany($model, $this->getMetaForeignKey()); + } + + //////////////////////////////// + // Scopes + //////////////////////////////// + + public function scopeWithMeta($q, $name, $operator, $value = null) + { + if (count(func_get_args()) === 3) { + $value = $operator; + $operator = '='; + } + + return $q->whereHas('metaAttributes', function ($q) use ($name, $operator, $value) { + return $q->where('name', $name)->where('value', $operator, $value); + }); + } + + public function scopeOrderByMeta(Builder $q, $name, $direction = 'asc') + { + // Get model details + $model = static::class; + $dummy = new $model; + $table = $dummy->getTable(); + $id = $dummy->getKeyName(); + + // Get meta model details + $metaModel = $this->getMetaModel(); + $dummyMeta = new $metaModel; + $meta_table = $dummyMeta->getTable(); + $meta_id = $dummyMeta->getKeyName(); + + // Get foreign + $foreign = $this->getForeignKey(); + + // Apply to query + $q->join($meta_table, "$meta_table.$foreign", '=', "$table.$id")->select("$table.*")->where("$meta_table.name", $name); + + $cast = $this->getMetaCast($name); + + isset($cast) ? $q->orderBy("CAST($meta_table.value as $cast)", $direction) : $q->orderBy("$meta_table.value", $direction); + + return $q; + } + + //////////////////////////////// + // Helpers + //////////////////////////////// + + public function getMetaAttribute($name) + { + // TODO - caching + //return $this->cache($name, function () use ($name) { + return $this->metaAttributes->where('name', $name)->first(); + //}); + } + + public function getMeta($name, $default = null) + { + $attr = $this->getMetaAttribute($name); + + return $attr ? $attr->value : $default; + } + + public function hasMeta($name) + { + return $this->getMetaAttribute($name) !== null; + } + + public function metaUpdated($name, $value){ + + } + + public function setMeta($name, $value, $type = null) + { + $model = $this->getMetaModel(); + $attr = $this->getMetaAttribute($name); + + if (is_null($attr)) { + $attr = new $model; + $attr->name = $name; + + // Cache new instance + //$this->cache($name, $attr); // TODO - caching + } + + $attr->type = $type ?? $this->getMetaType($name) ?? $attr->type ?? 'string'; + $attr->value = $value; + $attr->{$this->getMetaForeignKey()} = $this->getKey(); + + if ($attr->isDirty()) { + $this->metaUpdated($attr->name, $attr->value); + } + + $this->metaAttributes()->save($attr); + $this->load('metaAttributes'); + } + + public function setMetas($metas, $skip_insert = false) + { + $model = $this->getMetaModel(); + + $existing_metas = $this->metaAttributes->keyBy('name'); + + if (!$metas->isEmpty()) { + + $now = Carbon::now(); + + foreach ($metas as $name => $meta) { + + $type = $this->getMetaType($name); + + if (is_array($meta)) { + $type = $meta[1]; + $meta = $meta[0]; + } + + if (is_null($type)) { + $type = 'string'; + } + + $existing_meta = $existing_metas->get($name); + if ($existing_meta === null) { + static::$metasToInsert[] = [ + $this->getForeignKey() => $this->getKey(), + 'name' => $name, + 'type' => $type, + 'value' => $meta, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } else { + $existing_meta->value = $meta; + $existing_meta->type = $type; + $existing_meta->save(); + } + } + + if (!$skip_insert) { + $this->insertPendingMetas(); + } + + } + + } + + public function cacheMetasFromLoaded() + { + $metas = $this->metaAttributes; + + if ($metas->isNotEmpty()) { + foreach ($metas as $meta) { + $this->cached_attributes[$meta->name] = $meta; + } + } + } + + public static function insertPendingMetas() + { + $model = (new static())->getMetaModel(); + if (!empty(static::$metasToInsert)) { + $chunks = array_chunk(static::$metasToInsert, 1000); + foreach ($chunks as $chunk) { + DB::table((new $model)->getTable())->insert($chunk); + } + static::$metasToInsert = []; + } + } + +} diff --git a/app/Support/Traits/Meta/MetaModel.php b/app/Support/Traits/Meta/MetaModel.php new file mode 100644 index 0000000..a16291f --- /dev/null +++ b/app/Support/Traits/Meta/MetaModel.php @@ -0,0 +1,84 @@ +attributes['value']); + + case 'int': + case 'integer': + return intval($this->attributes['value']); + + case 'float': + return floatval($this->attributes['value']); + + case 'bool': + case 'boolean': + return intval($this->attributes['value']) === 1; + + case 'date': + return strlen($this->attributes['value']) > 0 ? + Carbon::createFromFormat('Y-m-d H:i:s', $this->attributes['value']) : + null; + + default: + return $this->attributes['value']; + } + } + + //////////////////////////////// + // Accessors + //////////////////////////////// + + public function getValueAttribute() + { + return $this->getValueAs($this->type); + } + + public function setValueAttribute($value) + { + switch ($this->type) { + case 'json': + $this->attributes['value'] = json_encode($value); + break; + + case 'int': + case 'integer': + $this->attributes['value'] = intval($value); + break; + + case 'float': + $this->attributes['value'] = floatval($value); + break; + + case 'bool': + case 'boolean': + $this->attributes['value'] = $value ? 1 : 0; + break; + + case 'date': + $this->attributes['value'] = $value instanceof Carbon ? + $value->format('Y-m-d H:i:s') : + ''; + + break; + + default: + $this->attributes['value'] = $value; + break; + } + } + +} diff --git a/app/Support/Traits/Moderation/Blockable.php b/app/Support/Traits/Moderation/Blockable.php new file mode 100644 index 0000000..dc9f42f --- /dev/null +++ b/app/Support/Traits/Moderation/Blockable.php @@ -0,0 +1,18 @@ +forUser($user)->update(['blocked_user' => true]); + } + + public static function unblockForUser(User $user) + { + static::query()->forUser($user)->update(['blocked_user' => false]); + } +} diff --git a/app/Support/Traits/Moderation/HasModerationActions.php b/app/Support/Traits/Moderation/HasModerationActions.php new file mode 100644 index 0000000..2dfab0e --- /dev/null +++ b/app/Support/Traits/Moderation/HasModerationActions.php @@ -0,0 +1,62 @@ +getCustomAutomaticResolutionActions($suspendUser); + + if ($suspendUser) { + $actions = array_merge($actions, [ + ModerationActions::SUSPEND_USER => [ + now(), + now()->addHours(config('permissions.moderation.suspension_time', 12)), + ], + ]); + } + + return $actions; + } + + /** + * Default ModerationActions + * + * @return array + */ + protected static function getCommonModerationActions(): array + { + return [ + ModerationActions::OPEN, + ModerationActions::RESOLVE, + ModerationActions::MODERATE_AUTOMATICALLY, + ModerationActions::REJECT, + ModerationActions::COMMENT, + ModerationActions::BLOCK_USER, + ModerationActions::SUSPEND_USER, + ModerationActions::APPEAL, + ModerationActions::NOTIFY_BLOCKED_USER, + ]; + } + +} diff --git a/app/Support/Traits/Moderation/HasModerationRequests.php b/app/Support/Traits/Moderation/HasModerationRequests.php new file mode 100644 index 0000000..8998cd1 --- /dev/null +++ b/app/Support/Traits/Moderation/HasModerationRequests.php @@ -0,0 +1,113 @@ +moderationCase)->triggerUpdatedEvent(); + }); + } + + /** + * ModerationCase relation + * + * @return \Illuminate\Database\Eloquent\Relations\MorphOne + */ + public function moderationCase() + { + return $this->morphOne(ModerationCase::class, 'moderateable'); + } + + /** + * Determine weather a User can flag the Moderateable entity + * Prevents User from reporting himself, or reporting the same entity multiple times before it has been resolved + * + * @param User $user + * @return bool + */ + public function userCanFlag(User $user): bool + { + return $this->getResponsibleUserId() !== $user->id && + !$this->userHasUnresolvedModerationRequests($user); + } + + public function userHasUnresolvedModerationRequests(User $user): bool + { + return $this->moderationCase() + ->whereHas('requests', function ($q) use ($user) { + return $q + ->where('reporter_id', $user->id) + ->resolved(false); + }) + ->exists(); + } + + /** + * Create the \App\Models\Moderation\ModerationRequest + * + * @param string $reason + * @param string $comment + */ + public function flag(string $reason, string $comment = null): void + { + $case = $this->moderationCase ?? new ModerationCase(); + $case->user_id = $case->user_id ?? $this->getResponsibleUserId(); + + $this->moderationCase()->save($case); + + $request = new ModerationRequest(); + $request->reason = $reason; + $request->comment = $comment; + $request->reporter_id = optional(Auth::user())->id; + + $case->requests()->save($request); + + if (!$case->is_pending) { + $case->performModeration(Open::class, null, $request); + } + } + + public function randomModerationRequest(Faker $faker) + { + // 25% chance of a User getting reported + if (rand(0, 99) < 25) { + $reporters = User::query() + ->where('id', '<>', $this->getResponsibleUserId()) + ->inRandomOrder() + ->limit(rand(1, 5)) + ->get() + ->each(function ($reporter) use ($faker) { + Auth::onceUsingId($reporter->id); + + $this->load('moderationCase'); + + $this->flag( + $faker->randomElement(ModerationReasons::values()), + $faker->sentences(rand(1, 5), true) + ); + }); + } + } + + // This works as a workaround for Moderateable models which don't have SoftDeletes + // We need this specifically for the moderateable (morphTo()) relation on ModerationCase + public function scopeWithTrashed($q) + { + return $q; + } +} diff --git a/app/Support/Traits/Moderation/RemovableViaRequest.php b/app/Support/Traits/Moderation/RemovableViaRequest.php new file mode 100644 index 0000000..92e86a5 --- /dev/null +++ b/app/Support/Traits/Moderation/RemovableViaRequest.php @@ -0,0 +1,18 @@ +isDeleted() + && $this->moderationCase()->whereHas('requests', function ($q) { + return $q->unresolvedRemovalRequestForUserId($this->getResponsibleUserId()); + }) + ->exists(); + } + +} diff --git a/app/Support/Traits/Notifications/Repeatable.php b/app/Support/Traits/Notifications/Repeatable.php new file mode 100644 index 0000000..a607302 --- /dev/null +++ b/app/Support/Traits/Notifications/Repeatable.php @@ -0,0 +1,86 @@ +shouldNotify($this)) { + return []; + } + + return ['broadcast', RepeatableDatabaseChannel::class]; + } + + protected static function transKey(string $key) + { + return 'notifications.' . static::$type . ".{$key}"; + } + + /** + * Used to specify identifier key => value pairs + * Use arrow->syntax for nested JSON keys + * + * @param mixed $notifiable + * @return array + */ + private function getIdentifiers($notifiable) : array + { + return []; + } + + /** + * Queries for a previously existing notification of same type and with same identifiers + * + * @param mixed $notifiable + * @param MorphMany $query + * @return Notification|null + */ + public function getPredecessor($notifiable, MorphMany $query) + { + $identifiers = $this->getIdentifiers($notifiable); + + if (empty($identifiers)) { + return null; + } + + return $query + ->whereType(static::class) + ->where(function ($query) use ($identifiers) { + return $this->constrain($query, $identifiers); + }) + ->first(); + } + + /** + * Constrain $query by each identifier + * + * @param Builder $query + * @param array $identifiers + * @return Builder + */ + public function constrain(Builder $query, array $identifiers) + { + foreach ($identifiers as $key => $value) { + $query->where("data->{$key}", $value); + } + + return $query; + } + +} diff --git a/app/Support/Traits/Reactable.php b/app/Support/Traits/Reactable.php new file mode 100644 index 0000000..e25b5ed --- /dev/null +++ b/app/Support/Traits/Reactable.php @@ -0,0 +1,91 @@ +reactions()->type(ReactionType::ENDORSEMENT); + } + + public function my_endorsements() + { + return $this->reactions()->type(ReactionType::ENDORSEMENT)->mine(); + } + + public function likes() + { + return $this->reactions()->type(ReactionType::LIKE); + } + + public function my_likes() + { + return $this->reactions()->type(ReactionType::LIKE)->mine(); + } + + /** + * Get the related reaction class + * + * @return \Illuminate\Database\Eloquent\Model + */ + protected function getNewReactionInstance() + { + $class = get_class($this->reactions()->getRelated()); + + return new $class; + } + + /** + * Get the related reaction foreign key + * + * @return string + */ + protected function getReactableForeignKey(): string + { + return $this->reactions()->getForeignKeyName(); + } + + protected function handleReaction() + { + + } + + public function triggerReaction(string $type, bool $react = true, User $user = null) + { + $user = optional($user)->id ?? optional(Auth::user())->id; + + /** @var \Illuminate\Database\Eloquent\Model | null $reaction */ + $reaction = $this->reactions()->type($type)->user($user)->first(); + + if ($reaction === null && $react === true) { + $key = $this->getReactableForeignKey(); + + $reaction = $this->getNewReactionInstance(); + $reaction->{$key} = $this->getKey(); + $reaction->user_id = $user; + $reaction->type = $type; + $reaction->save(); + + } elseif ($reaction !== null && $react === false) { + $reaction->delete(); + } + + $this->handleReaction(); + + return [ + "count" => $this->reactions()->type($type)->count(), + ]; + } + +} diff --git a/app/Support/Traits/Rewardable.php b/app/Support/Traits/Rewardable.php new file mode 100644 index 0000000..982f990 --- /dev/null +++ b/app/Support/Traits/Rewardable.php @@ -0,0 +1,51 @@ +morphMany(UserRewardItem::class, 'item'); + } + + ////////////////////////////////// + /// Scopes + ////////////////////////////////// + + public function scopeForUser(Builder $q, $user = null) + { + logger('rewardable', [ + 'user' => $user, + 'is_string' => is_string($user), + 'is_null' => is_null($user), + 'blank' => blank($user), + ]); + + if($user instanceof User) { + $user = $user->id; + } + + if (is_numeric($user)) { + return $q->whereHas('userRewards', function (Builder $q) use ($user) { + $q->where('user_id', (int)$user); + }); + } + + return $q->whereHas('userRewards', function (Builder $q) { + $q->where('user_id', Auth::id()); + }); + } + +} diff --git a/artisan b/artisan new file mode 100644 index 0000000..5c23e2e --- /dev/null +++ b/artisan @@ -0,0 +1,53 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100755 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4b9e028 --- /dev/null +++ b/composer.json @@ -0,0 +1,83 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The Laravel Framework.", + "keywords": [ + "framework", + "laravel" + ], + "license": "MIT", + "require": { + "php": "^7.2", + "bugsnag/bugsnag-laravel": "^2.17", + "doctrine/dbal": "^2.10", + "fideloper/proxy": "^4.0", + "guzzlehttp/guzzle": "^6.3", + "intervention/image": "^2.5", + "laravel/framework": "^6.0", + "laravel/passport": "^v9.4.0", + "laravel/tinker": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0", + "maatwebsite/excel": "^3.1", + "maennchen/zipstream-php": "^1.2", + "mews/purifier": "^3.2", + "morningtrain/laravel-context": "^2.1", + "morningtrain/laravel-fields": "^1.0", + "morningtrain/laravel-fields-files": "^1.0", + "morningtrain/laravel-filters": "^1.0", + "morningtrain/laravel-https": "^1.2", + "morningtrain/laravel-permissions": "^2.0", + "morningtrain/laravel-react": "^1.0", + "morningtrain/laravel-resources": "^2.4", + "morningtrain/laravel-support": "^1.0", + "pusher/pusher-php-server": "4.1.3" + }, + "require-dev": { + "barryvdh/laravel-debugbar": "^3.2", + "beyondcode/laravel-dump-server": "^1.0", + "filp/whoops": "^2.0", + "fzaninotto/faker": "^1.4", + "mockery/mockery": "^1.0", + "nunomaduro/collision": "^3.0", + "phpunit/phpunit": "^7.5" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "autoload": { + "psr-4": { + "App\\": "app/" + }, + "classmap": [ + "database/seeds", + "database/factories" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..553cddd --- /dev/null +++ b/composer.lock @@ -0,0 +1,8381 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "263d22d95890db329577b3c67aa9bec9", + "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.171.19", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "e786e4a8b2ec85b258833d0570ff6b61348cbdb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e786e4a8b2ec85b258833d0570ff6b61348cbdb6", + "reference": "e786e4a8b2ec85b258833d0570ff6b61348cbdb6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "^2.5", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2021-01-15T19:26:11+00:00" + }, + { + "name": "bugsnag/bugsnag", + "version": "v3.25.0", + "source": { + "type": "git", + "url": "https://github.com/bugsnag/bugsnag-php.git", + "reference": "5a62ea2449e090ac103b1e9883a8fc33567d4241" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bugsnag/bugsnag-php/zipball/5a62ea2449e090ac103b1e9883a8fc33567d4241", + "reference": "5a62ea2449e090ac103b1e9883a8fc33567d4241", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", + "php": ">=5.5" + }, + "require-dev": { + "guzzlehttp/psr7": "^1.3", + "mtdowling/burgomaster": "dev-master#72151eddf5f0cf101502b94bf5031f9c53501a04", + "php-mock/php-mock-phpunit": "^1.1|^2.1", + "phpunit/phpunit": "^4.8.36|^7.5.15|^9.3.10", + "sebastian/version": ">=1.0.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.20-dev" + } + }, + "autoload": { + "psr-4": { + "Bugsnag\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Smith", + "email": "notifiers@bugsnag.com", + "homepage": "https://bugsnag.com" + } + ], + "description": "Official Bugsnag notifier for PHP applications.", + "homepage": "https://github.com/bugsnag/bugsnag-php", + "keywords": [ + "bugsnag", + "errors", + "exceptions", + "logging", + "tracking" + ], + "time": "2020-11-25T13:15:13+00:00" + }, + { + "name": "bugsnag/bugsnag-laravel", + "version": "v2.21.0", + "source": { + "type": "git", + "url": "https://github.com/bugsnag/bugsnag-laravel.git", + "reference": "b470a34b2b6ba33b2aa4ef1a01c64843ae8ab91b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bugsnag/bugsnag-laravel/zipball/b470a34b2b6ba33b2aa4ef1a01c64843ae8ab91b", + "reference": "b470a34b2b6ba33b2aa4ef1a01c64843ae8ab91b", + "shasum": "" + }, + "require": { + "bugsnag/bugsnag": "^3.20", + "bugsnag/bugsnag-psr-logger": "^1.4", + "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0", + "illuminate/support": "^5.0|^6.0|^7.0|^8.0", + "monolog/monolog": "^1.12|^2.0", + "php": ">=5.5" + }, + "require-dev": { + "graham-campbell/testbench": "^3.1|^4.0|^5.0", + "mockery/mockery": "^0.9.4|^1.3.1", + "phpunit/phpunit": "^4.8.36|^6.3.1|^7.5.15|^8.3.5|^9.3.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.18-dev" + } + }, + "autoload": { + "psr-4": { + "Bugsnag\\BugsnagLaravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Smith", + "email": "notifiers@bugsnag.com" + } + ], + "description": "Official Bugsnag notifier for Laravel applications.", + "homepage": "https://github.com/bugsnag/bugsnag-laravel", + "keywords": [ + "bugsnag", + "errors", + "exceptions", + "laravel", + "logging", + "tracking" + ], + "time": "2020-11-25T13:26:13+00:00" + }, + { + "name": "bugsnag/bugsnag-psr-logger", + "version": "v1.4.3", + "source": { + "type": "git", + "url": "https://github.com/bugsnag/bugsnag-psr-logger.git", + "reference": "222a7338bc5c39833c7c3922a175c539e996797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bugsnag/bugsnag-psr-logger/zipball/222a7338bc5c39833c7c3922a175c539e996797c", + "reference": "222a7338bc5c39833c7c3922a175c539e996797c", + "shasum": "" + }, + "require": { + "bugsnag/bugsnag": "^3.10", + "php": ">=5.5", + "psr/log": "^1.0" + }, + "require-dev": { + "graham-campbell/testbench-core": "^1.1", + "mockery/mockery": "^0.9.4|^1.3.1", + "phpunit/phpunit": "^4.8.36|^7.5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Bugsnag\\PsrLogger\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Smith", + "email": "notifiers@bugsnag.com", + "homepage": "https://bugsnag.com" + } + ], + "description": "Official Bugsnag PHP PSR Logger.", + "homepage": "https://github.com/bugsnag/bugsnag-psr", + "keywords": [ + "bugsnag", + "errors", + "exceptions", + "logging", + "psr", + "tracking" + ], + "time": "2020-02-26T22:02:20+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.2.9", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2021-01-12T12:10:35+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.2.1", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.4.0" + }, + "require-dev": { + "nikic/php-parser": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^4|^5" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "time": "2018-07-24T23:27:56+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/cache", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "13e3381b25847283a91948d04640543941309727" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", + "reference": "13e3381b25847283a91948d04640543941309727", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^6.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "time": "2020-07-07T18:54:01+00:00" + }, + { + "name": "doctrine/dbal", + "version": "2.12.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "adce7a954a1c2f14f85e94aed90c8489af204086" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/adce7a954a1c2f14f85e94aed90c8489af204086", + "reference": "adce7a954a1c2f14f85e94aed90c8489af204086", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.0", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.3 || ^8" + }, + "require-dev": { + "doctrine/coding-standard": "^8.1", + "jetbrains/phpstorm-stubs": "^2019.1", + "phpstan/phpstan": "^0.12.40", + "phpunit/phpunit": "^9.4", + "psalm/plugin-phpunit": "^0.10.0", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "vimeo/psalm": "^3.17.2" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "time": "2020-11-14T20:26:58+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "time": "2020-05-29T18:28:51+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^7.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "time": "2020-05-29T15:13:26+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/65b2d8ee1f10915efb3b55597da3404f096acba2", + "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "time": "2020-10-13T00:52:37+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.25", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.10" + }, + "require-dev": { + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2020-12-29T14:50:06+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2020-06-29T00:56:53+00:00" + }, + { + "name": "fideloper/proxy", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/fideloper/TrustedProxy.git", + "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/c073b2bd04d1c90e04dc1b787662b558dd65ade0", + "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0", + "php": ">=5.4.0" + }, + "require-dev": { + "illuminate/http": "^5.0|^6.0|^7.0|^8.0|^9.0", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Fideloper\\Proxy\\TrustedProxyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Fideloper\\Proxy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Fidao", + "email": "fideloper@gmail.com" + } + ], + "description": "Set trusted proxies for Laravel", + "keywords": [ + "load balancing", + "proxy", + "trusted proxy" + ], + "time": "2020-10-22T13:48:01+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/feb0e820b8436873675fd3aca04f3728eb2185cb", + "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "time": "2020-03-25T18:49:23+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "60d379c243457e073cff02bc323a2a86cb355631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", + "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2020-09-30T07:37:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2020-09-30T07:37:11+00:00" + }, + { + "name": "intervention/image", + "version": "2.5.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "abbf18d5ab8367f96b3205ca3c89fb2fa598c69e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/abbf18d5ab8367f96b3205ca3c89fb2fa598c69e", + "reference": "abbf18d5ab8367f96b3205ca3c89fb2fa598c69e", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "guzzlehttp/psr7": "~1.1", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.2", + "phpunit/phpunit": "^4.8 || ^5.7" + }, + "suggest": { + "ext-gd": "to use GD library based image processing.", + "ext-imagick": "to use Imagick based image processing.", + "intervention/imagecache": "Caching extension for the Intervention Image library" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ], + "aliases": { + "Image": "Intervention\\Image\\Facades\\Image" + } + } + }, + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src/Intervention/Image" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@olivervogel.com", + "homepage": "http://olivervogel.com/" + } + ], + "description": "Image handling and manipulation library with support for Laravel integration", + "homepage": "http://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "laravel", + "thumbnail", + "watermark" + ], + "time": "2019-11-02T09:15:47+00:00" + }, + { + "name": "jakub-onderka/php-console-color", + "version": "v0.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "1.0", + "jakub-onderka/php-parallel-lint": "1.0", + "jakub-onderka/php-var-dump-check": "0.*", + "phpunit/phpunit": "~4.3", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "JakubOnderka\\PhpConsoleColor\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "jakub.onderka@gmail.com" + } + ], + "abandoned": "php-parallel-lint/php-console-color", + "time": "2018-09-29T17:23:10+00:00" + }, + { + "name": "jakub-onderka/php-console-highlighter", + "version": "v0.4", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547", + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "jakub-onderka/php-console-color": "~0.2", + "php": ">=5.4.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "~1.0", + "jakub-onderka/php-parallel-lint": "~1.0", + "jakub-onderka/php-var-dump-check": "~0.1", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "JakubOnderka\\PhpConsoleHighlighter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "acci@acci.cz", + "homepage": "http://www.acci.cz/" + } + ], + "description": "Highlight PHP code in terminal", + "abandoned": "php-parallel-lint/php-console-highlighter", + "time": "2018-09-29T18:48:56+00:00" + }, + { + "name": "laminas/laminas-diactoros", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "4ff7400c1c12e404144992ef43c8b733fd9ad516" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/4ff7400c1c12e404144992ef43c8b733fd9ad516", + "reference": "4ff7400c1c12e404144992ef43c8b733fd9ad516", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "replace": { + "zendframework/zend-diactoros": "^2.2.1" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.8.0", + "laminas/laminas-coding-standard": "~1.0.0", + "php-http/psr7-integration-tests": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.1" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Laminas\\Diactoros\\ConfigProvider", + "module": "Laminas\\Diactoros" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php", + "src/functions/create_uploaded_file.legacy.php", + "src/functions/marshal_headers_from_sapi.legacy.php", + "src/functions/marshal_method_from_sapi.legacy.php", + "src/functions/marshal_protocol_version_from_sapi.legacy.php", + "src/functions/marshal_uri_from_sapi.legacy.php", + "src/functions/normalize_server.legacy.php", + "src/functions/normalize_uploaded_files.legacy.php", + "src/functions/parse_cookie_header.legacy.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "time": "2020-11-18T18:39:28+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "laravel/framework", + "version": "v6.20.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "12ed06da14e3fe4257cb1ece91c2d3439cc2d34b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/12ed06da14e3fe4257cb1ece91c2d3439cc2d34b", + "reference": "12ed06da14e3fe4257cb1ece91c2d3439cc2d34b", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.4|^2.0", + "dragonmantank/cron-expression": "^2.3.1", + "egulias/email-validator": "^2.1.10", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "league/commonmark": "^1.3", + "league/flysystem": "^1.1", + "monolog/monolog": "^1.12|^2.0", + "nesbot/carbon": "^2.31", + "opis/closure": "^3.6", + "php": "^7.2.5|^8.0", + "psr/container": "^1.0", + "psr/simple-cache": "^1.0", + "ramsey/uuid": "^3.7", + "swiftmailer/swiftmailer": "^6.0", + "symfony/console": "^4.3.4", + "symfony/debug": "^4.3.4", + "symfony/finder": "^4.3.4", + "symfony/http-foundation": "^4.3.4", + "symfony/http-kernel": "^4.3.4", + "symfony/polyfill-php73": "^1.17", + "symfony/process": "^4.3.4", + "symfony/routing": "^4.3.4", + "symfony/var-dumper": "^4.3.4", + "tijsverkoyen/css-to-inline-styles": "^2.2.1", + "vlucas/phpdotenv": "^3.3" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.155", + "doctrine/dbal": "^2.6", + "filp/whoops": "^2.8", + "guzzlehttp/guzzle": "^6.3.1|^7.0.1", + "league/flysystem-cached-adapter": "^1.0", + "mockery/mockery": "~1.3.3|^1.4.2", + "moontoast/math": "^1.1", + "orchestra/testbench-core": "^4.8", + "pda/pheanstalk": "^4.0", + "phpunit/phpunit": "^7.5.15|^8.4|^9.3.3", + "predis/predis": "^1.1.1", + "symfony/cache": "^4.3.4" + }, + "suggest": { + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.8).", + "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.3.1|^7.0.1).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", + "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", + "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", + "moontoast/math": "Required to use ordered UUIDs (^1.1).", + "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", + "predis/predis": "Required to use the predis connector (^1.1.2).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).", + "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "time": "2021-01-19T13:41:17+00:00" + }, + { + "name": "laravel/passport", + "version": "v9.4.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "011bd500e8ae3d459b692467880a49ff1ecd60c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/011bd500e8ae3d459b692467880a49ff1ecd60c0", + "reference": "011bd500e8ae3d459b692467880a49ff1ecd60c0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^5.0", + "illuminate/auth": "^6.18.31|^7.22.4", + "illuminate/console": "^6.18.31|^7.22.4", + "illuminate/container": "^6.18.31|^7.22.4", + "illuminate/contracts": "^6.18.31|^7.22.4", + "illuminate/cookie": "^6.18.31|^7.22.4", + "illuminate/database": "^6.18.31|^7.22.4", + "illuminate/encryption": "^6.18.31|^7.22.4", + "illuminate/http": "^6.18.31|^7.22.4", + "illuminate/support": "^6.18.31|^7.22.4", + "laminas/laminas-diactoros": "^2.2", + "lcobucci/jwt": "^3.4|^4.0", + "league/oauth2-server": "^8.2.3", + "nyholm/psr7": "^1.0", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^2.0", + "symfony/psr-http-message-bridge": "^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.4|^5.0", + "phpunit/phpunit": "^8.5|^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "time": "2020-12-04T09:37:12+00:00" + }, + { + "name": "laravel/tinker", + "version": "v1.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "ad571aacbac1539c30d480908f9d0c9614eaf1a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/ad571aacbac1539c30d480908f9d0c9614eaf1a7", + "reference": "ad571aacbac1539c30d480908f9d0c9614eaf1a7", + "shasum": "" + }, + "require": { + "illuminate/console": "~5.1|^6.0", + "illuminate/contracts": "~5.1|^6.0", + "illuminate/support": "~5.1|^6.0", + "php": ">=5.5.9", + "psy/psysh": "0.7.*|0.8.*|0.9.*", + "symfony/var-dumper": "~3.0|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (~5.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "time": "2019-08-07T15:10:45+00:00" + }, + { + "name": "lcobucci/clock", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3", + "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "infection/infection": "^0.17", + "lcobucci/coding-standard": "^6.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/php-code-coverage": "9.1.4", + "phpunit/phpunit": "9.3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "time": "2020-08-27T18:56:02+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "6d8665ccd924dc076a9b65d1ea8abe21d68f6958" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/6d8665ccd924dc076a9b65d1ea8abe21d68f6958", + "reference": "6d8665ccd924dc076a9b65d1ea8abe21d68f6958", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "infection/infection": "^0.20", + "lcobucci/coding-standard": "^6.0", + "mikey179/vfsstream": "^1.6", + "phpbench/phpbench": "^0.17", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/php-invoker": "^3.1", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2020-11-25T02:06:12+00:00" + }, + { + "name": "league/commonmark", + "version": "1.5.7", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/11df9b36fd4f1d2b727a73bf14931d81373b9a54", + "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "scrutinizer/ocular": "1.7.*" + }, + "require-dev": { + "cebe/markdown": "~1.0", + "commonmark/commonmark.js": "0.29.2", + "erusev/parsedown": "~1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "~1.4", + "mikehaertl/php-shellcommand": "^1.4", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", + "scrutinizer/ocular": "^1.5", + "symfony/finder": "^4.2" + }, + "bin": [ + "bin/commonmark" + ], + "type": "library", + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "time": "2020-10-31T13:49:32+00:00" + }, + { + "name": "league/event", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "time": "2018-11-26T11:52:41+00:00" + }, + { + "name": "league/flysystem", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.29", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "4e25cc0582a36a786c31115e419c6e40498f6972" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4e25cc0582a36a786c31115e419c6e40498f6972", + "reference": "4e25cc0582a36a786c31115e419c6e40498f6972", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.20.0", + "league/flysystem": "^1.0.40", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "time": "2020-10-08T18:58:37+00:00" + }, + { + "name": "league/flysystem-cached-adapter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-cached-adapter.git", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "psr/cache": "^1.0.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0", + "tedivm/stash": "~0.12" + }, + "suggest": { + "ext-phpredis": "Pure C implemented extension for PHP" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Cached\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "frankdejonge", + "email": "info@frenky.net" + } + ], + "description": "An adapter decorator to enable meta-data caching.", + "time": "2020-07-25T15:56:04+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "time": "2021-01-18T20:58:21+00:00" + }, + { + "name": "league/oauth2-server", + "version": "8.2.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "622eaa1f28eb4a2dea0cfc7e4f5280fac794e83c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/622eaa1f28eb4a2dea0cfc7e4f5280fac794e83c", + "reference": "622eaa1f28eb4a2dea0cfc7e4f5280fac794e83c", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.2.1", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/jwt": "^3.4 || ^4.0", + "league/event": "^2.2", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0.1" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^2.4.1", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^8.5.13", + "roave/security-advisories": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "time": "2020-12-10T11:35:44+00:00" + }, + { + "name": "maatwebsite/excel", + "version": "3.1.19", + "source": { + "type": "git", + "url": "https://github.com/Maatwebsite/Laravel-Excel.git", + "reference": "96527a9ebc2e79e9a5fa7eaef7e23c9e9bcc587c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Maatwebsite/Laravel-Excel/zipball/96527a9ebc2e79e9a5fa7eaef7e23c9e9bcc587c", + "reference": "96527a9ebc2e79e9a5fa7eaef7e23c9e9bcc587c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/support": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0|^7.0", + "php": "^7.0", + "phpoffice/phpspreadsheet": "^1.10" + }, + "require-dev": { + "mockery/mockery": "^1.1", + "orchestra/database": "^4.0", + "orchestra/testbench": "^4.0", + "phpunit/phpunit": "^8.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ], + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + } + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@maatwebsite.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "time": "2020-02-28T15:47:45+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6373eefe0b3274d7b702d81f2c99aa977ff97dc2", + "reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "guzzlehttp/guzzle": ">= 6.3", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": ">= 7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "time": "2019-07-17T11:01:58+00:00" + }, + { + "name": "markbaker/complex", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/c3131244e29c08d44fefb49e0dd35021e9e39dd2", + "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2", + "shasum": "" + }, + "require": { + "php": "^5.6.0|^7.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0|^5.0|^6.0|^7.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^4.8.35|^5.0|^6.0|^7.0", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "^3.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + }, + "files": [ + "classes/src/functions/abs.php", + "classes/src/functions/acos.php", + "classes/src/functions/acosh.php", + "classes/src/functions/acot.php", + "classes/src/functions/acoth.php", + "classes/src/functions/acsc.php", + "classes/src/functions/acsch.php", + "classes/src/functions/argument.php", + "classes/src/functions/asec.php", + "classes/src/functions/asech.php", + "classes/src/functions/asin.php", + "classes/src/functions/asinh.php", + "classes/src/functions/atan.php", + "classes/src/functions/atanh.php", + "classes/src/functions/conjugate.php", + "classes/src/functions/cos.php", + "classes/src/functions/cosh.php", + "classes/src/functions/cot.php", + "classes/src/functions/coth.php", + "classes/src/functions/csc.php", + "classes/src/functions/csch.php", + "classes/src/functions/exp.php", + "classes/src/functions/inverse.php", + "classes/src/functions/ln.php", + "classes/src/functions/log2.php", + "classes/src/functions/log10.php", + "classes/src/functions/negative.php", + "classes/src/functions/pow.php", + "classes/src/functions/rho.php", + "classes/src/functions/sec.php", + "classes/src/functions/sech.php", + "classes/src/functions/sin.php", + "classes/src/functions/sinh.php", + "classes/src/functions/sqrt.php", + "classes/src/functions/tan.php", + "classes/src/functions/tanh.php", + "classes/src/functions/theta.php", + "classes/src/operations/add.php", + "classes/src/operations/subtract.php", + "classes/src/operations/multiply.php", + "classes/src/operations/divideby.php", + "classes/src/operations/divideinto.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "time": "2020-08-26T19:47:57+00:00" + }, + { + "name": "markbaker/matrix", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "182d44c3b2e3b063468f7481ae3ef71c69dc1409" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/182d44c3b2e3b063468f7481ae3ef71c69dc1409", + "reference": "182d44c3b2e3b063468f7481ae3ef71c69dc1409", + "shasum": "" + }, + "require": { + "php": "^5.6.0|^7.0.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "dev-master", + "phploc/phploc": "^4", + "phpmd/phpmd": "dev-master", + "phpunit/phpunit": "^5.7|^6.0|7.0", + "sebastian/phpcpd": "^3.0", + "squizlabs/php_codesniffer": "^3.0@dev" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + }, + "files": [ + "classes/src/functions/adjoint.php", + "classes/src/functions/antidiagonal.php", + "classes/src/functions/cofactors.php", + "classes/src/functions/determinant.php", + "classes/src/functions/diagonal.php", + "classes/src/functions/identity.php", + "classes/src/functions/inverse.php", + "classes/src/functions/minors.php", + "classes/src/functions/trace.php", + "classes/src/functions/transpose.php", + "classes/src/operations/add.php", + "classes/src/operations/directsum.php", + "classes/src/operations/subtract.php", + "classes/src/operations/multiply.php", + "classes/src/operations/divideby.php", + "classes/src/operations/divideinto.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "time": "2020-08-28T19:41:55+00:00" + }, + { + "name": "mews/purifier", + "version": "3.3.3", + "source": { + "type": "git", + "url": "https://github.com/mewebstudio/Purifier.git", + "reference": "8e0b3d87c79b38b8d88aeb3c0ba8b000a393a74c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mewebstudio/Purifier/zipball/8e0b3d87c79b38b8d88aeb3c0ba8b000a393a74c", + "reference": "8e0b3d87c79b38b8d88aeb3c0ba8b000a393a74c", + "shasum": "" + }, + "require": { + "ezyang/htmlpurifier": "4.13.*", + "illuminate/config": "^5.8|^6.0|^7.0|^8.0", + "illuminate/filesystem": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "php": "^7.2|^8.0" + }, + "require-dev": { + "graham-campbell/testbench": "^3.2|^5.5.1", + "mockery/mockery": "^1.3.3", + "phpunit/phpunit": "^8.0|^9.0" + }, + "suggest": { + "laravel/framework": "To test the Laravel bindings", + "laravel/lumen-framework": "To test the Lumen bindings" + }, + "type": "package", + "extra": { + "laravel": { + "providers": [ + "Mews\\Purifier\\PurifierServiceProvider" + ], + "aliases": { + "Purifier": "Mews\\Purifier\\Facades\\Purifier" + } + } + }, + "autoload": { + "psr-4": { + "Mews\\Purifier\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muharrem ERİN", + "email": "me@mewebstudio.com", + "homepage": "https://github.com/mewebstudio", + "role": "Developer" + } + ], + "description": "Laravel 5/6/7 HtmlPurifier Package", + "homepage": "https://github.com/mewebstudio/purifier", + "keywords": [ + "Purifier", + "htmlpurifier", + "laravel5 HtmlPurifier", + "laravel5 Purifier", + "laravel5 Security", + "laravel6 HtmlPurifier", + "laravel6 Purifier", + "laravel6 Security", + "security", + "xss" + ], + "time": "2020-11-03T19:46:27+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7", + "graylog2/gelf-php": "^1.4.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpspec/prophecy": "^1.6.1", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", + "ruflin/elastica": ">=0.90 <7.0.1", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2020-12-14T13:15:25+00:00" + }, + { + "name": "morningtrain/laravel-context", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelContext.git", + "reference": "ea40884e02775f89c2b0f6a613c2aa14033a965b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelContext/zipball/ea40884e02775f89c2b0f6a613c2aa14033a965b", + "reference": "ea40884e02775f89c2b0f6a613c2aa14033a965b", + "shasum": "" + }, + "require": { + "illuminate/routing": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "php": "^7.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Context\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Bjarne Bonde", + "email": "bb@morningtain.dk", + "homepage": "https://morningtrain.dk/", + "role": "Developer" + } + ], + "description": "A context helper package for Laravel", + "homepage": "http://morningtrain.dk/", + "time": "2020-12-08T16:38:24+00:00" + }, + { + "name": "morningtrain/laravel-fields", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelFields.git", + "reference": "f77e4416014fbbe5b0c20cb3a3d3ae862149640a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelFields/zipball/f77e4416014fbbe5b0c20cb3a3d3ae862149640a", + "reference": "f77e4416014fbbe5b0c20cb3a3d3ae862149640a", + "shasum": "" + }, + "require": { + "illuminate/database": "^5.8|^6.0|^7.0|^8.0", + "illuminate/http": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "illuminate/validation": "^5.8|^6.0|^7.0|^8.0", + "morningtrain/laravel-support": "^1.0", + "php": "^7.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Fields\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "API Fields", + "time": "2020-11-11T09:38:26+00:00" + }, + { + "name": "morningtrain/laravel-fields-files", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelFieldsFiles.git", + "reference": "598fa7c48fad19af0570947012c032d15472afd0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelFieldsFiles/zipball/598fa7c48fad19af0570947012c032d15472afd0", + "reference": "598fa7c48fad19af0570947012c032d15472afd0", + "shasum": "" + }, + "require": { + "illuminate/console": "^5.8|^6.0|^7.0|^8.0", + "illuminate/database": "^5.8|^6.0|^7.0|^8.0", + "illuminate/http": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "morningtrain/laravel-fields": "*", + "morningtrain/laravel-resources": "*", + "php": "^7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MorningTrain\\Laravel\\Fields\\Files\\ServiceProvider" + ], + "aliases": [] + } + }, + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Fields\\Files\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "Files field for Laravel using Filepond", + "time": "2020-09-09T17:19:55+00:00" + }, + { + "name": "morningtrain/laravel-filters", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelFilters.git", + "reference": "1b40c2f791867f71f966fdf5bd411565a29741a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelFilters/zipball/1b40c2f791867f71f966fdf5bd411565a29741a0", + "reference": "1b40c2f791867f71f966fdf5bd411565a29741a0", + "shasum": "" + }, + "require": { + "illuminate/database": "^5.8|^6.0|^7.0|^8.0", + "illuminate/http": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "morningtrain/laravel-support": "^1.0", + "php": "^7.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Filters\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "Query filters for Laravel", + "time": "2020-11-02T12:35:33+00:00" + }, + { + "name": "morningtrain/laravel-https", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/laravel-https.git", + "reference": "4f780cefb94434e366bac25189eae500af007bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/laravel-https/zipball/4f780cefb94434e366bac25189eae500af007bf2", + "reference": "4f780cefb94434e366bac25189eae500af007bf2", + "shasum": "" + }, + "require": { + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "php": "^7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MorningTrain\\Laravel\\Https\\LaravelHttpsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Https\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "Provides some initial https setup", + "time": "2020-09-09T17:22:23+00:00" + }, + { + "name": "morningtrain/laravel-permissions", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelPermissions.git", + "reference": "a8b5149a76887be2532fbfec7c1979b70cc90605" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelPermissions/zipball/a8b5149a76887be2532fbfec7c1979b70cc90605", + "reference": "a8b5149a76887be2532fbfec7c1979b70cc90605", + "shasum": "" + }, + "require": { + "illuminate/console": "^5.8|^6.0|^7.0", + "illuminate/contracts": "^5.8|^6.0|^7.0", + "illuminate/database": "^5.8|^6.0|^7.0", + "illuminate/filesystem": "^5.8|^6.0|^7.0", + "illuminate/support": "^5.8|^6.0|^7.0", + "morningtrain/laravel-context": "^2.0", + "morningtrain/laravel-resources": "^2.0", + "php": "^7.2", + "spatie/laravel-permission": "^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MorningTrain\\Laravel\\Permissions\\LaravelPermissionsServiceProvider", + "MorningTrain\\Laravel\\Permissions\\PermissionServiceProvider" + ], + "aliases": { + "PermissionsService": "MorningTrain\\Laravel\\Permissions\\Permissions", + "PermissionPolicy": "MorningTrain\\Laravel\\Permissions\\Policies\\PermissionPolicy" + }, + "dont-discover": [ + "spatie/laravel-permission" + ] + } + }, + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Permissions\\": "src/", + "MorningTrain\\Laravel\\Permissions\\Database\\Seeds\\": "database/seeds/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "Permissions setup and config", + "time": "2020-03-04T07:41:19+00:00" + }, + { + "name": "morningtrain/laravel-react", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelReact.git", + "reference": "899e96a0f0fa350698a6b76b523ae996a19a61b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelReact/zipball/899e96a0f0fa350698a6b76b523ae996a19a61b9", + "reference": "899e96a0f0fa350698a6b76b523ae996a19a61b9", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3", + "illuminate/console": "^5.8|^6.0|^7.0", + "illuminate/filesystem": "^5.8|^6.0|^7.0", + "illuminate/support": "^5.8|^6.0|^7.0", + "morningtrain/laravel-support": "^1.0", + "nesbot/carbon": "^2.16", + "php": "^7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MorningTrain\\Laravel\\React\\ReactServiceProvider" + ], + "aliases": { + "React": "MorningTrain\\Laravel\\React\\React" + } + } + }, + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\React\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Bjarne Bonde", + "email": "bb@morningtain.dk", + "homepage": "https://morningtrain.dk/", + "role": "Developer" + } + ], + "description": "A context helper package for Laravel", + "homepage": "http://morningtrain.dk/", + "time": "2020-03-04T07:44:55+00:00" + }, + { + "name": "morningtrain/laravel-resources", + "version": "2.11.1", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelResources.git", + "reference": "da8d7305954e443323fe70a8ff0d341ab140d626" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelResources/zipball/da8d7305954e443323fe70a8ff0d341ab140d626", + "reference": "da8d7305954e443323fe70a8ff0d341ab140d626", + "shasum": "" + }, + "require": { + "illuminate/auth": "^5.8|^6.0|^7.0", + "illuminate/console": "^5.8|^6.0|^7.0", + "illuminate/contracts": "^5.8|^6.0|^7.0", + "illuminate/database": "^5.8|^6.0|^7.0", + "illuminate/http": "^5.8|^6.0|^7.0", + "illuminate/routing": "^5.8|^6.0|^7.0", + "illuminate/support": "^5.8|^6.0|^7.0", + "morningtrain/laravel-context": "^2.0", + "morningtrain/laravel-fields": "^1.0", + "morningtrain/laravel-filters": "^1.0", + "morningtrain/laravel-support": "^1.0", + "php": "^7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MorningTrain\\Laravel\\Resources\\LaravelResourcesServiceProvider" + ], + "aliases": { + "ResourceRepository": "MorningTrain\\Laravel\\Resources\\ResourceRepository" + } + } + }, + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Resources\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Morningtrain", + "email": "mail@morningtrain.dk", + "homepage": "https://morningtrain.dk/" + } + ], + "description": "Resources system for Laravel", + "time": "2020-04-15T12:58:56+00:00" + }, + { + "name": "morningtrain/laravel-support", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Morning-Train/LaravelSupport.git", + "reference": "fd58d91be4345b62ee855ed8d943282d52818cd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Morning-Train/LaravelSupport/zipball/fd58d91be4345b62ee855ed8d943282d52818cd5", + "reference": "fd58d91be4345b62ee855ed8d943282d52818cd5", + "shasum": "" + }, + "require": { + "illuminate/database": "^5.8|^6.0|^7.0|^8.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0", + "php": "^7.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MorningTrain\\Laravel\\Support\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Bjarne Bonde", + "email": "bb@morningtain.dk", + "homepage": "https://morningtrain.dk/", + "role": "Developer" + } + ], + "description": "A context helper package for Laravel", + "homepage": "http://morningtrain.dk/", + "time": "2020-11-11T09:18:16+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2020-07-31T21:01:56+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.7.7", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "d178027d1e679832db9f38248fcc7200647dc2b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/d178027d1e679832db9f38248fcc7200647dc2b7", + "reference": "d178027d1e679832db9f38248fcc7200647dc2b7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "time": "2020-11-14T18:14:52+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.43.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "d32c57d8389113742f4a88725a170236470012e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d32c57d8389113742f4a88725a170236470012e2", + "reference": "d32c57d8389113742f4a88725a170236470012e2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^3.4 || ^4.0 || ^5.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", + "kylekatarnls/multi-tester": "^2.0", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.54", + "phpunit/phpunit": "^7.5 || ^8.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-3.x": "3.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "kylekatarnls", + "homepage": "http://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2020-12-17T20:55:32+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.10.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2020-12-20T10:01:03+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a272953743c454ac4af9626634daaf5ab3ce1173" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a272953743c454ac4af9626634daaf5ab3ce1173", + "reference": "a272953743c454ac4af9626634daaf5ab3ce1173", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.8", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "time": "2020-11-14T17:35:34+00:00" + }, + { + "name": "opis/closure", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5", + "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Closure\\": "src/" + }, + "files": [ + "functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "time": "2020-11-07T02:01:34+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-02T15:55:56+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3", + "reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "time": "2020-12-03T16:26:19+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "f79611d6dc1f6b7e8e30b738fc371b392001dbfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/f79611d6dc1f6b7e8e30b738fc371b392001dbfd", + "reference": "f79611d6dc1f6b7e8e30b738fc371b392001dbfd", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "markbaker/complex": "^1.4", + "markbaker/matrix": "^1.2", + "php": "^7.1", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "dompdf/dompdf": "^0.8.3", + "friendsofphp/php-cs-fixer": "^2.16", + "jpgraph/jpgraph": "^4.0", + "mpdf/mpdf": "^8.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.5", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.3" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "time": "2020-04-27T08:12:48+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2020-07-20T17:29:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.30", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/136b9ca7eebef78be14abf90d65c5e57b6bc5d36", + "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "time": "2020-12-17T05:42:04+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.9.12", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "90da7f37568aee36b116a030c5f99c915267edd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/90da7f37568aee36b116a030c5f99c915267edd4", + "reference": "90da7f37568aee36b116a030c5f99c915267edd4", + "shasum": "" + }, + "require": { + "dnoegel/php-xdg-base-dir": "0.1.*", + "ext-json": "*", + "ext-tokenizer": "*", + "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*", + "nikic/php-parser": "~1.3|~2.0|~3.0|~4.0", + "php": ">=5.4.0", + "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0|~5.0", + "symfony/var-dumper": "~2.7|~3.0|~4.0|~5.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "hoa/console": "~2.15|~3.16", + "phpunit/phpunit": "~4.8.35|~5.0|~6.0|~7.0" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", + "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.", + "hoa/console": "A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.9.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "time": "2019-12-06T14:19:43+00:00" + }, + { + "name": "pusher/pusher-php-server", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "c010de563b18c9aa7dd6cd4468ea539a838c263c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/c010de563b18c9aa7dd6cd4468ea539a838c263c", + "reference": "c010de563b18c9aa7dd6cd4468ea539a838c263c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "paragonie/sodium_compat": "^1.6", + "php": "^7.1", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "time": "2020-04-01T14:30:45+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.9.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", + "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", + "shasum": "" + }, + "require": { + "ext-json": "*", + "paragonie/random_compat": "^1 | ^2 | 9.99.99", + "php": "^5.4 | ^7 | ^8", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1 | ^2", + "doctrine/annotations": "^1.2", + "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", + "jakub-onderka/php-parallel-lint": "^1", + "mockery/mockery": "^0.9.11 | ^1", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock-phpunit": "^0.3 | ^1.1", + "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + }, + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2020-02-21T04:36:14+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "3.18.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "1c51a5fa12131565fe3860705163e53d7a26258a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/1c51a5fa12131565fe3860705163e53d7a26258a", + "reference": "1c51a5fa12131565fe3860705163e53d7a26258a", + "shasum": "" + }, + "require": { + "illuminate/auth": "^5.8|^6.0|^7.0|^8.0", + "illuminate/container": "^5.8|^6.0|^7.0|^8.0", + "illuminate/contracts": "^5.8|^6.0|^7.0|^8.0", + "illuminate/database": "^5.8|^6.0|^7.0|^8.0", + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^3.8|^4.0|^5.0|^6.0", + "phpunit/phpunit": "^8.0|^9.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Permission\\": "src" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 5.8 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "time": "2020-11-09T14:08:36+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.5", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/698a6a9f54d7eb321274de3ad19863802c879fb7", + "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.0", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "symfony/phpunit-bridge": "^4.4|^5.0" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "https://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2021-01-12T09:35:59+00:00" + }, + { + "name": "symfony/console", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "12e071278e396cc3e1c149857337e9e192deca0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b", + "reference": "12e071278e396cc3e1c149857337e9e192deca0b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<4.3|>=5", + "symfony/lock": "<4.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.3", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/var-dumper": "^4.3|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2020-12-18T07:41:31+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "f789e7ead4c79e04ca9a6d6162fc629c89bd8054" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f789e7ead4c79e04ca9a6d6162fc629c89bd8054", + "reference": "f789e7ead4c79e04ca9a6d6162fc629c89bd8054", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2020-12-08T17:02:38+00:00" + }, + { + "name": "symfony/debug", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544", + "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2020-12-10T16:34:26+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3", + "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/debug": "^4.4.5", + "symfony/polyfill-php80": "^1.15", + "symfony/var-dumper": "^4.4|^5.0" + }, + "require-dev": { + "symfony/http-kernel": "^4.4|^5.0", + "symfony/serializer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ErrorHandler Component", + "homepage": "https://symfony.com", + "time": "2020-12-09T11:15:38+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5d4c874b0eb1c32d40328a09dbc37307a5a910b0", + "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher-contracts": "^1.1" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "~3.4|~4.4", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2020-12-18T07:41:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.1.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2020-07-06T13:19:58+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b", + "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2020-12-08T16:59:59+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-version": "2.3", + "branch-alias": { + "dev-main": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2020-10-14T17:08:19+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5ebda66b51612516bf338d5f87da2f37ff74cf34", + "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/mime": "^4.3|^5.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/expression-language": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2020-12-18T07:41:31+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/eaff9a43e74513508867ecfa66ef94fbb96ab128", + "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/error-handler": "^4.4", + "symfony/event-dispatcher": "^4.4", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/browser-kit": "<4.3", + "symfony/config": "<3.4", + "symfony/console": ">=5", + "symfony/dependency-injection": "<4.3", + "symfony/translation": "<4.2", + "twig/twig": "<1.34|<2.4,>=2" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/cache": "~1.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpKernel Component", + "homepage": "https://symfony.com", + "time": "2020-12-18T13:32:33+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "de97005aef7426ba008c46ba840fc301df577ada" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/de97005aef7426ba008c46ba840fc301df577ada", + "reference": "de97005aef7426ba008c46ba840fc301df577ada", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/property-access": "^4.4|^5.1", + "symfony/property-info": "^4.4|^5.1", + "symfony/serializer": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A library to manipulate MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "time": "2020-12-09T18:54:12+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6", + "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T17:09:11+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/process", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "075316ff72233ce3d04a9743414292e834f2cb4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/075316ff72233ce3d04a9743414292e834f2cb4a", + "reference": "075316ff72233ce3d04a9743414292e834f2cb4a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2020-12-08T16:59:59+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "51a21cb3ba3927d4b4bf8f25cc55763351af5f2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/51a21cb3ba3927d4b4bf8f25cc55763351af5f2e", + "reference": "51a21cb3ba3927d4b4bf8f25cc55763351af5f2e", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^4.4 || ^5.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "symfony/phpunit-bridge": "^4.4 || ^5.0" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "time": "2020-09-29T08:17:46+00:00" + }, + { + "name": "symfony/routing", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "80b042c20b035818daec844723e23b9825134ba0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/80b042c20b035818daec844723e23b9825134ba0", + "reference": "80b042c20b035818daec844723e23b9825134ba0", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "conflict": { + "symfony/config": "<4.2", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "~1.2", + "psr/log": "~1.0", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2020-12-08T16:59:59+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/translation", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "c1001b7d75b3136648f94b245588209d881c6939" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/c1001b7d75b3136648f94b245588209d881c6939", + "reference": "c1001b7d75b3136648f94b245588209d881c6939", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1.6|^2" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/http-kernel": "<4.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "symfony/translation-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/finder": "~2.8|~3.0|~4.0|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/intl": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1.2|^2", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2020-12-08T16:59:59+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105", + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2020-09-28T13:05:58+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4f31364bbc8177f2a6dbc125ac3851634ebe2a03", + "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php72": "~1.5", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/console": "<3.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/process": "^4.4|^5.0", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "time": "2020-12-08T16:59:59+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "b43b05cf43c1b6d849478965062b6ef73e223bb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/b43b05cf43c1b6d849478965062b6ef73e223bb5", + "reference": "b43b05cf43c1b6d849478965062b6ef73e223bb5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0 || ^8.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "time": "2020-07-13T06:12:54+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v3.6.7", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "2065beda6cbe75e2603686907b2e45f6f3a5ad82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2065beda6cbe75e2603686907b2e45f6f3a5ad82", + "reference": "2065beda6cbe75e2603686907b2e45f6f3a5ad82", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "phpoption/phpoption": "^1.5.2", + "symfony/polyfill-ctype": "^1.17" + }, + "require-dev": { + "ext-filter": "*", + "ext-pcre": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator.", + "ext-pcre": "Required to use most of the library." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "homepage": "https://gjcampbell.co.uk/" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://vancelucas.com/" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2020-07-14T19:04:52+00:00" + } + ], + "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "cae0a8d1cb89b0f0522f65e60465e16d738e069b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/cae0a8d1cb89b0f0522f65e60465e16d738e069b", + "reference": "cae0a8d1cb89b0f0522f65e60465e16d738e069b", + "shasum": "" + }, + "require": { + "illuminate/routing": "^6|^7|^8", + "illuminate/session": "^6|^7|^8", + "illuminate/support": "^6|^7|^8", + "maximebf/debugbar": "^1.16.3", + "php": ">=7.2", + "symfony/debug": "^4.3|^5", + "symfony/finder": "^4.3|^5" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^4|^5|^6", + "phpunit/phpunit": "^8.5|^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facade" + } + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2021-01-06T14:21:44+00:00" + }, + { + "name": "beyondcode/laravel-dump-server", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/beyondcode/laravel-dump-server.git", + "reference": "fcc88fa66895f8c1ff83f6145a5eff5fa2a0739a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beyondcode/laravel-dump-server/zipball/fcc88fa66895f8c1ff83f6145a5eff5fa2a0739a", + "reference": "fcc88fa66895f8c1ff83f6145a5eff5fa2a0739a", + "shasum": "" + }, + "require": { + "illuminate/console": "5.6.*|5.7.*|5.8.*|^6.0", + "illuminate/http": "5.6.*|5.7.*|5.8.*|^6.0", + "illuminate/support": "5.6.*|5.7.*|5.8.*|^6.0", + "php": "^7.1", + "symfony/var-dumper": "^4.1.1" + }, + "require-dev": { + "larapack/dd": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "BeyondCode\\DumpServer\\DumpServerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "BeyondCode\\DumpServer\\": "src" + }, + "files": [ + "helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcel Pociot", + "role": "Developer", + "email": "marcel@beyondco.de", + "homepage": "https://beyondco.de" + } + ], + "description": "Symfony Var-Dump Server for Laravel", + "homepage": "https://github.com/beyondcode/laravel-dump-server", + "keywords": [ + "beyondcode", + "laravel-dump-server" + ], + "time": "2019-08-11T13:17:40+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "filp/whoops", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771", + "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "psr/log": "^1.0.1" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "time": "2020-11-01T12:00:00+00:00" + }, + { + "name": "fzaninotto/faker", + "version": "v1.9.2", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e", + "reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7", + "squizlabs/php_codesniffer": "^2.9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "abandoned": true, + "time": "2020-12-11T09:56:16+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "time": "2020-07-09T08:09:16+00:00" + }, + { + "name": "maximebf/debugbar", + "version": "v1.16.4", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/c86c717e4bf3c6d98422da5c38bfa7b0f494b04c", + "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c", + "shasum": "" + }, + "require": { + "php": "^7.1|^8", + "psr/log": "^1.0", + "symfony/var-dumper": "^2.6|^3|^4|^5" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.20 || ^9.4.2" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.16-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "time": "2020-12-07T10:48:48+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "60fa2f67f6e4d3634bb4a45ff3171fa52215800d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/60fa2f67f6e4d3634bb4a45ff3171fa52215800d", + "reference": "60fa2f67f6e4d3634bb4a45ff3171fa52215800d", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.10|^6.5|^7.5|^8.5|^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mockery": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "http://davedevelopment.co.uk" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "time": "2020-08-11T18:10:21+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/88b58b5bd9bdcc54756480fb3ce87234696544ee", + "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.1.4", + "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*", + "php": "^7.1 || ^8.0", + "symfony/console": "~2.8|~3.3|~4.0" + }, + "require-dev": { + "laravel/framework": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "time": "2020-10-29T16:05:21+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.12.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "245710e971a030f42e08f4912863805570f23d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", + "reference": "245710e971a030f42e08f4912863805570f23d39", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-12-19T10:15:11+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2020-11-30T08:25:21+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2020-11-30T08:20:02+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "abandoned": true, + "time": "2020-11-30T08:38:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2020-01-08T08:45:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2020-11-30T08:15:22+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2020-11-30T08:04:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2020-11-30T07:59:04+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2020-11-30T07:53:42+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2020-11-30T07:47:53+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2020-11-30T07:40:27+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2020-11-30T07:37:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2020-11-30T07:34:24+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2020-11-30T07:30:19+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^7.2" + }, + "platform-dev": [] +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..3126f1a --- /dev/null +++ b/config/app.php @@ -0,0 +1,237 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL', null), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'da', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'da_DK', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + + /* + * Package Service Providers... + */ + Bugsnag\BugsnagLaravel\BugsnagServiceProvider::class, + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + App\Providers\ContextServiceProvider::class, + App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => Illuminate\Support\Facades\App::class, + 'Arr' => Illuminate\Support\Arr::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Str' => Illuminate\Support\Str::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + + /* + * Package Aliases... + */ + 'Context' => MorningTrain\Laravel\Context\Context::class, + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..d050ad9 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,121 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + 'api' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => \App\Models\User\User::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_resets', + 'expire' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Config for shutdown functionality + |-------------------------------------------------------------------------- + | + | + | + */ + + 'shutdown' => [ + 'to' => env('SHUTDOWN_TO', 390), // 21:30 + 'from' => env('SHUTDOWN_FROM', 1290), // 6:30 + 'permissions' => [ + 'api.forum.*', + ], + 'roles' => [\App\Support\Enums\UserRoles::PIRATE, \App\Support\Enums\UserRoles::LANDLUBBER] + ] + + +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..3ca45ea --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,59 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'encrypted' => true, + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..30f0cae --- /dev/null +++ b/config/cache.php @@ -0,0 +1,102 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'cache', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..1f96d0d --- /dev/null +++ b/config/database.php @@ -0,0 +1,141 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'predis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'predis'), + 'prefix' => Str::slug(env('APP_NAME', 'laravel'), '_').'_database', + ], + + 'default' => [ + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => env('REDIS_DB', 0), + ], + + 'cache' => [ + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => env('REDIS_CACHE_DB', 1), + ], + + ], + +]; diff --git a/config/filepond.php b/config/filepond.php new file mode 100644 index 0000000..9dfd0f0 --- /dev/null +++ b/config/filepond.php @@ -0,0 +1,35 @@ + sys_get_temp_dir(), + + /* + |-------------------------------------------------------------------------- + | Disk + |-------------------------------------------------------------------------- + | + | Here you may specify the filesystem disk that should be used. + | + */ + 'disk' => 'uploads', + + /* + |-------------------------------------------------------------------------- + | Location + |-------------------------------------------------------------------------- + | + | Here you may specify the location used for storage. + | + */ + 'location' => 'filepond', + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..6b4e20c --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,81 @@ + env('FILESYSTEM_DRIVER', 'local'), + + /* + |-------------------------------------------------------------------------- + | Default Cloud Filesystem Disk + |-------------------------------------------------------------------------- + | + | Many applications store files both locally and in the cloud. For this + | reason, you may specify a default "cloud" driver here. This driver + | will be bound as the Cloud disk implementation in the container. + | + */ + + 'cloud' => env('FILESYSTEM_CLOUD', 's3'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'app' => [ + 'driver' => 'local', + 'root' => app_path(), + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + ], + + 'uploads' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL') . '/storage', + 'visibility' => 'public', + ], + + ], + +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..8425770 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/https.php b/config/https.php new file mode 100644 index 0000000..457d0f4 --- /dev/null +++ b/config/https.php @@ -0,0 +1,18 @@ + env('USE_SSL', true), + + 'redirect_to_https' => env('REDIRECT_TO_HTTPS', env('USE_SSL', true)), + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..25adf9d --- /dev/null +++ b/config/logging.php @@ -0,0 +1,98 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['daily', 'bugsnag'], + 'ignore_exceptions' => false, + ], + + 'bugsnag' => [ + 'driver' => 'bugsnag', + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => 'critical', + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => 'debug', + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => 'debug', + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => 'debug', + ], + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..48b9ecc --- /dev/null +++ b/config/mail.php @@ -0,0 +1,150 @@ + env('MAIL_DRIVER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Address + |-------------------------------------------------------------------------- + | + | Here you may provide the host address of the SMTP server used by your + | applications. A default option is provided that is compatible with + | the Mailgun mail service which will provide reliable deliveries. + | + */ + + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Port + |-------------------------------------------------------------------------- + | + | This is the SMTP port used by your application to deliver e-mails to + | users of the application. Like the host we have set this value to + | stay compatible with the Mailgun e-mail application by default. + | + */ + + 'port' => env('MAIL_PORT', 587), + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Global "To" Address + |-------------------------------------------------------------------------- + | + | Administrative emails (such as ContactSubmissions are sent to this email + | + */ + + 'default_to' => [ + 'email' => env('MAIL_TO_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_TO_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | E-Mail Encryption Protocol + |-------------------------------------------------------------------------- + | + | Here you may specify the encryption protocol that should be used when + | the application send e-mail messages. A sensible default using the + | transport layer security protocol should provide great security. + | + */ + + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + + /* + |-------------------------------------------------------------------------- + | SMTP Server Username + |-------------------------------------------------------------------------- + | + | If your SMTP server requires a username for authentication, you should + | set it here. This will get used to authenticate with your server on + | connection. You may also set the "password" value below this one. + | + */ + + 'username' => env('MAIL_USERNAME'), + + 'password' => env('MAIL_PASSWORD'), + + /* + |-------------------------------------------------------------------------- + | Sendmail System Path + |-------------------------------------------------------------------------- + | + | When using the "sendmail" driver to send e-mails, we will need to know + | the path to where Sendmail lives on this server. A default path has + | been provided here, which will work well on most of your systems. + | + */ + + 'sendmail' => '/usr/sbin/sendmail -bs', + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Log Channel + |-------------------------------------------------------------------------- + | + | If you are using the "log" driver, you may specify the logging channel + | if you prefer to keep mail messages separate from other log entries + | for simpler reading. Otherwise, the default channel will be used. + | + */ + + 'log_channel' => env('MAIL_LOG_CHANNEL'), + +]; diff --git a/config/permissions.php b/config/permissions.php new file mode 100644 index 0000000..66d4d4f --- /dev/null +++ b/config/permissions.php @@ -0,0 +1,214 @@ + [ + UserRoles::ADMIN, + UserRoles::LANDLUBBER, + UserRoles::PIRATE, + UserRoles::MENTOR, + UserRoles::MODERATOR, + ], + + /* + |-------------------------------------------------------------------------- + | Moderation Weight + |-------------------------------------------------------------------------- + | + | Each roles moderating "weight" (how important is a report) + | + */ + + 'moderation' => [ + 'suspension_time' => env('SUSPENSION_TIME', 12), // HOURS + 'threshold' => env('MODERATION_THRESHOLD', 5), + 'weight' => [ + UserRoles::ADMIN => env('ADMIN_WEIGHT', 5), + UserRoles::MODERATOR => env('MODERATOR_WEIGHT', 5), + UserRoles::MENTOR => env('MENTOR_WEIGHT', 3), + UserRoles::PIRATE => env('PIRATE_WEIGHT', 1), + UserRoles::LANDLUBBER => env('PIRATE_WEIGHT', 0), + ], + ], + + + /* + |-------------------------------------------------------------------------- + | Super Admin + |-------------------------------------------------------------------------- + | + | Super admins automatically get complete access. + | If you don't want any roles to have this kind of power, + | Just leave the array empty. + | + | Keep in mind a super_admin doesn't need any permissions, + | So they don't need to be included in permission_roles. + | + */ + + 'super_admin' => [ + //UserRoles::ADMIN, + ], + + + /* + |-------------------------------------------------------------------------- + | Permission roles + |-------------------------------------------------------------------------- + | + | List all roles you want seeded for each permission here. + | + */ + + 'permission_roles' => [ + 'api' => [ + 'user' => [ + 'index' => [], + 'read' => UserRoles::everybody(), + 'store' => UserRoles::everybody(), + 'delete' => UserRoles::everybody(), + 'edit_notifications' => UserRoles::everybody(), + 'flag' => UserRoles::everybody(), + 'accept_pirate_vows' => [UserRoles::LANDLUBBER], + ], + 'user_avatar' => [ + 'index' => [], + 'read' => UserRoles::everybody(), + 'store' => UserRoles::everybody(), + 'delete' => [], + ], + 'notification' => [ + 'index' => UserRoles::everybody(), + 'read' => [], + 'store' => [], + 'delete' => [], + 'markAsRead' => UserRoles::everybody(), + 'count' => UserRoles::everybody(), + ], + 'rewards' => [ + 'user_reward' => [ + 'index' => UserRoles::everybody(), + 'open' => UserRoles::everybody(), + ], + 'badge' => [ + ], + 'user_title' => [ + 'select' => UserRoles::everybody(), + ], + ], + 'projects' => [ + 'project' => [ + 'create' => UserRoles::active(), + 'store' => UserRoles::active(), + 'delete' => UserRoles::active(), + 'like' => UserRoles::active(), + 'flag' => UserRoles::active(), + 'endorse' => UserRoles::facilitators(), + 'resolve_invite' => UserRoles::everybody(), + ], + ], + 'courses' => [ + 'courses' => [ + 'make_progress' => UserRoles::everybody(), + ], + ], + 'forum' => [ + 'thread' => [ + 'create' => UserRoles::active(), + 'send' => UserRoles::active(), + 'toggle_mute' => UserRoles::active(), + 'toggle_sticky' => UserRoles::admins(), + 'flag' => UserRoles::active(), + 'request_removal' => UserRoles::active(), + ], + 'message' => [ + 'store' => UserRoles::everybody(), + 'delete' => UserRoles::everybody(), + 'accept' => UserRoles::active(), + 'flag' => UserRoles::active(), + 'like' => UserRoles::everybody(), + 'endorse' => UserRoles::facilitators(), + 'request_removal' => UserRoles::active(), + ], + ], + 'filepond' => [ + 'process' => UserRoles::everybody(), + 'revert' => UserRoles::everybody(), + 'load' => UserRoles::everybody(), + ], + + 'backend' => [ + 'gamification' => UserRoles::admins(), + 'users' => [ + 'backend_user' => UserRoles::admins(), + 'user' => UserRoles::facilitators(), + 'contact' => UserRoles::admins(), + ], + 'regions' => UserRoles::admins(), + 'contact' => UserRoles::admins(), + 'moderation' => UserRoles::moderators(), + 'projects' => UserRoles::moderators(), + 'content' => UserRoles::admins(), + 'forum' => UserRoles::admins(), + 'courses' => UserRoles::admins(), + + ], + ], + + 'app' => [ + 'projects' => [ + 'download' => UserRoles::everybody(), + ] + ], + + 'backend' => [ + 'home' => UserRoles::backend(), + 'users' => [ + 'pirates' => [ + 'index' => UserRoles::facilitators(), + 'create' => UserRoles::admins(), + 'download' => UserRoles::admins(), + 'edit' => UserRoles::facilitators(), + ], + 'backend' => UserRoles::admins(), + ], + 'contact' => UserRoles::admins(), + 'moderation' => UserRoles::moderators(), + 'projects' => UserRoles::moderators(), + 'content' => UserRoles::admins(), + 'forum' => UserRoles::admins(), + 'courses' => UserRoles::admins(), + 'gamification' => UserRoles::admins(), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Custom permission roles + |-------------------------------------------------------------------------- + | + | Here you can define some custom permissions, + | Which don't need to be based on a Resource. + | List all roles you want seeded for each custom permission here. + | + */ + + 'custom_permission_roles' => [ + CustomPermissions::MANAGE_PROJECT_MEMBERS => UserRoles::everybody(), + CustomPermissions::UPGRADE_TO_PIRATE => [UserRoles::LANDLUBBER], + ], + +]; diff --git a/config/purifier.php b/config/purifier.php new file mode 100644 index 0000000..9ad26d6 --- /dev/null +++ b/config/purifier.php @@ -0,0 +1,133 @@ +set('Core.Encoding', $this->config->get('purifier.encoding')); + * $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath')); + * if ( ! $this->config->get('purifier.finalize')) { + * $config->autoFinalize = false; + * } + * $config->loadArray($this->getConfig()); + * + * You must NOT delete the default settings + * anything in settings should be compacted with params that needed to instance HTMLPurifier_Config. + * + * @link http://htmlpurifier.org/live/configdoc/plain.html + */ + +return [ + 'encoding' => 'UTF-8', + 'finalize' => true, + 'cachePath' => storage_path('app/purifier'), + 'cacheFileMode' => 0755, + 'settings' => [ + 'default' => [ + 'HTML.Doctype' => 'HTML 4.01 Transitional', + 'HTML.Allowed' => 'div,b,strong[style],i,em[style],u[style],s[style],blockquote,pre[class],a[href|title],ul,ol,li,p[style|class],h1[class],h2[class],h3[class],h4[class],h5[class],h6[class],br,sub,sup,span[class|style|data-name|data-id|data-src|data-value|data-index|data-denotation-char|contenteditable],img[width|height|alt|src]', + 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align', + 'AutoFormat.AutoParagraph' => true, + 'AutoFormat.RemoveEmpty' => true, + ], + 'admin' => [ + 'HTML.Doctype' => 'HTML 4.01 Transitional', + 'HTML.Allowed' => 'div,b,strong[style],i,em[style],u[style],s[style],blockquote,pre[class],a[href|title],ul,ol,li,p[style|class],h1[class],h2[class],h3[class],h4[class],h5[class],h6[class],br,sub,sup,span[class|style|data-name|data-id|data-src|data-value|data-index|data-denotation-char|contenteditable],img[width|height|alt|src],iframe[src|class|frameborder|allowfullscreen]', + 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align', + 'AutoFormat.AutoParagraph' => true, + 'AutoFormat.RemoveEmpty' => true, + 'URI.AllowedSchemes' => [ + 'data' => true, // Allows embeded images + //Default values + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, + 'tel' => true, + ], + // Allows Youtube videos + "HTML.SafeIframe" => 'true', + "URI.SafeIframeRegexp" => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/)%", + ], + 'test' => [ + 'Attr.EnableID' => 'true', + ], + "youtube" => [ + "HTML.SafeIframe" => 'true', + "URI.SafeIframeRegexp" => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/)%", + ], + 'custom_definition' => [ + 'id' => 'html5-definitions', + 'rev' => 1, + 'debug' => false, + 'elements' => [ + // http://developers.whatwg.org/sections.html + ['section', 'Block', 'Flow', 'Common'], + ['nav', 'Block', 'Flow', 'Common'], + ['article', 'Block', 'Flow', 'Common'], + ['aside', 'Block', 'Flow', 'Common'], + ['header', 'Block', 'Flow', 'Common'], + ['footer', 'Block', 'Flow', 'Common'], + + // Content model actually excludes several tags, not modelled here + ['address', 'Block', 'Flow', 'Common'], + ['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'], + + // http://developers.whatwg.org/grouping-content.html + ['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'], + ['figcaption', 'Inline', 'Flow', 'Common'], + + // http://developers.whatwg.org/the-video-element.html#the-video-element + ['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [ + 'src' => 'URI', + 'type' => 'Text', + 'width' => 'Length', + 'height' => 'Length', + 'poster' => 'URI', + 'preload' => 'Enum#auto,metadata,none', + 'controls' => 'Bool', + ]], + ['source', 'Block', 'Flow', 'Common', [ + 'src' => 'URI', + 'type' => 'Text', + ]], + + // http://developers.whatwg.org/text-level-semantics.html + ['s', 'Inline', 'Inline', 'Common'], + ['var', 'Inline', 'Inline', 'Common'], + ['sub', 'Inline', 'Inline', 'Common'], + ['sup', 'Inline', 'Inline', 'Common'], + ['mark', 'Inline', 'Inline', 'Common'], + ['wbr', 'Inline', 'Empty', 'Core'], + + // http://developers.whatwg.org/edits.html + ['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']], + ['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']], + ], + 'attributes' => [ + ['iframe', 'allowfullscreen', 'Bool'], + ['span', 'contenteditable', 'Enum#false'], + ['table', 'height', 'Text'], + ['td', 'border', 'Text'], + ['th', 'border', 'Text'], + ['tr', 'width', 'Text'], + ['tr', 'height', 'Text'], + ['tr', 'border', 'Text'], + ], + ], + 'custom_attributes' => [ + ['a', 'target', 'Enum#_blank,_self,_target,_top'], + ['span', 'data-name', 'Text'], + ['span', 'data-id', 'Text'], + ['span', 'data-src', 'Text'], + ['span', 'data-index', 'Text'], + ['span', 'data-value', 'Text'], + ['span', 'data-denotation-char', 'Text'], + ], + 'custom_elements' => [ + ['u', 'Inline', 'Inline', 'Common'], + ], + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..07c7d2a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,87 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/resources.php b/config/resources.php new file mode 100644 index 0000000..20e2c6a --- /dev/null +++ b/config/resources.php @@ -0,0 +1,139 @@ + [ + \App\Resources\Auth\Login::class, + \App\Resources\Auth\Register::class, + \App\Resources\Auth\Password::class, + ], + + 'api' => [ + \App\Resources\Api\User::class, + \App\Resources\Api\Contact::class, + \App\Resources\Api\AvatarItem::class, + \App\Resources\Api\UserAvatar::class, + \App\Resources\Api\News::class, + \App\Resources\Api\Event::class, + \App\Resources\Api\Notification::class, + \MorningTrain\Laravel\Fields\Files\Resources\Filepond::class, + 'courses' => [ + \App\Resources\Api\Courses\Courses::class, + \App\Resources\Api\Courses\CourseResource::class, + \App\Resources\Api\Courses\Category::class, + ], + 'forum' => [ + \App\Resources\Api\Forum\Message::class, + \App\Resources\Api\Forum\Thread::class, + \App\Resources\Api\Forum\Topics::class, + ], + 'rewards' => [ + \App\Resources\Api\Rewards\UserReward::class, + \App\Resources\Api\Rewards\UserTitle::class, + ], + 'projects' => [ + \App\Resources\Api\Projects\Category::class, + \App\Resources\Api\Projects\Project::class, + ], + 'moderation' => [ + \App\Resources\Api\Moderation\Appeal::class, + ], + 'content' => [ + \App\Resources\Api\Content\Posts::class, + ], + + 'backend' => [ + 'users' => [ + \App\Resources\Api\Backend\Users\BackendUser::class, + \App\Resources\Api\Backend\Users\User::class, + \App\Resources\Api\Backend\Users\Contact::class, + ], + 'forum' => [ + \App\Resources\Api\Backend\Forum\Topic::class, + ], + 'regions' => [ + \App\Resources\Api\Backend\Regions\Region::class, + ], + 'courses' => [ + \App\Resources\Api\Backend\Courses\Category::class, + \App\Resources\Api\Backend\Courses\Course::class, + ], + 'projects' => [ + \App\Resources\Api\Backend\Projects\Category::class, + ], + 'moderation' => [ + \App\Resources\Api\Backend\Moderation\ModerationCase::class, + \App\Resources\Api\Backend\Moderation\ModerationAction::class, + \App\Resources\Api\Backend\Moderation\ModerationRequest::class, + \App\Resources\Api\Backend\Moderation\ModerationComment::class, + \App\Resources\Api\Backend\Moderation\UserSuspension::class, + ], + 'content' => [ + \App\Resources\Api\Backend\Content\Event::class, + \App\Resources\Api\Backend\Content\News::class, + \App\Resources\Api\Backend\Content\TwitchChannel::class, + \App\Resources\Api\Backend\Content\Meeting::class, + \App\Resources\Api\Backend\Content\Posts::class, + ], + 'gamification' => [ + \App\Resources\Api\Backend\Gamification\Achievement::class, + \App\Resources\Api\Backend\Gamification\UserTitle::class, + \App\Resources\Api\Backend\Gamification\AvatarItem::class, + \App\Resources\Api\Backend\Gamification\User::class, + ], + ], + + ], + + 'app' => [ + \App\Resources\App\Home::class, + \App\Resources\App\Courses::class, + \App\Resources\App\Forum::class, + \App\Resources\App\Projects::class, + \App\Resources\App\Test::class, + \App\Resources\App\PasswordReset::class, + \App\Resources\App\Tv::class, + \App\Resources\App\Pirate::class, + \App\Resources\App\Pages::class, + \App\Resources\App\Appeal::class, + \App\Resources\App\Submissions::class, + ], + + 'backend' => [ + \App\Resources\Backend\Login::class, + 'users' => [ + \App\Resources\Backend\Users\Pirates::class, + \App\Resources\Backend\Users\Backend::class, + ], + \App\Resources\Backend\Courses::class, + \App\Resources\Backend\Forum::class, + \App\Resources\Backend\Projects::class, + \App\Resources\Backend\Home::class, + \App\Resources\Backend\Contact::class, + \App\Resources\Backend\Moderation::class, + 'content' => [ + \App\Resources\Backend\Content\News::class, + \App\Resources\Backend\Content\Events::class, + \App\Resources\Backend\Content\TwitchChannels::class, + \App\Resources\Backend\Content\Meetings::class, + \App\Resources\Backend\Content\Posts::class, + ], + 'gamification' => [ + \App\Resources\Backend\Gamification\Achievements::class, + \App\Resources\Backend\Gamification\AvatarItems::class, + \App\Resources\Backend\Gamification\UserTitles::class, + \App\Resources\Backend\Gamification\Users::class, + ], + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..ce94e7f --- /dev/null +++ b/config/services.php @@ -0,0 +1,74 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'sparkpost' => [ + 'secret' => env('SPARKPOST_SECRET'), + ], + + 'stripe' => [ + 'model' => \App\Models\User\User::class, + 'key' => env('STRIPE_KEY'), + 'secret' => env('STRIPE_SECRET'), + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + ], + ], + + 'twitch' => [ + 'user_id' => env('TWITCH_USER_ID', ''), + 'client_id' => env('TWITCH_CLIENT_ID', ''), + 'client_secret' => env('TWITCH_CLIENT_SECRET', ''), + ], + + 'google' => [ + 'recaptcha' => [ + 'base_uri' => 'https://www.google.com/recaptcha/api/', + 'key' => env('RECAPTCHA_SITE_KEY'), + 'secret' => env('RECAPTCHA_SITE_SECRET'), + ], + ], + + 'tenor' => [ + 'key' => env('TENOR_API_KEY', '') + ], + + 'bugsnag' => [ + 'key' => env('BUGSNAG_API_KEY', null), + 'notify_stages' => env('BUGSNAG_NOTIFY_RELEASE_STAGES', 'production') + ], + + 'instagram' => [ + 'url' => env('INSTAGRAM_LINK', null), + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..fbb9b4d --- /dev/null +++ b/config/session.php @@ -0,0 +1,199 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using the "apc", "memcached", or "dynamodb" session drivers you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | do not enable this as other CSRF protection services are in place. + | + | Supported: "lax", "strict" + | + */ + + 'same_site' => null, + +]; diff --git a/config/system.php b/config/system.php new file mode 100644 index 0000000..3d29b60 --- /dev/null +++ b/config/system.php @@ -0,0 +1,16 @@ + [ + 'days_since_first' => env('FIRST_SIGNUP_REMINDER', 2), + 'days_since_second' => env('SECOND_SIGNUP_REMINDER', 14), + 'send_at_hours' => env('REMINDER_EMAIL_TIME', 14), + ] +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..97fc976 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1,2 @@ +*.sqlite +*.sqlite-journal diff --git a/database/factories/AchievementFactory.php b/database/factories/AchievementFactory.php new file mode 100644 index 0000000..f479e26 --- /dev/null +++ b/database/factories/AchievementFactory.php @@ -0,0 +1,33 @@ +define(Achievement::class, function (Faker $faker) { + return [ + 'name' => implode(' ', $faker->words(rand(1, 3))), + 'description' => $faker->text(180), + ]; +}); + + +$factory->afterCreating(Achievement::class, function (Achievement $achievement, Faker $faker) { + collect([ + AvatarItem::query()->take(20), + UserTitle::query(), + ]) + ->each(function ($query) use ($achievement) { + $reward = $query->inRandomOrder()->first(); + + $item = new AchievementItem(); + $item->item_id = $reward->id; + $item->item_type = get_class($reward); + + $achievement->achievementItems()->save($item); + }); +}); diff --git a/database/factories/BadgeFactory.php b/database/factories/BadgeFactory.php new file mode 100644 index 0000000..5e3bab3 --- /dev/null +++ b/database/factories/BadgeFactory.php @@ -0,0 +1,12 @@ +define(Badge::class, function (Faker $faker) { + return [ + 'name' => implode(' ', $faker->words(rand(1, 3))), + ]; +}); diff --git a/database/factories/CourseCategoryFactory.php b/database/factories/CourseCategoryFactory.php new file mode 100644 index 0000000..e77fc63 --- /dev/null +++ b/database/factories/CourseCategoryFactory.php @@ -0,0 +1,23 @@ +define(CourseCategory::class, function (Faker $faker) { + + return [ + 'title' => $faker->words(rand(1, 3), true), + 'description' => '

' . implode('

', $faker->paragraphs(3)) . '

', + 'color' => $faker->hexcolor, + 'logo_id' => factory(File::class)->state('randomImage'), + 'thumbnail_id' => factory(File::class)->state('randomImage'), + ]; +}); + +$factory->afterMaking(CourseCategory::class, function ($category, $faker) { + $category->slug = Str::slug($category->title, '_'); +}); diff --git a/database/factories/CourseResourceFactory.php b/database/factories/CourseResourceFactory.php new file mode 100644 index 0000000..0ca2fcc --- /dev/null +++ b/database/factories/CourseResourceFactory.php @@ -0,0 +1,35 @@ +define(\App\Models\Course\CourseResource::class, function (Faker $faker) { + + $type = $faker->randomElement(array('video', 'step', 'text', 'questionnaire')); + + $meta = '{}'; + + if($type === 'video') $meta = json_encode(array('title' => $faker->word,'link' => 'https://www.youtube.com/watch?v=ecC5FrJQUek')); + if($type === 'questionnaire') $meta = json_encode(array('questions' => array('q' => $faker->text(), 'a' => $faker->word(), 'options' => array($faker->word, $faker->word)))); + if($type === 'text') $meta = json_encode(array('text' => $faker->paragraphs())); + if($type === 'step') $meta = json_encode(array('title' => $faker->word(), 'text' => $faker->text())); + + return [ + 'course_id' => $faker->biasedNumberBetween(1, 20), + 'position' => $faker->biasedNumberBetween(1, 20), + 'type' => $type, + 'meta' => $meta, + ]; +}); diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php new file mode 100644 index 0000000..d1595a3 --- /dev/null +++ b/database/factories/EventFactory.php @@ -0,0 +1,44 @@ +define(Event::class, + function (Faker $faker) { + $start_date = $faker->dateTimeBetween(now()->subMonths(1), now()->addMonths(2)); + $start = Carbon::instance($start_date); + + return [ + 'title' => $faker->sentence(), + 'link' => $faker->url, + 'img' => asset('img/favicon/apple-touch-icon.png'), + 'description' => $faker->paragraph(), + 'status' => $faker + ->optional(0.25, EventStatus::PUBLISHED) // favor PUBLISHED 75% of the time + ->randomElement(array_values(array_diff(EventStatus::values(), [EventStatus::PUBLISHED]))), + + 'start_at' => $start_date, + 'end_at' => $faker->dateTimeBetween( + $start->clone()->addHours(rand(1, 2)), + $start->clone()->addHours(rand(3, 48)) + ), + + 'publish_at' => $start->clone()->subDays(rand(30, 90)), + ]; + }); + + +$factory->afterCreating(Event::class, + function (Event $event, Faker $faker) { + $regions = Region::query() + ->inRandomOrder() + ->limit(rand(1, 5)) + ->pluck('id'); + + $event->regions()->sync($regions); + }); diff --git a/database/factories/FileFactory.php b/database/factories/FileFactory.php new file mode 100644 index 0000000..6bec008 --- /dev/null +++ b/database/factories/FileFactory.php @@ -0,0 +1,31 @@ +define(File::class, function (Faker $faker) { + return [ + 'name' => $faker->word, + 'uuid' => $faker->uuid, + 'disk' => config('filepond.disk', 'local'), + 'location' => config('filepond.location', 'filepond'), +// 'extension' => '', +// 'description' => '', +// 'type' => '', +// 'mime' => '', +// 'size' => '', +// 'meta' => '', + ]; +}); + +$factory->afterMakingState(File::class, 'randomImage', function ($file, $faker) { + + $image = $faker->image($file->storage->path($file->location), 640, 480, null, false); + + $parts = explode('.', $image); + + $file->extension = array_pop($parts); + $file->uuid = join('.', $parts); +}); diff --git a/database/factories/ForumFactory.php b/database/factories/ForumFactory.php new file mode 100644 index 0000000..1eb5452 --- /dev/null +++ b/database/factories/ForumFactory.php @@ -0,0 +1,50 @@ +define(Message::class, function (Faker $faker) { + + $pirates = \Illuminate\Support\Facades\Cache::store('array')->rememberForever('all_pirates', function(){ + return User::query()->role('pirate')->get()->keyBy('id'); + }); + + $content_values = [ + $faker->paragraph(), + $faker->paragraph(5), + "

Aut nisi consectetur cumque eligendi. Sit possimus soluta ea ab sed esse officia. Provident ipsa qui repudiandae repudiandae sapiente sed voluptatem.




", + "

Etiam ligula odio, dapibus eu felis at, elementum rutrum lorem. Mauris tincidunt diam sed purus cursus mattis. Proin dignissim dui sit amet consectetur pharetra. Vestibulum ut enim quis enim pulvinar ullamcorper non sed metus. Etiam placerat sit amet nisl et maximus.

Quisque eleifend sagittis commodo. Vivamus at eleifend est, ac porttitor turpis. Etiam in mi massa. Nunc imperdiet consectetur massa. Donec iaculis justo in tincidunt euismod. Morbi eget magna purus. Quisque at dapibus libero. Curabitur nec erat luctus, dapibus sapien vel, molestie tortor.

Consectetur non qui ab nihil praesentium. Consectetur cupiditate dolor sequi rerum omnis qui. Amet repudiandae et quod. Qui cum vel explicabo sit molestiae.

", + $faker->paragraphs(3, true), + $faker->paragraphs(1, true), + ]; + + return [ + 'content' => $content_values[array_rand($content_values)], + 'created_at' => $faker->dateTime, + 'user_id' => $pirates->random()->id + ]; +}); + +$factory->afterCreating(Message::class, function (Message $message, Faker $faker) { + $message->randomModerationRequest($faker); +}); + +$factory->define(\App\Models\Forum\Topic::class, function (Faker $faker) { + return [ + 'name' => $faker->sentence, + 'slug' => $faker->slug, + 'description' => $faker->text(200) + ]; +}); + +$factory->define(Thread::class, function (Faker $faker) { + return [ + 'subject' => $faker->sentence(5, true) + ]; +}); + +$factory->afterCreating(Thread::class, function (Thread $thread, Faker $faker) { + $thread->randomModerationRequest($faker); +}); diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php new file mode 100644 index 0000000..5ea19f5 --- /dev/null +++ b/database/factories/ProjectFactory.php @@ -0,0 +1,56 @@ +define(Project::class, function (Faker $faker) { + return [ + 'owner_id' => User::query()->inRandomOrder()->first('id')->id, + 'title' => $faker->sentence, + 'description' => $faker->paragraph, + 'status' => $faker + ->optional(0.25, GenericStatus::PUBLISHED)// favor PUBLISHED 75% of the time + ->passthrough(GenericStatus::DRAFT), + + 'sort_score' => $faker->numberBetween(60, 100), + ]; +}); + +$factory->afterCreating(Project::class, + function (Project $project, Faker $faker) { + // Add categories + $categories = Category::query()->parent(false)->pluck('id'); + + $project->categories()->sync($categories->random(random_int(1, 4))->all()); + + + + $all_users = User::query()->where('id', '<>', $project->user_id)->pluck('id')->shuffle(); + $members = $all_users->splice(0, random_int(1, 4)); // Ensuring owners and contributors won't have reactions to their own project + $reactions = $all_users->random(random_int(0, 10)); + + + // Add users + $members = $members->mapWithKeys(function ($id, $index) use ($faker) { + return [$id => ['accepted' => $faker->boolean(65)]]; + }); + + $project->members()->sync($members); + + + + // Create reactions + $reactions = $reactions->map(function ($id) { + return ['user_id' => $id]; + }); + + $project->reactions()->createMany($reactions->all()); + + $project->seedThread(); + + }); diff --git a/database/factories/RegionFactory.php b/database/factories/RegionFactory.php new file mode 100644 index 0000000..c62fcfb --- /dev/null +++ b/database/factories/RegionFactory.php @@ -0,0 +1,21 @@ +define(Region::class, function (Faker $faker) { + return [ + 'name' => $faker->kommune, + ]; +}); + +$factory->afterCreating(Region::class, function (Region $region) { + + $zipcodes = Zipcode::getAll()->pluck('zipcode'); + + $region->zipcodes()->sync($zipcodes->random(random_int(0, 30))->all()); +}); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..0ebb643 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,113 @@ +define(User::class, function (Faker $faker) { + return [ + 'name' => $faker->name, + 'username' => $faker->userName, + 'email' => $faker->unique()->safeEmail, + 'parent_email' => $faker->safeEmail, + 'password' => bcrypt('password'), + 'description' => '

' . implode('

', $faker->paragraphs(3)) . '

', + 'age' => $faker->numberBetween(7, 13), + 'birthday' => $faker->dateTimeBetween('-13 years', '-7 years'), + 'remember_token' => Str::random(10), + 'email_verified_at' => now(), + ]; +}); + +$factory->state(User::class, 'predefined', function (Faker $faker) { + static $nr = 0; + $nr++; + + return [ + "username" => "pirat{$nr}", + "name" => "Pirat {$nr}", + "email" => "pirat{$nr}@morningtrain.dk", + "parent_email" => "pirat{$nr}_parent@morningtrain.dk", + "password" => bcrypt("pirat{$nr}"), + ]; +}); + +// All Users Get this +$factory->afterCreating(User::class, function (User $user, Faker $faker) { + $zipcodes = Zipcode::getAll()->pluck('zipcode'); + + $user->setMeta('zipcode', $zipcodes->random(), 'int'); +}); + + +////////////////////////// +/// Role States +////////////////////////// + +$factory->afterCreatingState(User::class, 'admin', function (User $user, Faker $faker) { + $user->assignRole('admin'); +}); + +$factory->afterCreatingState(User::class, 'pirate', function (User $user, Faker $faker) { + $user->assignRole('pirate'); +}); + + +////////////////////////// +/// Other States +////////////////////////// + +$factory->afterCreatingState(User::class, 'all_avatar_items', function (User $user, Faker $faker) { + $user->rewardItems()->createMany( + AvatarItem::pluck('id')->map(function ($id) { + return [ + 'item_id' => $id, + 'item_type' => AvatarItem::class, + ]; + }) + ); +}); + +$factory->afterCreatingState(User::class, 'dummy', function (User $user, Faker $faker) { + $user->randomModerationRequest($faker); + + Achievement::inRandomOrder() + ->limit(rand(0, 10))->get() + ->each->grantAchievement($user); + + $user->rewards() + ->inRandomOrder() + ->limit($faker->biasedNumberBetween(0, 10))->get() + ->each->open(); + + $title = $user->rewardItems() + ->whereItemType(UserTitle::class) + ->with('item') + ->inRandomOrder() + ->first(); + + if ($title !== null) { + $user->selectTitle($title->item); + } +}); diff --git a/database/factories/UserTitleFactory.php b/database/factories/UserTitleFactory.php new file mode 100644 index 0000000..6880159 --- /dev/null +++ b/database/factories/UserTitleFactory.php @@ -0,0 +1,12 @@ +define(UserTitle::class, function (Faker $faker) { + return [ + 'title' => implode(' ', $faker->words(rand(1, 3))), + ]; +}); diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..3cd12bc --- /dev/null +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,61 @@ +bigIncrements('id'); + $table->string('name'); + $table->string('username')->unique(); + $table->string('email')->unique(); + $table->string('parent_email')->nullable(); + $table->string('password'); + $table->longText('description')->nullable(); + $table->unsignedInteger('age'); + $table->date('birthday')->nullable(); + $table->rememberToken(); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('user_meta', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('user_id')->unsigned(); + $table->string('name'); + $table->string('type')->default('string'); + $table->text('value')->nullable(); + $table->timestamps(); + + $table + ->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + // user_meta_user_id_name_unique + $table->unique(['user_id', 'name']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_meta'); + Schema::dropIfExists('users'); + } +} diff --git a/database/migrations/2014_10_12_100000_create_password_resets_table.php b/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 0000000..0d5cb84 --- /dev/null +++ b/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,32 @@ +string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('password_resets'); + } +} diff --git a/database/migrations/2019_04_08_093828_create_permission_tables.php b/database/migrations/2019_04_08_093828_create_permission_tables.php new file mode 100644 index 0000000..0fe3cac --- /dev/null +++ b/database/migrations/2019_04_08_093828_create_permission_tables.php @@ -0,0 +1,102 @@ +increments('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + }); + + Schema::create($tableNames['roles'], function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + }); + + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { + $table->unsignedInteger('permission_id'); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type', ]); + + $table->foreign('permission_id') + ->references('id') + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->primary(['permission_id', $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + }); + + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { + $table->unsignedInteger('role_id'); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type', ]); + + $table->foreign('role_id') + ->references('id') + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary(['role_id', $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + }); + + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { + $table->unsignedInteger('permission_id'); + $table->unsignedInteger('role_id'); + + $table->foreign('permission_id') + ->references('id') + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign('role_id') + ->references('id') + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary(['permission_id', 'role_id']); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $tableNames = config('permission.table_names'); + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +} diff --git a/database/migrations/2019_04_11_064032_create_reserved_usernames_table.php b/database/migrations/2019_04_11_064032_create_reserved_usernames_table.php new file mode 100644 index 0000000..46d9c40 --- /dev/null +++ b/database/migrations/2019_04_11_064032_create_reserved_usernames_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + + $table->string('username')->unique(); + $table->string('identifier'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('reserved_usernames'); + } +} diff --git a/database/migrations/2019_04_12_124649_create_avatar_tables.php b/database/migrations/2019_04_12_124649_create_avatar_tables.php new file mode 100644 index 0000000..5a039c7 --- /dev/null +++ b/database/migrations/2019_04_12_124649_create_avatar_tables.php @@ -0,0 +1,79 @@ +bigIncrements('id'); + $table->string('name'); + $table->string('category'); + $table->longText('content'); + $table->string('meta')->nullable(); + $table->boolean('is_public'); + $table->boolean('is_default'); + $table->boolean('is_featured')->default(false); + $table->string('status'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('user_avatars', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->bigInteger('user_avatar_id')->unsigned()->nullable()->after('id'); + + $table->foreign('user_avatar_id') + ->references('id') + ->on('user_avatars') + ->onDelete('set null'); + }); + + Schema::create('user_avatar_items', + function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('user_avatar_id')->unsigned(); + $table->bigInteger('avatar_item_id')->unsigned(); + $table->string('category'); + $table->timestamps(); + + $table->foreign('user_avatar_id') + ->references('id') + ->on('user_avatars') + ->onDelete('cascade'); + + $table->foreign('avatar_item_id') + ->references('id') + ->on('avatar_items') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['user_avatar_id']); + }); + + Schema::dropIfExists('user_avatar_items'); + Schema::dropIfExists('user_avatars'); + Schema::dropIfExists('avatar_items'); + } +} diff --git a/database/migrations/2019_05_04_094221_create_files_table.php b/database/migrations/2019_05_04_094221_create_files_table.php new file mode 100644 index 0000000..9b97391 --- /dev/null +++ b/database/migrations/2019_05_04_094221_create_files_table.php @@ -0,0 +1,45 @@ +bigIncrements('id'); + $table->timestamps(); + + $table->string('name')->nullable(); + $table->string('extension')->nullable(); + $table->string('description')->nullable(); + $table->string('uuid')->nullable(); + $table->string('location')->nullable(); + $table->string('disk')->nullable(); + + $table->string('type')->nullable(); + $table->string('mime')->nullable(); + $table->integer('size')->nullable(); + + $table->longText('meta')->nullable(); + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('files'); + } +} diff --git a/database/migrations/2019_05_06_093839_add_news_table.php b/database/migrations/2019_05_06_093839_add_news_table.php new file mode 100644 index 0000000..83d360d --- /dev/null +++ b/database/migrations/2019_05_06_093839_add_news_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + $table->string('title'); + $table->string('img'); + $table->string('link'); + $table->text('subtext'); + $table->string('theme'); + $table->string('status')->default('draft'); + $table->tinyInteger('featured')->default(0); + $table->dateTime('publish_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('news'); + } +} diff --git a/database/migrations/2019_05_06_124649_create_forum_tables.php b/database/migrations/2019_05_06_124649_create_forum_tables.php new file mode 100644 index 0000000..f167a45 --- /dev/null +++ b/database/migrations/2019_05_06_124649_create_forum_tables.php @@ -0,0 +1,187 @@ +down(); + + Schema::create('forum_topics', function (Blueprint $table) { + + $table->bigIncrements('id'); + + $table->string('name')->nullable(); + $table->string('slug')->nullable(); + $table->string('status')->nullable(); + $table->longText('description')->nullable(); + $table->bigInteger('parent_id')->unsigned()->nullable(); + //$table->bigInteger('upper_parent_id')->unsigned()->nullable(); + + }); + + Schema::create('forum_topic_ancestry', function (Blueprint $table) { + $table->bigInteger('topic_id')->unsigned()->nullable(); + $table->bigInteger('ancestor_id')->unsigned()->nullable(); + + $table->foreign('topic_id') + ->references('id') + ->on('forum_topics') + ->onDelete('cascade'); + + $table->foreign('ancestor_id') + ->references('id') + ->on('forum_topics') + ->onDelete('cascade'); + + }); + + Schema::create('forum_threads', function (Blueprint $table) { + $table->bigIncrements('id'); + + $table->string('subject')->nullable(); + $table->string('status')->default(\App\Support\Enums\SystemStatus::ACTIVE); + $table->string('type')->default(\App\Support\Enums\ForumThreadType::DISCUSSION); + + $table->bigInteger('original_message_id')->unsigned()->nullable(); + $table->bigInteger('accepted_answer_id')->unsigned()->nullable(); + $table->bigInteger('most_popular_answer_id')->unsigned()->nullable(); + $table->bigInteger('topic_id')->unsigned()->nullable(); + + $table->boolean('grownups_can_participate')->default(false); + $table->boolean('is_embedded')->default(false); + + $table->float('sort_score')->default(80); + + $table->bigInteger('created_by')->unsigned()->nullable(); + $table->boolean('blocked_user')->default(false); + $table->timestamps(); + + $table->softDeletes(); + + $table->foreign('created_by') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('topic_id') + ->references('id') + ->on('forum_topics') + ->onDelete('cascade'); + + }); + + Schema::create('forum_messages', function (Blueprint $table) { + + $table->bigIncrements('id'); + $table->timestamps(); + + $table->longText('content'); + $table->bigInteger('thread_id')->unsigned()->nullable(); + $table->bigInteger('user_id')->unsigned()->nullable(); + $table->boolean('blocked_user')->default(false); + $table->boolean('moderated')->default(false); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('thread_id') + ->references('id') + ->on('forum_threads') + ->onDelete('cascade'); + + }); + + Schema::create('forum_message_changes', function (Blueprint $table) { + + $table->bigIncrements('id'); + $table->timestamps(); + + $table->longText('content'); + $table->string('reason'); + $table->bigInteger('user_id')->unsigned()->nullable(); + $table->bigInteger('message_id')->unsigned()->nullable(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('message_id') + ->references('id') + ->on('forum_messages') + ->onDelete('cascade'); + + }); + + Schema::create('forum_message_reactions', function (Blueprint $table) { + + $table->bigIncrements('id'); + $table->timestamps(); + + $table->string('type')->default(\App\Support\Enums\ReactionType::LIKE); + + $table->bigInteger('user_id')->unsigned()->nullable(); + $table->bigInteger('message_id')->unsigned()->nullable(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('message_id') + ->references('id') + ->on('forum_messages') + ->onDelete('cascade'); + + }); + + Schema::create('forum_message_mentions', function (Blueprint $table) { + + $table->bigIncrements('id'); + $table->timestamps(); + + $table->bigInteger('user_id')->unsigned()->nullable(); + $table->bigInteger('message_id')->unsigned()->nullable(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('message_id') + ->references('id') + ->on('forum_messages') + ->onDelete('cascade'); + + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('forum_message_mentions'); + Schema::dropIfExists('forum_message_reactions'); + Schema::dropIfExists('forum_message_changes'); + Schema::dropIfExists('forum_messages'); + Schema::dropIfExists('forum_threads'); + Schema::dropIfExists('forum_topic_ancestry'); + Schema::dropIfExists('forum_topics'); + } +} diff --git a/database/migrations/2019_05_09_121009_create_jobs_table.php b/database/migrations/2019_05_09_121009_create_jobs_table.php new file mode 100644 index 0000000..58d7715 --- /dev/null +++ b/database/migrations/2019_05_09_121009_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/database/migrations/2019_05_13_073201_course_tables.php b/database/migrations/2019_05_13_073201_course_tables.php new file mode 100644 index 0000000..e4c4ef5 --- /dev/null +++ b/database/migrations/2019_05_13_073201_course_tables.php @@ -0,0 +1,113 @@ +bigIncrements('id'); + $table->string('slug'); + $table->string('title'); + $table->boolean('active')->default(true); + $table->text('description')->nullable(); + $table->string('color')->nullable(); + $table->bigInteger('logo_id')->unsigned()->nullable(); + $table->bigInteger('thumbnail_id')->unsigned()->nullable(); + $table->timestamps(); + + $table->foreign('logo_id') + ->references('id')->on('files') + ->onDelete('set null'); + + $table->foreign('thumbnail_id') + ->references('id')->on('files') + ->onDelete('set null'); + }); + + Schema::create('courses', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('title'); + $table->string('slug'); + $table->bigInteger('category_id')->unsigned()->nullable(); + $table->integer('level'); + $table->text('description'); + $table->integer('position'); + $table->timestamps(); + + + $table->foreign('category_id') + ->references('id') + ->on('course_categories') + ->onDelete('set null'); + }); + + Schema::create('course_resources', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('course_id')->unsigned()->nullable(); + $table->integer('position')->default(0); + $table->string('type'); + $table->longText('meta')->nullable(); + $table->timestamps(); + + $table->foreign('course_id') + ->references('id') + ->on('courses') + ->onDelete('cascade'); + }); + + Schema::create('course_progress', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('course_category_id')->unsigned()->nullable(); + $table->bigInteger('course_id')->unsigned()->nullable(); + $table->bigInteger('course_resource_id')->unsigned()->nullable(); + $table->bigInteger('user_id')->unsigned(); + $table->longText('meta')->nullable(); + $table->string('status'); + $table->timestamps(); + + $table->foreign('course_category_id') + ->references('id') + ->on('course_categories') + ->onDelete('cascade'); + + $table->foreign('course_id') + ->references('id') + ->on('courses') + ->onDelete('cascade'); + + $table->foreign('course_resource_id') + ->references('id') + ->on('course_resources') + ->onDelete('cascade'); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('course_progress'); + Schema::dropIfExists('course_resources'); + Schema::dropIfExists('courses'); + Schema::dropIfExists('course_categories'); + } +} diff --git a/database/migrations/2019_05_15_124005_setup_zipcodes_and_regions.php b/database/migrations/2019_05_15_124005_setup_zipcodes_and_regions.php new file mode 100644 index 0000000..70be666 --- /dev/null +++ b/database/migrations/2019_05_15_124005_setup_zipcodes_and_regions.php @@ -0,0 +1,47 @@ +integer('zipcode')->unsigned(); + $table->string('city'); + $table->timestamps(); + + $table->primary('zipcode'); + }); + + Schema::create('regions', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('region_zipcode', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('region_id')->unsigned(); + $table->integer('zipcode_zipcode')->unsigned(); + + $table->foreign('region_id') + ->references('id')->on('regions') + ->onDelete('cascade'); + + $table->foreign('zipcode_zipcode') + ->references('zipcode')->on('zipcodes') + ->onDelete('cascade'); + }); + } + + public function down() + { + Schema::dropIfExists('region_zipcode'); + Schema::dropIfExists('regions'); + Schema::dropIfExists('zipcodes'); + } +} diff --git a/database/migrations/2019_05_24_090232_add_events_table.php b/database/migrations/2019_05_24_090232_add_events_table.php new file mode 100644 index 0000000..9587f12 --- /dev/null +++ b/database/migrations/2019_05_24_090232_add_events_table.php @@ -0,0 +1,69 @@ +bigIncrements('id'); + $table->string('title'); + $table->string('link'); + $table->string('img')->nullable(); + $table->text('description'); + $table->string('status')->default(EventStatus::DRAFT); + + $table->dateTime('start_at'); + $table->dateTime('end_at'); + $table->dateTime('publish_at'); + + $table->timestamps(); + }); + + Schema::create('event_reminders', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('event_id')->unsigned(); + $table->dateTime('remind_at'); + $table->boolean('reminded')->default(false); + $table->timestamps(); + + $table->foreign('event_id') + ->references('id')->on('events') + ->onDelete('cascade'); + }); + + Schema::create('event_region', function (Blueprint $table) { + $table->bigInteger('event_id')->unsigned(); + $table->bigInteger('region_id')->unsigned(); + + $table->foreign('event_id') + ->references('id')->on('events') + ->onDelete('cascade'); + + $table->foreign('region_id') + ->references('id')->on('regions') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('event_reminders'); + Schema::dropIfExists('event_region'); + Schema::dropIfExists('events'); + } +} diff --git a/database/migrations/2019_06_03_132122_create_notifications_table.php b/database/migrations/2019_06_03_132122_create_notifications_table.php new file mode 100644 index 0000000..fb16d5b --- /dev/null +++ b/database/migrations/2019_06_03_132122_create_notifications_table.php @@ -0,0 +1,35 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('notifications'); + } +} diff --git a/database/migrations/2019_07_19_084929_create_project_categories_table.php b/database/migrations/2019_07_19_084929_create_project_categories_table.php new file mode 100644 index 0000000..a256044 --- /dev/null +++ b/database/migrations/2019_07_19_084929_create_project_categories_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->string('name')->nullable(); + $table->string('slug')->nullable(); + $table->bigInteger('parent_id')->unsigned()->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('project_categories'); + } +} diff --git a/database/migrations/2019_07_19_103901_setup_projects_tables.php b/database/migrations/2019_07_19_103901_setup_projects_tables.php new file mode 100644 index 0000000..8931c94 --- /dev/null +++ b/database/migrations/2019_07_19_103901_setup_projects_tables.php @@ -0,0 +1,127 @@ +bigIncrements('id'); + $table->bigInteger('owner_id')->unsigned(); + $table->string('title'); + $table->unsignedBigInteger('thread_id')->nullable(); + $table->bigInteger('cover_image_id')->unsigned()->nullable(); + $table->bigInteger('thumbnail_id')->unsigned()->nullable(); + $table->longText('description')->nullable(); + $table->string('status')->default(\App\Support\Enums\GenericStatus::DRAFT); + $table->string('system_status')->default(\App\Support\Enums\SystemStatus::ACTIVE); + $table->float('sort_score')->default(80); + $table->boolean('blocked_user')->default(false); + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('owner_id') + ->references('id')->on('users') + ->onDelete('cascade'); + + $table->foreign('thread_id') + ->references('id')->on('forum_threads') + ->onDelete('set null'); + + $table->foreign('cover_image_id') + ->references('id')->on('files') + ->onDelete('set null'); + + $table->foreign('thumbnail_id') + ->references('id')->on('files') + ->onDelete('set null'); + }); + + Schema::create('project_user', function (Blueprint $table) { + $table->bigInteger('project_id')->unsigned(); + $table->bigInteger('user_id')->unsigned(); + $table->boolean('accepted')->default(false); + + $table->primary(['project_id', 'user_id']); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + + $table->foreign('project_id') + ->references('id')->on('projects') + ->onDelete('cascade'); + }); + + Schema::create('project_files', function (Blueprint $table) { + $table->bigInteger('project_id')->unsigned(); + $table->bigInteger('file_id')->unsigned(); + + $table->primary(['project_id', 'file_id']); + + $table->foreign('project_id') + ->references('id')->on('projects') + ->onDelete('cascade'); + $table->foreign('file_id') + ->references('id')->on('files') + ->onDelete('cascade'); + }); + + Schema::create('project_reactions', function (Blueprint $table) { + + $table->bigIncrements('id'); + $table->timestamps(); + $table->string('type')->default(\App\Support\Enums\ReactionType::LIKE); + $table->bigInteger('user_id')->unsigned()->nullable(); + $table->bigInteger('project_id')->unsigned()->nullable(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('project_id') + ->references('id') + ->on('projects') + ->onDelete('cascade'); + }); + + + Schema::create('project_project_categories', function (Blueprint $table) { + $table->bigInteger('project_id')->unsigned(); + $table->bigInteger('category_id')->unsigned(); + + $table->primary(['project_id', 'category_id']); + + $table->foreign('category_id') + ->references('id')->on('project_categories') + ->onDelete('cascade'); + + $table->foreign('project_id') + ->references('id')->on('projects') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('project_project_categories'); + Schema::dropIfExists('project_reactions'); + Schema::dropIfExists('project_user'); + Schema::dropIfExists('project_files'); + Schema::dropIfExists('projects'); + } +} diff --git a/database/migrations/2019_07_30_103519_setup_moderation_tables.php b/database/migrations/2019_07_30_103519_setup_moderation_tables.php new file mode 100644 index 0000000..153ebf6 --- /dev/null +++ b/database/migrations/2019_07_30_103519_setup_moderation_tables.php @@ -0,0 +1,72 @@ +bigIncrements('id'); + + $table->unsignedBigInteger('moderateable_id'); + $table->string('moderateable_type'); + $table->unsignedBigInteger('user_id')->nullable(); + $table->string('status')->default(\App\Support\Enums\ModerationCaseStatus::PENDING); + + $table->timestamps(); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + }); + + Schema::create('moderation_actions', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('moderation_case_id'); + $table->unsignedBigInteger('user_id')->nullable(); + $table->string('type')->default(\App\Support\Enums\ModerationActionType::SYSTEM); + $table->string('action_class'); + $table->string('note')->nullable(); + $table->longText('meta')->nullable(); + $table->timestamps(); + + $table->foreign('moderation_case_id') + ->references('id')->on('moderation_cases') + ->onDelete('cascade'); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('set null'); + }); + + Schema::create('moderation_requests', function (Blueprint $table) { + $table->bigIncrements('id'); + + $table->unsignedBigInteger('moderation_case_id'); + $table->unsignedBigInteger('reporter_id')->nullable(); + $table->string('reason'); + $table->text('comment')->nullable(); + + $table->timestamps(); + $table->timestamp('resolved_at')->nullable(); + + $table->foreign('moderation_case_id') + ->references('id')->on('moderation_cases') + ->onDelete('cascade'); + + $table->foreign('reporter_id') + ->references('id')->on('users') + ->onDelete('set null'); + }); + } + + public function down() + { + Schema::dropIfExists('moderation_requests'); + Schema::dropIfExists('moderation_actions'); + Schema::dropIfExists('moderation_cases'); + } +} diff --git a/database/migrations/2019_08_08_103519_setup_animated_ticker_texts_tables.php b/database/migrations/2019_08_08_103519_setup_animated_ticker_texts_tables.php new file mode 100644 index 0000000..a2af142 --- /dev/null +++ b/database/migrations/2019_08_08_103519_setup_animated_ticker_texts_tables.php @@ -0,0 +1,22 @@ +bigIncrements('id'); + $table->string('text'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('animated_ticker_texts'); + } +} diff --git a/database/migrations/2019_08_09_103519_setup_twitch_tables.php b/database/migrations/2019_08_09_103519_setup_twitch_tables.php new file mode 100644 index 0000000..0b64c02 --- /dev/null +++ b/database/migrations/2019_08_09_103519_setup_twitch_tables.php @@ -0,0 +1,27 @@ +bigIncrements('id'); + $table->string('channel_name'); + $table->string('collection'); + $table->boolean('is_live')->default(false); + $table->dateTime('stream_checked_at')->nullable(); + $table->dateTime('stream_started_at')->nullable(); + $table->string('stream_title')->default(false); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('twitch_channels'); + } +} diff --git a/database/migrations/2019_08_14_103519_setup_posts_tables.php b/database/migrations/2019_08_14_103519_setup_posts_tables.php new file mode 100644 index 0000000..79fbf9a --- /dev/null +++ b/database/migrations/2019_08_14_103519_setup_posts_tables.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + + $table->unsignedBigInteger('parent_id')->nullable(); + $table->unsignedInteger('version')->default(1); + + $table->string('type')->default(\App\Support\Enums\PostType::PAGE); + $table->string('status')->default(\App\Support\Enums\GenericStatus::DRAFT); + + $table->string('path'); + $table->string('title'); + $table->longText('content'); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('posts'); + } +} diff --git a/database/migrations/2019_08_23_103519_setup_contact_submissions_tables.php b/database/migrations/2019_08_23_103519_setup_contact_submissions_tables.php new file mode 100644 index 0000000..1649300 --- /dev/null +++ b/database/migrations/2019_08_23_103519_setup_contact_submissions_tables.php @@ -0,0 +1,33 @@ +bigIncrements('id'); + + $table->string('type')->default(\App\Support\Enums\ContactSubmissionType::GENERIC); + $table->string('status')->default(\App\Support\Enums\ContactSubmissionStatus::DRAFT); + + $table->string('name')->nullable(); + $table->string('subject')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->longText('message')->nullable(); + $table->decimal('score', 2, 1)->nullable(); + + $table->timestamps(); + $table->timestamp('recaptcha_at')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('contact_submissions'); + } +} diff --git a/database/migrations/2019_08_23_115610_create_user_titles_table.php b/database/migrations/2019_08_23_115610_create_user_titles_table.php new file mode 100644 index 0000000..8690e7e --- /dev/null +++ b/database/migrations/2019_08_23_115610_create_user_titles_table.php @@ -0,0 +1,48 @@ +bigIncrements('id'); + $table->string('title'); + $table->timestamps(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->unsignedBigInteger('title_id')->nullable()->after('birthday'); + + $table->foreign('title_id') + ->references('id') + ->on('user_titles') + ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if (Schema::hasColumn('users', 'title_id')) { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['title_id']); + $table->dropColumn('title_id'); + }); + } + + Schema::dropIfExists('user_titles'); + } +} diff --git a/database/migrations/2019_08_23_115616_create_badges_table.php b/database/migrations/2019_08_23_115616_create_badges_table.php new file mode 100644 index 0000000..114bda7 --- /dev/null +++ b/database/migrations/2019_08_23_115616_create_badges_table.php @@ -0,0 +1,33 @@ +bigIncrements('id'); + $table->string('name'); + // TODO color? image? + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('badges'); + } +} diff --git a/database/migrations/2019_08_23_115622_setup_achievement_rewards.php b/database/migrations/2019_08_23_115622_setup_achievement_rewards.php new file mode 100644 index 0000000..87793a1 --- /dev/null +++ b/database/migrations/2019_08_23_115622_setup_achievement_rewards.php @@ -0,0 +1,82 @@ +bigIncrements('id'); + + $table->string('name')->nullable(); + $table->string('description')->nullable(); + + $table->timestamps(); + }); + + Schema::create('achievement_items', function (Blueprint $table) { + $table->bigIncrements('id'); + + $table->unsignedBigInteger('achievement_id'); + + $table->unsignedBigInteger('item_id'); + $table->string('item_type'); + + $table->timestamps(); + }); + + Schema::create('user_rewards', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('achievement_id'); + + $table->timestamps(); + $table->timestamp('opened_at')->nullable(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->unique(['user_id', 'achievement_id']); + }); + + + Schema::create('user_reward_items', function (Blueprint $table) { + $table->bigIncrements('id'); + + $table->unsignedBigInteger('user_id'); + + $table->unsignedBigInteger('item_id'); + $table->string('item_type'); + + $table->timestamps(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('achievement_items'); + Schema::dropIfExists('user_rewards'); + Schema::dropIfExists('achievements'); + Schema::dropIfExists('user_reward_items'); + } +} diff --git a/database/migrations/2019_09_12_124027_create_appeals_table.php b/database/migrations/2019_09_12_124027_create_appeals_table.php new file mode 100644 index 0000000..449b70b --- /dev/null +++ b/database/migrations/2019_09_12_124027_create_appeals_table.php @@ -0,0 +1,42 @@ +bigIncrements('id'); + $table->unsignedBigInteger('moderation_case_id'); + + $table->string('name')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->longText('message')->nullable(); + + $table->timestamps(); + + $table->foreign('moderation_case_id') + ->references('id')->on('moderation_cases') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('appeals'); + } +} diff --git a/database/migrations/2019_09_13_053255_create_user_suspensions_table.php b/database/migrations/2019_09_13_053255_create_user_suspensions_table.php new file mode 100644 index 0000000..dd89fa7 --- /dev/null +++ b/database/migrations/2019_09_13_053255_create_user_suspensions_table.php @@ -0,0 +1,43 @@ +bigIncrements('id'); + + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('issued_by')->nullable(); + + $table->boolean('active')->default(true); + $table->timestamp('start_at')->nullable(); + $table->timestamp('end_at')->nullable(); + + $table->timestamps(); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_suspensions'); + } +} diff --git a/database/migrations/2019_10_14_053255_create_change_log_table.php b/database/migrations/2019_10_14_053255_create_change_log_table.php new file mode 100644 index 0000000..f271a5b --- /dev/null +++ b/database/migrations/2019_10_14_053255_create_change_log_table.php @@ -0,0 +1,48 @@ +bigIncrements('id'); + + $table->unsignedBigInteger('changed_by')->nullable(); + + $table->unsignedBigInteger('changeable_id')->nullable(); + $table->string('changeable_type')->nullable(); + + $table->timestamps(); + $table->timestamp('changed_at')->nullable(); + + $table->string('column_name')->nullable(); + + $table->mediumText('from_value')->nullable(); + $table->string('from_type')->nullable(); + + $table->mediumText('to_value')->nullable(); + $table->string('to_type')->nullable(); + + $table->index(['changeable_id', 'changeable_type']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('changes'); + } +} diff --git a/database/migrations/2019_12_20_100448_create_failed_jobs_table.php b/database/migrations/2019_12_20_100448_create_failed_jobs_table.php new file mode 100644 index 0000000..389bdf7 --- /dev/null +++ b/database/migrations/2019_12_20_100448_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/database/migrations/2020_03_02_134752_add_is_sticky_to_forum_threads.php b/database/migrations/2020_03_02_134752_add_is_sticky_to_forum_threads.php new file mode 100644 index 0000000..8106d22 --- /dev/null +++ b/database/migrations/2020_03_02_134752_add_is_sticky_to_forum_threads.php @@ -0,0 +1,32 @@ +boolean('is_sticky')->default(false)->after('status'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('forum_threads', function (Blueprint $table) { + $table->dropColumn('is_sticky'); + }); + } +} diff --git a/database/migrations/2020_04_14_133439_add_priority_to_project_categories_table.php b/database/migrations/2020_04_14_133439_add_priority_to_project_categories_table.php new file mode 100644 index 0000000..025f588 --- /dev/null +++ b/database/migrations/2020_04_14_133439_add_priority_to_project_categories_table.php @@ -0,0 +1,32 @@ +unsignedInteger('priority')->default(0)->after('parent_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('project_categories', function (Blueprint $table) { + $table->dropColumn('priority'); + }); + } +} diff --git a/database/migrations/2020_04_15_071542_add_status_to_project_categories_table.php b/database/migrations/2020_04_15_071542_add_status_to_project_categories_table.php new file mode 100644 index 0000000..1ebc758 --- /dev/null +++ b/database/migrations/2020_04_15_071542_add_status_to_project_categories_table.php @@ -0,0 +1,33 @@ +string('status')->default(VisibleStatus::VISIBLE)->after('slug'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('project_categories', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +} diff --git a/database/migrations/2020_04_15_135645_add_og_meta_to_posts_table.php b/database/migrations/2020_04_15_135645_add_og_meta_to_posts_table.php new file mode 100644 index 0000000..2a75a4d --- /dev/null +++ b/database/migrations/2020_04_15_135645_add_og_meta_to_posts_table.php @@ -0,0 +1,34 @@ +string('image')->nullable()->after('title'); + $table->string('description')->nullable()->after('title'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('image'); + $table->dropColumn('description'); + }); + } +} diff --git a/database/migrations/2020_04_20_114743_add_last_activity_at_column_to_users_table.php b/database/migrations/2020_04_20_114743_add_last_activity_at_column_to_users_table.php new file mode 100644 index 0000000..ede7f1e --- /dev/null +++ b/database/migrations/2020_04_20_114743_add_last_activity_at_column_to_users_table.php @@ -0,0 +1,32 @@ +timestamp('last_activity_at')->nullable()->after('email_verified_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('last_activity_at'); + }); + } +} diff --git a/database/migrations/2020_04_21_110621_create_meetings_table.php b/database/migrations/2020_04_21_110621_create_meetings_table.php new file mode 100644 index 0000000..7562293 --- /dev/null +++ b/database/migrations/2020_04_21_110621_create_meetings_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('description'); + $table->string('meeting_room')->nullable(); + $table->boolean('is_active')->default(false); + $table->dateTime('from'); + $table->dateTime('to'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('meetings'); + } +} diff --git a/database/migrations/2020_04_28_082212_add_achievement_id_column_to_courses_table.php b/database/migrations/2020_04_28_082212_add_achievement_id_column_to_courses_table.php new file mode 100644 index 0000000..31ba7ef --- /dev/null +++ b/database/migrations/2020_04_28_082212_add_achievement_id_column_to_courses_table.php @@ -0,0 +1,38 @@ +unsignedBigInteger('achievement_id')->nullable()->after('category_id'); + + $table->foreign('achievement_id') + ->references('id') + ->on('achievements') + ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('courses', function (Blueprint $table) { + $table->dropForeign(['achievement_id']); + $table->dropColumn('achievement_id'); + }); + } +} diff --git a/database/migrations/2020_04_30_071106_change_meetings_table.php b/database/migrations/2020_04_30_071106_change_meetings_table.php new file mode 100644 index 0000000..47819bf --- /dev/null +++ b/database/migrations/2020_04_30_071106_change_meetings_table.php @@ -0,0 +1,38 @@ +renameColumn('is_active', 'banner_active'); + $table->boolean('meeting_active')->default(false)->after('meeting_room'); + $table->time('from')->change(); + $table->time('to')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('meetings', function (Blueprint $table) { + $table->dateTime('from')->change(); + $table->dateTime('to')->change(); + $table->dropColumn('meeting_active'); + $table->renameColumn('banner_active', 'is_active'); + }); + } +} diff --git a/database/migrations/2020_05_07_110840_add_published_at_column_to_projects_table.php b/database/migrations/2020_05_07_110840_add_published_at_column_to_projects_table.php new file mode 100644 index 0000000..348f6d0 --- /dev/null +++ b/database/migrations/2020_05_07_110840_add_published_at_column_to_projects_table.php @@ -0,0 +1,38 @@ +timestamp('published_at')->nullable()->after('updated_at'); + }); + + Project::query() + ->published(true) + ->update(['published_at' => DB::raw('updated_at')]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('projects', function (Blueprint $table) { + $table->dropColumn('published_at'); + }); + } +} diff --git a/database/migrations/2020_05_18_091035_rework_user_rewards.php b/database/migrations/2020_05_18_091035_rework_user_rewards.php new file mode 100644 index 0000000..30bfd16 --- /dev/null +++ b/database/migrations/2020_05_18_091035_rework_user_rewards.php @@ -0,0 +1,59 @@ +string('description')->nullable()->after('achievement_id'); + $table->string('name')->nullable()->after('achievement_id'); + }); + + Schema::table('user_reward_items', function (Blueprint $table) { + $table->unsignedBigInteger('user_reward_id')->nullable()->after('user_id'); + + $table->foreign('user_reward_id') + ->references('id') + ->on('user_rewards') + ->onDelete('cascade'); + }); + + DB::table('user_rewards as a') + ->join('achievements as b', 'a.achievement_id', '=', 'b.id') + ->select('a.*', 'b.name', 'b.description') + ->update([ + 'a.name' => DB::raw('b.name'), + 'a.description' => DB::raw('b.description'), + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('user_rewards', function (Blueprint $table) { + $table->dropColumn('description'); + $table->dropColumn('name'); + }); + + Schema::table('user_reward_items', function (Blueprint $table) { + $table->dropForeign('user_reward_items_user_reward_id_foreign'); + $table->dropColumn('user_reward_id'); + }); + } +} diff --git a/database/migrations/2020_05_27_113308_add_weekly_newsletter_notification_setting_for_users.php b/database/migrations/2020_05_27_113308_add_weekly_newsletter_notification_setting_for_users.php new file mode 100644 index 0000000..b405d56 --- /dev/null +++ b/database/migrations/2020_05_27_113308_add_weekly_newsletter_notification_setting_for_users.php @@ -0,0 +1,48 @@ +where('name', 'notification_settings') + ->get() + ->each(function (UserMeta $meta) { + $value = $meta->value; + $value->{NotificationType::WEEKLY_NEWSLETTER} = false; + + $meta->value = $value; + $meta->save(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + \App\Models\User\UserMeta::query() + ->where('name', 'notification_settings') + ->get() + ->each(function (UserMeta $meta) { + $value = $meta->value; + unset($value->{NotificationType::WEEKLY_NEWSLETTER}); + + $meta->value = $value; + $meta->save(); + }); + } +} diff --git a/database/seeds/AnimatedTickerTextSeeder.php b/database/seeds/AnimatedTickerTextSeeder.php new file mode 100644 index 0000000..b1058c9 --- /dev/null +++ b/database/seeds/AnimatedTickerTextSeeder.php @@ -0,0 +1,30 @@ +text = $text; + $ticker->save(); + } + + } +} diff --git a/database/seeds/AvatarItemsTableSeeder.php b/database/seeds/AvatarItemsTableSeeder.php new file mode 100644 index 0000000..c1bf69d --- /dev/null +++ b/database/seeds/AvatarItemsTableSeeder.php @@ -0,0 +1,1283 @@ +delete(); + + \DB::table('avatar_items')->insert($this->getItems()); + + } + + protected function getItems() + { + + $avatar_items = array( + array( + "name" => "Tunge skrå", + "category" => "mouth", + "content" => "\r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"8.456254482269287\",\"y\":\"2.3323941826820374\"},\"viewBox\":{\"height\":\"10.882106065750122\",\"width\":\"17.84978199005127\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 09:03:24", + "updated_at" => "2019-12-20 09:03:24", + "deleted_at" => NULL, + ), + array( + "name" => "Tunge", + "category" => "mouth", + "content" => "\r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"8.256290912628174\",\"y\":\"2.5206636786460876\"},\"viewBox\":{\"height\":\"12.011723041534424\",\"width\":\"16.65000057220459\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 09:00:02", + "updated_at" => "2019-12-20 09:01:43", + "deleted_at" => NULL, + ), + array( + "name" => "Monokel", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"13.125\",\"y\":\"7.117500066757202\"},\"viewBox\":{\"height\":\"21.705000400543213\",\"width\":\"45.75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 08:51:38", + "updated_at" => "2019-12-20 08:56:38", + "deleted_at" => NULL, + ), + array( + "name" => "Røde kinder", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"30.9325008392334\",\"y\":\"25.605000019073486\"},\"viewBox\":{\"height\":\"9.630000114440918\",\"width\":\"61.99500274658203\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 08:44:57", + "updated_at" => "2019-12-20 08:46:40", + "deleted_at" => NULL, + ), + array( + "name" => "Skærm med ^^ øjne", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"21.617499828338623\",\"y\":\"7.549999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"42.70499897003174\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 08:40:39", + "updated_at" => "2019-12-20 08:43:12", + "deleted_at" => NULL, + ), + array( + "name" => "Matroshat", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"21.984237670898438\",\"y\":\"53.598978996276855\"},\"viewBox\":{\"height\":\"21.44988441467285\",\"width\":\"44.309085845947266\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 08:34:53", + "updated_at" => "2019-12-20 08:37:07", + "deleted_at" => NULL, + ), + array( + "name" => "Stort skæg", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"26.324413299560547\",\"y\":\"39.17325973510742\"},\"viewBox\":{\"height\":\"54.999916076660156\",\"width\":\"52.93963623046875\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-12-20 08:25:35", + "updated_at" => "2019-12-20 08:31:48", + "deleted_at" => NULL, + ), + array( + "name" => "Blue Anime eyes", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"24.639999389648438\",\"y\":\"6.452561974525452\"},\"viewBox\":{\"height\":\"11.775381803512573\",\"width\":\"49.290000915527344\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => "2019-11-19 13:10:34", + "deleted_at" => NULL, + ), + array( + "name" => "Green headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:08:24", + "updated_at" => "2019-11-19 13:09:07", + "deleted_at" => NULL, + ), + array( + "name" => "Orange headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:06:32", + "updated_at" => "2019-11-19 13:07:12", + "deleted_at" => NULL, + ), + array( + "name" => "Brun sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:05:31", + "updated_at" => "2019-11-19 13:05:45", + "deleted_at" => NULL, + ), + array( + "name" => "Orange sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:05:00", + "updated_at" => "2019-11-19 13:05:00", + "deleted_at" => NULL, + ), + array( + "name" => "Lille sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:04:25", + "updated_at" => "2019-11-19 13:04:25", + "deleted_at" => NULL, + ), + array( + "name" => "Blå sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:03:52", + "updated_at" => "2019-11-19 13:03:52", + "deleted_at" => NULL, + ), + array( + "name" => "Grøn sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 13:03:08", + "updated_at" => "2019-11-19 13:03:28", + "deleted_at" => NULL, + ), + array( + "name" => "Rød sløjfe", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"29.203932762145996\",\"y\":\"49.94792127609253\"},\"viewBox\":{\"height\":\"29.68931293487549\",\"width\":\"58.38446617126465\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:25:24", + "updated_at" => "2019-11-19 13:02:43", + "deleted_at" => NULL, + ), + array( + "name" => "Solbriller - Grønne", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"13.035617351531982\"},\"viewBox\":{\"height\":\"24.27074146270752\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 12:53:17", + "updated_at" => "2019-11-19 12:53:37", + "deleted_at" => NULL, + ), + array( + "name" => "Solbriller - Pink", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"13.035617351531982\"},\"viewBox\":{\"height\":\"24.27074146270752\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:25:50", + "updated_at" => "2019-11-19 12:53:00", + "deleted_at" => NULL, + ), + array( + "name" => "Rød gasmaske", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"30.250000476837158\"},\"viewBox\":{\"height\":\"31.50000286102295\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-11-19 12:50:30", + "updated_at" => "2019-11-19 12:52:09", + "deleted_at" => NULL, + ), + array( + "name" => "Sort gasmaske", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"30.250000476837158\"},\"viewBox\":{\"height\":\"31.50000286102295\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => "2019-11-19 12:50:09", + "deleted_at" => NULL, + ), + array( + "name" => "Kysmund - rød/lilla", + "category" => "mouth", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"10.352499961853027\",\"y\":\"4.254509329795837\"},\"viewBox\":{\"height\":\"10.50541090965271\",\"width\":\"20.714999198913574\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:30:11", + "updated_at" => "2019-11-19 12:46:23", + "deleted_at" => NULL, + ), + array( + "name" => "Kysmund", + "category" => "mouth", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"10.352499961853027\",\"y\":\"4.254509329795837\"},\"viewBox\":{\"height\":\"10.50541090965271\",\"width\":\"20.714999198913574\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:23:16", + "updated_at" => "2019-11-19 12:46:02", + "deleted_at" => NULL, + ), + array( + "name" => "Solbriller - Blå", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"13.035617351531982\"},\"viewBox\":{\"height\":\"24.27074146270752\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:31:41", + "updated_at" => "2019-10-21 09:31:41", + "deleted_at" => NULL, + ), + array( + "name" => "Hula skirt", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"40.26044845581055\",\"y\":\"24.614999294281006\"},\"viewBox\":{\"height\":\"39.689995765686035\",\"width\":\"84.88853073120117\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:22:36", + "updated_at" => "2019-10-21 09:28:28", + "deleted_at" => NULL, + ), + array( + "name" => "Muslingeskal halskæde - detaljeret", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"27.233510494232178\"},\"viewBox\":{\"height\":\"25.401062965393066\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:24:58", + "updated_at" => "2019-10-21 09:24:58", + "deleted_at" => NULL, + ), + array( + "name" => "Muslingeskal halskæde", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.45000076293945\",\"y\":\"29.233510494232178\"},\"viewBox\":{\"height\":\"25.401062965393066\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:24:32", + "updated_at" => "2019-10-21 09:24:32", + "deleted_at" => NULL, + ), + array( + "name" => "Muslingeskal bælte", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"11.81308901309967\"},\"viewBox\":{\"height\":\"10.808393239974976\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => "2019-10-21 09:24:08", + "updated_at" => "2019-10-21 09:24:08", + "deleted_at" => NULL, + ), + array( + "name" => "Bandana", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"45.418304443359375\",\"y\":\"43.25\"},\"viewBox\":{\"height\":\"19.5\",\"width\":\"84.51396560668945\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Captains Hat", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"55.56769943237305\",\"y\":\"65.66130924224854\"},\"viewBox\":{\"height\":\"49.52557182312012\",\"width\":\"111.10901641845703\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Eye patch left", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"37.5\",\"y\":\"11.007500171661377\"},\"viewBox\":{\"height\":\"28.244999885559082\",\"width\":\"75\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Eye patch right", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":28.25},\"translate\":{\"x\":37.5,\"y\":11}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Green Anime eyes", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":49.290000915527344,\"height\":11.775381803512573},\"translate\":{\"x\":24.639999389648438,\"y\":6.452561974525452}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Scar", + "category" => "accessories", + "content" => " ", + "meta" => "{\"viewBox\":{\"width\":15.75,\"height\":13.5},\"translate\":{\"x\":21.125,\"y\":17.25}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Terminator eye", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":36.734999656677246,\"height\":33.6002082824707},\"translate\":{\"x\":12.122499942779541,\"y\":12.600034713745117}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Eye with right scar", + "category" => "eyes", + "content" => "\r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":23.205000400543213,\"height\":12.885000228881836},\"translate\":{\"x\":9.667500257492065,\"y\":5.947499990463257}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Eye with left scar", + "category" => "eyes", + "content" => "\r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"9.667500257492065\",\"y\":\"5.947499990463257\"},\"viewBox\":{\"height\":\"12.885000228881836\",\"width\":\"23.205000400543213\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Belt", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":9.135000228881836},\"translate\":{\"x\":37.5,\"y\":10.522500038146973}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Belt with gun", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":36.69711685180664},\"translate\":{\"x\":37.5,\"y\":26.61811637878418}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Belt with sword", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":79.49506759643555,\"height\":58.10576248168945},\"translate\":{\"x\":41.25588798522949,\"y\":29.681467056274414}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Legs", + "category" => "legs", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":21,\"height\":30},\"translate\":{\"x\":10.5,\"y\":5}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Robot eyes with red dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"25.994999885559082\",\"y\":\"7.049999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"51.989999771118164\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Red robot eyes without dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":51.989999771118164,\"height\":12.299999713897705},\"translate\":{\"x\":25.994999885559082,\"y\":7.049999952316284}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "White glowing eyes", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":46.40999794006348,\"height\":18},\"translate\":{\"x\":23.204999923706055,\"y\":8.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Antenna ears", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"51.47999954223633\",\"y\":\"34.40750026702881\"},\"viewBox\":{\"height\":\"14.445001602172852\",\"width\":\"102.95999908447266\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Robot Arms", + "category" => "arms", + "content" => "", + "meta" => "{\"translate\":{\"x\":\"49\",\"y\":\"7.875\"},\"viewBox\":{\"height\":\"20.25\",\"width\":\"102\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Metal plate", + "category" => "accessories", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":35.565001487731934,\"height\":26.85000228881836},\"translate\":{\"x\":3.3475003242492676,\"y\":22.375}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Eyes", + "category" => "eyes", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":18,\"height\":3},\"translate\":{\"x\":9,\"y\":1.5}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Speaker mouth", + "category" => "mouth", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"viewBox\":{\"width\":31.185001373291016,\"height\":9},\"translate\":{\"x\":15.592500686645508,\"y\":1.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Left Robot Eye", + "category" => "eyes", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":21,\"height\":9},\"translate\":{\"x\":9.5,\"y\":4.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Right Robot Eye", + "category" => "eyes", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":21,\"height\":9},\"translate\":{\"x\":11.5,\"y\":4.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Smile", + "category" => "mouth", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":15,\"height\":3.75},\"translate\":{\"x\":7.5,\"y\":0.625}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Mouth", + "category" => "mouth", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":15,\"height\":2},\"translate\":{\"x\":7.5,\"y\":1}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Vampire Mouth", + "category" => "mouth", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":15,\"height\":3},\"translate\":{\"x\":7.5,\"y\":0.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #fffbb8", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #1cd9e8", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #0eb2bf", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #058ec1", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #5bb2d0", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #fff65b", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #0494f3", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #00AEEF", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #acaeaf", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #5492a9", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #c55497", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #ccc432", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #848484", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #b7e3f9", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #6bd7ff", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #ff35ad", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #EC008B", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #ffcf00", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #d8b51d", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #f9d742", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #ff81cc", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #85a0ab", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #FFF100", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #f71094", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #c1bb4d", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Body - #e63ca1", + "category" => "body", + "content" => "", + "meta" => "{\"viewBox\":{\"width\":75,\"height\":75},\"translate\":{\"x\":12.5,\"y\":12.5}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Red captain hat", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"55.56769943237305\",\"y\":\"65.66130924224854\"},\"viewBox\":{\"height\":\"49.52557182312012\",\"width\":\"111.10901641845703\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Røde anime øjne", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"24.639999389648438\",\"y\":\"6.452561974525452\"},\"viewBox\":{\"height\":\"11.775381803512573\",\"width\":\"49.290000915527344\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Robot eyes with yellow dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"25.994999885559082\",\"y\":\"7.049999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"51.989999771118164\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Robot eyes with blue dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"25.994999885559082\",\"y\":\"7.049999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"51.989999771118164\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Yellow robot eyes without dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"25.994999885559082\",\"y\":\"7.049999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"51.989999771118164\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Blue robot eyes without dots", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"25.994999885559082\",\"y\":\"7.049999952316284\"},\"viewBox\":{\"height\":\"12.299999713897705\",\"width\":\"51.989999771118164\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Standard arme", + "category" => "arms", + "content" => "\r\n \r\n \r\n", + "meta" => "{\"translate\":{\"x\":\"49.67262268066406\",\"y\":\"4.543535631150007\"},\"viewBox\":{\"height\":\"27.160268783569336\",\"width\":\"99.9411392211914\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Intet tilbehør", + "category" => "accessories", + "content" => "", + "meta" => "{\"translate\":{\"x\":\"0\",\"y\":\"0\"},\"viewBox\":{\"height\":\"0\",\"width\":\"0\"}}", + "is_public" => 1, + "is_default" => 1, + "is_featured" => 1, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Krabbe", + "category" => "hat", + "content" => "\r\n \r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t", + "meta" => "{\"translate\":{\"x\":\"-3.863445281982422\",\"y\":\"61.50937366485596\"},\"viewBox\":{\"height\":\"31.25624656677246\",\"width\":\"33.63622283935547\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Brown captain hat", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"55.56769943237305\",\"y\":\"65.66130924224854\"},\"viewBox\":{\"height\":\"49.52557182312012\",\"width\":\"111.10901641845703\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Light headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Red headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Blue headphones", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"46.78000068664551\",\"y\":\"55.532562255859375\"},\"viewBox\":{\"height\":\"75.19537353515625\",\"width\":\"93.47999954223633\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Yellow bandana", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"45.418304443359375\",\"y\":\"43.25\"},\"viewBox\":{\"height\":\"19.5\",\"width\":\"84.51396560668945\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Red bandana", + "category" => "hat", + "content" => "\r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"45.418304443359375\",\"y\":\"43.25\"},\"viewBox\":{\"height\":\"19.5\",\"width\":\"84.51396560668945\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + array( + "name" => "Cyclops eye", + "category" => "eyes", + "content" => "\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ", + "meta" => "{\"translate\":{\"x\":\"9\",\"y\":\"14\"},\"viewBox\":{\"height\":\"18\",\"width\":\"18\"}}", + "is_public" => 1, + "is_default" => 0, + "is_featured" => 0, + "status" => "published", + "created_at" => NULL, + "updated_at" => NULL, + "deleted_at" => NULL, + ), + ); + + + return $avatar_items; + } + + +} + diff --git a/database/seeds/CourseCategorySeeder.php b/database/seeds/CourseCategorySeeder.php new file mode 100644 index 0000000..6e9fde7 --- /dev/null +++ b/database/seeds/CourseCategorySeeder.php @@ -0,0 +1,76 @@ +create([ + 'title' => 'Scratch', + 'description' => 'Scratch er et gratis værktøj, som du kan bruge til at programmere dine egne interaktive historier, spil og tegnefilm. Hvis du aldrig har prøvet at programmere før, er Scratch et rigtig godt sted at starte, da Scratch er meget begyndervenligt.', + 'color' => '#f9d12b', + 'logo_id' => function (array $category) { + return File::createFromStorage('public', 'courses/logos/scratch.png')->id; + }, + 'thumbnail_id' => function (array $category) { + return File::createFromStorage('public', 'courses/course-placeholder.png')->id; + }, + ]); + + factory(\App\Models\Course\CourseCategory::class, 1)->create([ + 'title' => 'Javascript via p5.js', + 'description' => 'p5.js er et JavaScript-bibliotek, der gør kodning let og tilgængelig for børn og unge, der interesserer sig for at kodning inden for design.', + 'color' => '#F7DF1E', + 'logo_id' => function (array $category) { + return File::createFromStorage('public', 'courses/logos/javascript.svg')->id; + }, + 'thumbnail_id' => function (array $category) { + return File::createFromStorage('public', 'courses/course-placeholder2.png')->id; + }, + ]); + + factory(\App\Models\Course\CourseCategory::class, 1)->create([ + 'title' => 'Processing.py', + 'color' => '#006673', + 'active' => false, + 'logo_id' => function (array $category) { + return File::createFromStorage('public', 'courses/logos/processing.png')->id; + }, + 'thumbnail_id' => function (array $category) { + return File::createFromStorage('public', 'courses/course-placeholder2.png')->id; + }, + ]); + + factory(\App\Models\Course\CourseCategory::class, 1)->create([ + 'title' => 'HTML/CSS', + 'description' => 'HTML og CSS er de to kode-sprog, som ligger bag enhver moderne hjemmeside. HTML og CSS sørger for, at tekst, billeder, design osv. ser flot ud i internetbrowseren.', + 'color' => '#007b51', + 'logo_id' => function (array $category) { + return File::createFromStorage('public', 'courses/logos/html-css.svg')->id; + }, + 'thumbnail_id' => function (array $category) { + return File::createFromStorage('public', 'courses/course-placeholder.png')->id; + }, + ]); + + factory(\App\Models\Course\CourseCategory::class, 1)->create([ + 'title' => 'Sonic Pi', + 'color' => '#00268a', + 'active' => false, + 'logo_id' => function (array $category) { + return File::createFromStorage('public', 'courses/logos/sonic-pi.svg')->id; + }, + 'thumbnail_id' => function (array $category) { + return File::createFromStorage('public', 'courses/course-placeholder2.png')->id; + }, + ]); + + } +} diff --git a/database/seeds/CourseDummySeeder.php b/database/seeds/CourseDummySeeder.php new file mode 100644 index 0000000..e4199b4 --- /dev/null +++ b/database/seeds/CourseDummySeeder.php @@ -0,0 +1,341 @@ +level = 0; + $c->title = 'Intro til Scratch'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'Formålet med dette dyk er bare at vise de helt basale scratch komponenter og få børnene sat op.'; + $c->category_id = 1; + $c->position = 1; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Kom godt i gang","link":"Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'text'; + $crc->meta = ["text" =>"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus varius euismod odio et suscipit. Nulla justo augue, auctor vel elit non, ultrices vulputate libero. In auctor turpis at metus ultrices, ac commodo tellus interdum. Aliquam pretium commodo rhoncus. Fusce maximus imperdiet laoreet. Duis vel dapibus nisl, a suscipit tortor. Aliquam erat volutpat. + +Cras eu suscipit sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi eget pretium risus. Vestibulum convallis diam vitae sem malesuada, nec aliquet turpis fermentum. Suspendisse ante nisi, maximus nec neque id, pharetra tincidunt sem. Pellentesque nisl ante, auctor sit amet vulputate a, condimentum non velit. Sed a iaculis enim. Morbi id enim vestibulum, tempor sapien at, suscipit nunc. + +Nullam rutrum justo purus, eget suscipit enim tempus ac. Nunc porttitor aliquam massa, at aliquam nibh. Sed fringilla tortor tortor. Mauris molestie purus sit amet lectus malesuada, sed vulputate diam venenatis. Nullam ultricies nunc sed diam rutrum tincidunt. Nulla at blandit mauris, ac fermentum purus. Mauris varius aliquam sagittis. Fusce commodo mi nulla, a elementum metus placerat id. + +Donec leo felis, laoreet egestas sollicitudin eget, volutpat vitae quam. Integer volutpat fringilla orci, ac molestie arcu posuere ut. Maecenas tempor, nisl vel vulputate efficitur, elit orci consectetur leo, at pulvinar turpis ligula nec metus. Phasellus sed imperdiet nibh. Donec nulla dolor, facilisis ac porttitor id, semper in odio. Ut vel felis augue. Phasellus vel tellus egestas, sollicitudin risus vitae, lacinia velit. Mauris elementum pharetra commodo. Nulla ac aliquam nulla. Fusce efficitur molestie pretium. Quisque laoreet nec quam ac condimentum. + +Donec tincidunt auctor dapibus. Vestibulum eu ultrices purus. Suspendisse nunc nibh, commodo vitae fermentum eu, molestie vel arcu. Aliquam in tempor nisl. Suspendisse lectus ante, blandit quis varius non, scelerisque at est. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur volutpat sem quis consequat condimentum. Duis nec lorem id sem tincidunt scelerisque quis nec enim. Vestibulum convallis turpis id erat ultricies sollicitudin. Ut ullamcorper, quam vitae rutrum eleifend, lacus lectus semper nibh, aliquam interdum ipsum purus a dui. Ut in mollis dolor, sed interdum nisl. Curabitur metus ante, fermentum ut maximus ut, tempor ac urna. Maecenas aliquam dolor sed fermentum laoreet. Pellentesque vitae tincidunt eros. Morbi vulputate eget dui quis accumsan."]; + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gem projekter","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Udforsk og del projekter","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + + + + + $c = new Course(); + $c->level = 0; + $c->title = 'Tegn med Scratch'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'I dette dyk laver vi en tegning, hvor vi styrer spritens bevægelse med musen og dens størrelse med vores stemme. + +Dette dyk er ment til at være en relativt simpel øvelse, der introducerer børnene til nogle af de grundlæggende komponenter (bevægelse, løkker, udseende, kostumer) og demonstrerer den fleksibilitet og kreativitet Scratch tilbyder.'; + $c->category_id = 1; + $c->position = 2; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få spriten til at følge musen","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Skift spritens udseende: farve og kostume","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Pen udvidelse: Stempel/slet","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Ændr spritens størrelse med din stemme","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 5; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Lav dit eget kostume","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 6; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gør det til dit eget","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + + + + + $c = new Course(); + $c->level = 1; + $c->title = 'Samlespil'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'I dette dyk laver vi et spil, hvor en fugl flyver på tværs af skærmen igen og igen, og prøver at fange krystaller, der flytter sig rundt. + +Dette dyk er tænkt som en sjov og simpel øvelse, der introducerer nogle fundamentale koncepter: bevægelse og koordinater, hvis-brikken, berøring, og variabler til scores.'; + $c->category_id = 1; + $c->position = 3; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få fuglen til at flyve","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Fang krystaller","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Score","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gør det til dit eget","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 5; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Udvidelser","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + + + + + $c = new Course(); + $c->level = 1; + $c->title = 'Tegnefilm'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'I dette dyk laver vi en tegnefilm om en dreng og hans frø, der kommer på en mystisk tur, efter de finder en tryllestav. Det er meningen, at børnene kan fortsætte historien. + +Dette dyk viser, hvordan Scratch kan bruges til at lave film og interaktioner og dækker nogle sjove og brugbare komponenter til den slags: snak sammen, send/modtag beskeder, lav brikker, lav dine egne kostumer, reager på baggrundsskift, og visuelle effekter.'; + $c->category_id = 1; + $c->position = 4; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få sprites til at snakke sammen","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Animer en sprite","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Lav dine egne blokke","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Vilde effekter","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 5; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gør det til dit eget","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + + + + + $c = new Course(); + $c->level = 2; + $c->title = 'Gribespil'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'I dette spil bruger vi musen til at styre en skål, for at fange jordbær, der falder fra himlen. Spillet ender når vi har misset 3 jordbær eller, hvis vi fanger en af de pindsvin, der også falder fra himlen. + + Dette dyk viser både velkendte og nye anvendelser af viden fra tidliger dyk. Vi genbruger i høj grad bevægelse, berøring og score-variabel fra samlespillet og begivenheder fra tegnefilmen, men anvender også principperne i en ny sammenhæng, fx med en ekstra, farlig sprite og en variabel til at holde styr på liv.'; + $c->category_id = 1; + $c->position = 5; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få jordbær til at falde ned fra himlen","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få skålen til at følge musen","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Tilføj en farlig sprite","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Tilføj liv","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 5; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Game over!","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 6; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gør det til dit eget","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + + + + + $c = new Course(); + $c->level = 2; + $c->title = 'Multi-player Pong'; + $c->slug = str_replace(' ', '_', strtolower($c->title)); + $c->description = 'Dette spil er det gode gamle multi-player pong spil. + +Dette dyk fokuserer ikke på at introducere nye koncepter, men viser i stedet en lidt mere avanceret brug af mange af de koncepter, de kender på dette tidspunkt. Det er også håbet, at det er sjovt (og øjenåbnende) for dem at lave et spil, de kan spille med deres venner eller familie. Og startskærmen er et generelt koncept, de kan bruge på mange sjove måder i deres andre spil.'; + $c->category_id = 1; + $c->position = 6; + $c->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 1; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Lav en hoppebold","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 2; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Indsæt paddle (1 spiller)","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 3; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Få bolden til at ramme paddlen","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 4; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Hold score","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 5; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Tilføj spiller 2","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 6; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Lav en startskærm","link":"https:\/\/www.youtube.com\/watch?v=Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + $crc = new CourseResource(); + $crc->course_id = $c->id; + $crc->position = 7; + $crc->type = 'video'; + $crc->meta = json_decode('{"title":"Gør det til dig eget","link":"Y7dpJ0oseIA","type":"youtube"}'); + $crc->save(); + + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 0000000..f4ae27a --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,43 @@ +call([ + PermissionSeeder::class, + AvatarItemsTableSeeder::class, + ZipcodeSeeder::class, + CourseCategorySeeder::class, + ForumCategorySeeder::class, + ProjectCategorySeeder::class, + AnimatedTickerTextSeeder::class, + TwitchChannelsSeeder::class, + UsersTableSeeder::class, + ]); + + if(app()->isLocal()) { + $this->call([ + DummyRegionSeeder::class, + RewardsSeeder::class, + DummyAchievementsSeeder::class, + DummyPiratesSeeder::class, + demoNewsSeeder::class, + ForumTestSeeder::class, + CourseDummySeeder::class, + EventSeeder::class, + DummyProjectSeeder::class, + ]); + } + + } +} diff --git a/database/seeds/DummyAchievementsSeeder.php b/database/seeds/DummyAchievementsSeeder.php new file mode 100644 index 0000000..624b780 --- /dev/null +++ b/database/seeds/DummyAchievementsSeeder.php @@ -0,0 +1,17 @@ +create(); + } +} diff --git a/database/seeds/DummyPiratesSeeder.php b/database/seeds/DummyPiratesSeeder.php new file mode 100644 index 0000000..deb147a --- /dev/null +++ b/database/seeds/DummyPiratesSeeder.php @@ -0,0 +1,12 @@ +states('pirate', 'dummy')->create(); + } +} diff --git a/database/seeds/DummyProjectSeeder.php b/database/seeds/DummyProjectSeeder.php new file mode 100644 index 0000000..d57f24a --- /dev/null +++ b/database/seeds/DummyProjectSeeder.php @@ -0,0 +1,18 @@ +create(); + } +} diff --git a/database/seeds/DummyRegionSeeder.php b/database/seeds/DummyRegionSeeder.php new file mode 100644 index 0000000..0599b20 --- /dev/null +++ b/database/seeds/DummyRegionSeeder.php @@ -0,0 +1,17 @@ +create(); + } +} diff --git a/database/seeds/EventSeeder.php b/database/seeds/EventSeeder.php new file mode 100644 index 0000000..f5ffede --- /dev/null +++ b/database/seeds/EventSeeder.php @@ -0,0 +1,17 @@ +create(); + } +} diff --git a/database/seeds/ForumCategorySeeder.php b/database/seeds/ForumCategorySeeder.php new file mode 100644 index 0000000..e7b9246 --- /dev/null +++ b/database/seeds/ForumCategorySeeder.php @@ -0,0 +1,68 @@ + [ + "Annonceringer" => 'Her kan du læse spændende nyheder og se de nyeste events.', + "Er du ny Pirat?" => "Velkommen til Piratskibet. Vi er glade for at have dig med ombord! Her kan du finde en masse information om, hvordan du kommer godt i gang.", + ], + "Spørgsmål til de IT-kreative forløb" => [ + "Scratch" => "Her kan du stille spørgsmål til Kodehavet-forløbet om Scratch.", + "Webprogrammering" => "Her kan du stille spørgsmål til Kodehavet-forløbet om webprogrammering.", + "Javascript via p5.js" => "Her kan du stille spørgsmål til Kodehavet-forløbet om Javascript." + ], + "Spørgsmål til teknologi" => [ + "Spildesign" => "Kunne du tænke dig at lave dit helt eget spil, eller er du i fuld gang, men sidder med et spørgsmål? Så er det her, du kan stille spørgsmål eller fremvise dit projekt.", + "Video og musik" => "Arbejder du med musik og/eller video, eller vil du gerne? Så stil dine spørgsmål her.", + "Webudvikling" => "Kunne du tænke dig at lave din egen hjemmeside, eller er du allerede i gang og har spørgsmål? Så kan du starte en chat, hvor du kan stille spørgsmål eller vise dit projekt frem.", + "Apps" => "Har du også fået øjnene op for AppInventor, eller laver du apps på en anden måde? Så kan du spørge om hjælp her, eller bare vise din nye app frem!", + "Hardware" => "Sidder du og bakser med Arduino, Raspberry Pie, LEGO eller anden fysisk elektronik, så kan du stille spørgsmål eller fremvise dit projekt her.", + ], + "Andet" => [ + "Hyggesnak" => "Her kan du chatte om alle mulige ting: lige fra spændende projekter til hvilke gode film, der går i biografen.", + "Bugs, glitches eller andre fejl på Piratskibet" => "Oplever du fejl på Piratskibet, så skriv dem her, så vi kan lappe skuden og få Piratskibet til at sejle for fulde sejl igen.", + ], + ]; + + $faker = Faker::create(); + + foreach ($structure as $category => $children) + { + $cat = new Topic(); + $cat->name = $category; + $cat->slug = ForumCategorySeeder::createSlug($category, '_'); + + $cat->save(); + + foreach ($children as $topic => $description) + { + $child = new Topic(); + $child->name = $topic; + $child->parent_id = $cat->id; + $child->slug = ForumCategorySeeder::createSlug($topic, '_'); + $child->description = $description; + + $child->save(); + } + + } + } + + public static function createSlug($str, $delimiter = '-'){ + + $slug = strtolower(trim(preg_replace('/[\s-]+/', $delimiter, preg_replace('/[^A-Za-z0-9-]+/', $delimiter, preg_replace('/[&]/', 'and', preg_replace('/[\']/', '', iconv('UTF-8', 'ASCII//TRANSLIT', $str))))), $delimiter)); + return $slug; + + } +} diff --git a/database/seeds/ForumTestSeeder.php b/database/seeds/ForumTestSeeder.php new file mode 100644 index 0000000..4cdf050 --- /dev/null +++ b/database/seeds/ForumTestSeeder.php @@ -0,0 +1,72 @@ +get(); + + $pirates = User::query()->role('pirate')->get()->keyBy('id'); + + if ($cats) { + foreach ($cats as $category) { + + $user = $pirates->random(); + + $thread_data = [ + 'created_by' => $user->id, + 'topic_id' => $category->id, + 'created_at' => \Carbon\Carbon::now()->subDays(rand(1, 20)) + ]; + + $number_of_threads = rand(1, 3); + if($category->id === 1) { + $number_of_threads = 1; + } + + factory(Thread::class, $number_of_threads) + ->create($thread_data) + ->each(function (Thread $thread) use($user, $pirates) { + + + + $message = $thread->createMessage( + (($thread->id === 1)? + "

Aut nisi consectetur cumque eligendi. Sit possimus soluta ea ab sed esse officia. Provident ipsa qui repudiandae repudiandae sapiente sed voluptatem.




" + :'In vel vehicula orci. Praesent vulputate pulvinar ante, a feugiat felis molestie non. Ut at magna consequat, luctus orci a, faucibus nulla. + Duis metus velit, ullamcorper sed mauris eu, fermentum porta ante'), + $user, [ + 'created_at' => $thread->created_at + ]); + + $thread->original_message_id = $message->id; + $thread->save(); + + $number_of_messages = rand(1, 5); + if($thread->id === 1) { + $number_of_messages = 100; + } + + factory(Message::class, $number_of_messages)->create(['thread_id' => $thread->id])->each(function (Message $message) use ($pirates) { + $pirates->random(random_int(0, $pirates->count() * 0.3))->each(function (User $user) use ($message) { + $message->triggerReaction('like', true, $user); + }); + }); + + $thread->updateMostPopularAnswer(true); + }); + + + } + + return true; + } + + } +} diff --git a/database/seeds/ProjectCategorySeeder.php b/database/seeds/ProjectCategorySeeder.php new file mode 100644 index 0000000..a0d09c3 --- /dev/null +++ b/database/seeds/ProjectCategorySeeder.php @@ -0,0 +1,176 @@ +parents as $parent) { + $id = Category::create($parent)->id; + $children = $this->getChildren($parent['slug'], $id); + + if (!empty($children)) { + \DB::table('project_categories')->insert($children); + } + } + + } + + protected function getChildren(string $slug, int $id) + { + $children = Arr::get($this->children, $slug, []); + + return array_map(function ($child) use ($id) { + $child['parent_id'] = $id; + $child['created_at'] = now(); + $child['updated_at'] = now(); + + return $child; + }, $children); + } + + protected $parents = [ + [ + 'name' => 'Spil', + 'slug' => 'spil', + ], + [ + 'name' => 'Web', + 'slug' => 'web', + ], + [ + 'name' => 'Grafik', + 'slug' => 'grafik', + ], + [ + 'name' => 'Apps', + 'slug' => 'apps', + ], + [ + 'name' => 'Hardware', + 'slug' => 'hardware', + ], + [ + 'name' => 'Robot', + 'slug' => 'robot', + ], + [ + 'name' => 'Hacks', + 'slug' => 'hacks', + ], + [ + 'name' => 'Lyd', + 'slug' => 'lyd', + ], + [ + 'name' => 'Film', + 'slug' => 'film', + ], + ]; + + protected $children = [ + 'spil' => [ + [ + 'name' => '2D', + 'slug' => 'spil_2d', + ], + [ + 'name' => '3D', + 'slug' => 'spil_3d', + ], + [ + 'name' => 'VR', + 'slug' => 'spil_vr', + ], + [ + 'name' => 'Andet', + 'slug' => 'spil_andet', + ], + ], + 'web' => [ + [ + 'name' => 'Front-end / client', + 'slug' => 'web_frontend', + ], + [ + 'name' => 'Backend / server', + 'slug' => 'web_backend', + ], + [ + 'name' => 'Andet', + 'slug' => 'web_andet', + ], + ], + 'grafik' => [ + [ + 'name' => '2D', + 'slug' => 'grafik_2d', + ], + [ + 'name' => '3D', + 'slug' => 'grafik_3d', + ], + [ + 'name' => 'Pixelart', + 'slug' => 'grafik_pixelart', + ], + [ + 'name' => 'Vektor', + 'slug' => 'grafik_vektor', + ], + [ + 'name' => 'Andet', + 'slug' => 'grafik_andet', + ], + ], + 'apps' => [ + [ + 'name' => 'Windows', + 'slug' => 'apps_windows', + ], + [ + 'name' => 'iOs / MacOS', + 'slug' => 'apps_apple', + ], + [ + 'name' => 'Linux', + 'slug' => 'apps_linux', + ], + [ + 'name' => 'Andet', + 'slug' => 'apps_andet', + ], + ], + 'hardware' => [ + [ + 'name' => 'Arduino', + 'slug' => 'hardware_arduino', + ], + [ + 'name' => 'LEGO', + 'slug' => 'hardware_lego', + ], + [ + 'name' => 'Raspberry Pi', + 'slug' => 'hardware_raspberry_pi', + ], + [ + 'name' => 'Micro:bit / Ultra:bit', + 'slug' => 'hardware_microbit', + ], + [ + 'name' => 'Andet', + 'slug' => 'hardware_andet', + ], + ], + ]; +} diff --git a/database/seeds/RewardsSeeder.php b/database/seeds/RewardsSeeder.php new file mode 100644 index 0000000..b4c16ef --- /dev/null +++ b/database/seeds/RewardsSeeder.php @@ -0,0 +1,17 @@ +create(); + } +} diff --git a/database/seeds/TwitchChannelsSeeder.php b/database/seeds/TwitchChannelsSeeder.php new file mode 100644 index 0000000..da029e1 --- /dev/null +++ b/database/seeds/TwitchChannelsSeeder.php @@ -0,0 +1,25 @@ + '4JjXzyeU2BXYKQ', + ]; + + foreach ($channels as $channel => $collection) { + $ticker = new \App\Models\Content\TwitchChannel(); + $ticker->channel_name = $channel; + $ticker->collection = $collection; + $ticker->save(); + } + + } +} diff --git a/database/seeds/UsersTableSeeder.php b/database/seeds/UsersTableSeeder.php new file mode 100644 index 0000000..3349fee --- /dev/null +++ b/database/seeds/UsersTableSeeder.php @@ -0,0 +1,36 @@ +states('admin')->create([ + 'username' => 'piratskibet@codingpirates.dk', + 'name' => 'Piratskibet admin', + 'email' => 'piratskibet@codingpirates.dk', + 'parent_email' => 'piratskibet@codingpirates.dk', + 'password' => bcrypt('piratskibet'), + 'birthday' => now()->subYears(25)->startOfDay(), + ]); + + factory(User::class, 1)->states('admin')->create([ + 'username' => 'admin@morningtrain.dk', + 'name' => 'Admin', + 'email' => 'admin@morningtrain.dk', + 'parent_email' => 'admin@morningtrain.dk', + 'password' => bcrypt('admin'), + 'birthday' => now()->subYears(25)->startOfDay(), + ]); + + if(app()->isLocal()) { + factory(User::class, 3) + ->states('predefined', 'pirate', 'all_avatar_items') + ->create(); + } + + } +} diff --git a/database/seeds/ZipcodeSeeder.php b/database/seeds/ZipcodeSeeder.php new file mode 100644 index 0000000..8b927fb --- /dev/null +++ b/database/seeds/ZipcodeSeeder.php @@ -0,0 +1,38 @@ + $line) { + $line = trim($line); + + if (empty($line)) { + continue; + } + + $line = explode(";", $line); + $zip = $line[4]; + + // Some zipcodes exist multiple times in the file, but we need uniques + $zipcodes[$zip] = [ + 'zipcode' => $zip, + 'city' => $line[5], + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + \App\Models\Regions\Zipcode::insert($zipcodes); + } +} diff --git a/database/seeds/demoNewsSeeder.php b/database/seeds/demoNewsSeeder.php new file mode 100644 index 0000000..cb9f6c4 --- /dev/null +++ b/database/seeds/demoNewsSeeder.php @@ -0,0 +1,87 @@ +title = 'Frivilligprojektet: Status på Frivilligmøder og opstartsguide'; + $news->subtext = '

2. maj 2019 Frivilligmøderne er i godt i gang, og vi kan allerede nu se nogen af de samme tendenser på tværs af afdelingerne. Ida er startet som projektmedarbejder, og hendes første opgave bliver en opstartsguide. God energi til Frivilligmøderne

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/05/Frivilligm%C3%B8de_Rentemestervej.png'; + $news->theme = 'yellow-pink'; + $news->link = 'https://codingpirates.dk/frivilligprojektet-status-paa-frivilligmoeder-og-opstartsguide/'; + $news->publish_at = \Carbon\Carbon::now(); + $news->status = 'published'; + $news->save(); + + //---------------------------------------------------------------------------------// + + $news = new \App\Models\News(); + $news->title = 'En dag på Sekretariatet i Coding Pirates'; + $news->subtext = '

Klokken er 8.27, og solen skinner over Odense Havn, da jeg mandag morgen i påskeugen kommer cyklende fra byen og ned mod Coding Pirates-kontoret på havnen.

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/04/P1000148-2-1024x675.jpg'; + $news->theme = 'pink-yellow'; + $news->link = 'https://codingpirates.dk/en-dag-paa-sekretariatet-i-coding-pirates/'; + $news->publish_at = \Carbon\Carbon::now(); + $news->status = 'published'; + $news->save(); + + //---------------------------------------------------------------------------------// + + $news = new \App\Models\News(); + $news->title = 'Ny besætning i cp billund (featured)'; + $news->subtext = '

Coding Pirates Billund har netop afholdt ordinær generalforsamling. En ny besætning har derfor afløst den tiligere besætning på sørøverskibet CP Billund.

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/04/cpbillund-bestyrelse-e1555592451893-1080x675.jpg'; + $news->featured = 1; + $news->theme = 'blue-yellow'; + $news->link = 'https://codingpirates.dk/ny-besaetning-i-cp-billund/'; + $news->publish_at = \Carbon\Carbon::now(); + $news->status = 'published'; + $news->save(); + + //---------------------------------------------------------------------------------// + + $news = new \App\Models\News(); + $news->title = 'Piraterne indtager Strynø'; + $news->subtext = '

Mikael Kian Hansen søgte Landdistriktspuljens ø-støtte, som støtter initiativer og projekter, der kan skabe udvikling og arbejdspladser på småøerne.

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/04/Piraterne_indtager_Stryn%C3%B8.png'; + $news->theme = 'yellow-pink'; + $news->link = 'https://codingpirates.dk/piraterne-indtager-strynoe/'; + $news->publish_at = \Carbon\Carbon::now(); + $news->status = 'published'; + $news->save(); + + //---------------------------------------------------------------------------------// + + $news = new \App\Models\News(); + $news->title = 'Piratskibet: Webbureauet Morning Train skal bygge Piratskibet'; + $news->subtext = '

Udviklingen af Piratskibet får nu for alvor vind i sejlene, idet vi kan annoncere Morning Train som webbureauet, der skal udvikle online platformen.

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/04/test-1080x675.jpg'; + $news->theme = 'grey-pink'; + $news->link = 'https://codingpirates.dk/webbureauet-morning-train-skal-bygge-piratskibet/'; + $news->publish_at = \Carbon\Carbon::now(); + $news->status = 'published'; + $news->save(); + + //---------------------------------------------------------------------------------// + + $news = new \App\Models\News(); + $news->title = 'unpublished'; + $news->subtext = '

Udviklingen af Piratskibet får nu for alvor vind i sejlene, idet vi kan annoncere Morning Train som webbureauet, der skal udvikle online platformen.

'; + $news->img = 'https://codingpirates.dk/wp-content/uploads/2019/04/test-1080x675.jpg'; + $news->theme = 'pink-black'; + $news->link = 'https://codingpirates.dk/webbureauet-morning-train-skal-bygge-piratskibet/'; + $news->publish_at = \Carbon\Carbon::now()->addDays(5); + $news->status = 'draft'; + $news->save(); + + } +} diff --git a/modules/mt-react-core/index.js b/modules/mt-react-core/index.js new file mode 100644 index 0000000..0d71ce6 --- /dev/null +++ b/modules/mt-react-core/index.js @@ -0,0 +1,5 @@ +module.exports = { + Providers: { + Context: require('./providers/Context') + } +} diff --git a/modules/mt-react-core/package.json b/modules/mt-react-core/package.json new file mode 100644 index 0000000..c764b2c --- /dev/null +++ b/modules/mt-react-core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@morningtrain/react-core", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/modules/mt-react-core/providers/Context.js b/modules/mt-react-core/providers/Context.js new file mode 100644 index 0000000..f44be20 --- /dev/null +++ b/modules/mt-react-core/providers/Context.js @@ -0,0 +1,34 @@ +const Provider = require("./Provider"); +const {get, observable} = require("mobx"); +const {createTransformer} = require("mobx-utils"); + +class Context extends Provider { + + + ///////////////////////////////// + // Helpers + ///////////////////////////////// + + get providerProps() { + return { + context: this + }; + } + + ///////////////////////////////// + // Constraints + ///////////////////////////////// + + @observable constraints = new Map(); + + setConstraint(key, value) { + this.constraints.set(key, value); + } + + getConstraint = createTransformer(key => { + return get(this.constraints, key); + }) + +} + +module.exports = Context; \ No newline at end of file diff --git a/modules/mt-react-core/providers/Provider.js b/modules/mt-react-core/providers/Provider.js new file mode 100644 index 0000000..bfe2f5e --- /dev/null +++ b/modules/mt-react-core/providers/Provider.js @@ -0,0 +1,58 @@ +const React = require('react') +const { Provider } = require('mobx-react') +const shortid = require('shortid') + +class BaseProvider extends React.Component { + constructor (props) { + super(props) + + this.provider_uuid = shortid.generate() + this.state = { uuid: shortid.generate() } + } + + updateUuid () { + if (this.props.frozen !== true) { + // console.log('Provider not frozen, updating', this); + this.setState({ uuid: shortid.generate() }) + } else { + // console.log('Provider frozen, dont update', this); + } + } + + /// ////////////////////////////// + // Statics + /// ////////////////////////////// + + static get propTypes () { + return {} + } + + static get defaultProps () { + return {} + } + + /// ////////////////////////////// + // Helpers + /// ////////////////////////////// + + get providerProps () { + return {} + } + + /// ////////////////////////////// + // Renderers + /// ////////////////////////////// + + render () { // new Error('provider props test'), + // console.log( this.provider_uuid, this.providerProps); + return ( + + <> + {this.props.children} + + + ) + } +} + +module.exports = BaseProvider diff --git a/modules/mt-react-polyfill/index.js b/modules/mt-react-polyfill/index.js new file mode 100644 index 0000000..0c045bb --- /dev/null +++ b/modules/mt-react-polyfill/index.js @@ -0,0 +1,28 @@ +require('es5-shim') +require('es6-shim') + +require('es7-shim/Array').shim() +require('es7-shim/Object').shim() +require('es7-shim/String').shim() + +if (typeof window.Symbol === 'undefined') { + require('es6-symbol/implement') +} + +if (typeof window.Proxy === 'undefined') { + // window.Proxy = require('proxy-polyfill/src/proxy'); +} + +if (typeof window.Promise === 'undefined' || typeof ((new (window.Promise)(() => 1)).finally) === 'undefined') { + window.Promise = require('es6-promise') +} + +require('url-search-params-polyfill') + +/// ////////////////////////////// +// NodeList forEach +/// ////////////////////////////// + +if (typeof NodeList.prototype.forEach !== 'function') { + NodeList.prototype.forEach = Array.prototype.forEach +} diff --git a/modules/mt-react-polyfill/package.json b/modules/mt-react-polyfill/package.json new file mode 100644 index 0000000..01ff3ab --- /dev/null +++ b/modules/mt-react-polyfill/package.json @@ -0,0 +1,16 @@ +{ + "name": "@morningtrain/react-polyfill", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "peerDependencies": { + "es5-shim": "*", + "es6-shim": "*", + "es7-shim": "*" + } +} diff --git a/modules/mt-react-spawner/Pool.js b/modules/mt-react-spawner/Pool.js new file mode 100644 index 0000000..b6f071a --- /dev/null +++ b/modules/mt-react-spawner/Pool.js @@ -0,0 +1,26 @@ +import React from "react"; +import {inject} from "@morningtrain/react-decorators"; + +@inject(["spawner"]) +class Pool extends React.Component { + + renderSpawns() { + + if (!this.props.spawner) { + return null; + } + + return this.props.spawner.getSpawns(); + } + + render() { + return ( + + {this.renderSpawns()} + + ); + } + +} + +export default Pool; diff --git a/modules/mt-react-spawner/Spawner.js b/modules/mt-react-spawner/Spawner.js new file mode 100644 index 0000000..78e527d --- /dev/null +++ b/modules/mt-react-spawner/Spawner.js @@ -0,0 +1,44 @@ +import React from "react"; +import {Provider} from "mobx-react"; +import Pool from "./Pool"; +import {observable} from "mobx"; + +export default class Spawner extends React.Component { + + @observable spawns = new Map(); + + constructor(props) { + super(props); + + this.spawn.bind(this); + + } + + getSpawns() { + if (this.spawns.has('component') && this.spawns.has('props')) { + return React.cloneElement( + this.spawns.get('component'), + this.spawns.get('props') + ); + } + } + + spawn(component, props = {}) { + this.spawns.clear(); + setTimeout(() => { + this.spawns.replace({component, props}); + }, 0) + } + + render() { + return ( + + + + {this.props.children} + + + ); + } + +} \ No newline at end of file diff --git a/modules/mt-react-spawner/index.js b/modules/mt-react-spawner/index.js new file mode 100644 index 0000000..1a4a8a5 --- /dev/null +++ b/modules/mt-react-spawner/index.js @@ -0,0 +1,2 @@ +export Spawner from "./Spawner"; +export Pool from "./Pool"; \ No newline at end of file diff --git a/modules/mt-react-spawner/package.json b/modules/mt-react-spawner/package.json new file mode 100644 index 0000000..4888c80 --- /dev/null +++ b/modules/mt-react-spawner/package.json @@ -0,0 +1,8 @@ +{ + "name": "@morningtrain/react-spawner", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/modules/mt-react-tabs/Tab.js b/modules/mt-react-tabs/Tab.js new file mode 100644 index 0000000..a39e7e8 --- /dev/null +++ b/modules/mt-react-tabs/Tab.js @@ -0,0 +1,29 @@ +import React from "react"; +import {inject} from "@morningtrain/react-decorators"; + +export default +@inject(["tabManager"]) +class Tab extends React.Component { + + constructor(props) { + super(props); + + props.tabManager.registerTab(props.slug, props.label); + } + + get isActive() { + const {tabManager, slug} = this.props; + + return tabManager.isActive(slug); + } + + render() { + if (!this.isActive) return null; + + return ( + + {this.props.children} + + ); + } +} diff --git a/modules/mt-react-tabs/TabSwitcher.js b/modules/mt-react-tabs/TabSwitcher.js new file mode 100644 index 0000000..8204e34 --- /dev/null +++ b/modules/mt-react-tabs/TabSwitcher.js @@ -0,0 +1,44 @@ +import React from "react"; +import {inject} from "@morningtrain/react-decorators"; + +export default +@inject(["tabManager"]) +class TabSwitcher extends React.Component { + + get tabs() { + return this.props.tabManager.tabs; + } + + getClasses(slug) { + return [ + this.props.tabManager.isActive(slug) ? 'active' : null, + 'mtt-tab' + ] + .filter(e => e) + .join(' '); + } + + switchTab(slug) { + this.props.tabManager.setActive(slug); + } + + renderTab(slug) { + return ( +
this.switchTab(slug)}> + {this.tabs.get(slug)} +
+ ); + } + + renderTabs() { + return Array.from(this.tabs.keys()).map(this.renderTab.bind(this)) + } + + render() { + return ( +
+ {this.renderTabs()} +
+ ); + } +} diff --git a/modules/mt-react-tabs/Tabs.js b/modules/mt-react-tabs/Tabs.js new file mode 100644 index 0000000..957a6dc --- /dev/null +++ b/modules/mt-react-tabs/Tabs.js @@ -0,0 +1,43 @@ +import React from "react"; +import {Provider} from "mobx-react"; +import {observable} from "mobx"; + +export default class Tabs extends React.Component { + + @observable tabs = new Map(); + @observable current_tab = this.props.defaultActive; + + constructor(props) { + super(props); + } + + isActive(slug) { + return this.current_tab === slug; + } + + setActive(slug) { + this.current_tab = slug; + } + + registerTab(slug, label) { + this.tabs.set(slug, label); + + if (this.current_tab === '') { + this.setActive(slug); + } + } + + render() { + return ( + + + {this.props.children} + + + ); + } +} + +Tabs.defaultProps = { + defaultActive: '', +}; diff --git a/modules/mt-react-tabs/index.js b/modules/mt-react-tabs/index.js new file mode 100644 index 0000000..9a2a74e --- /dev/null +++ b/modules/mt-react-tabs/index.js @@ -0,0 +1,3 @@ +export Tabs from "./Tabs"; +export Tab from "./Tab"; +export TabSwitcher from "./TabSwitcher"; diff --git a/modules/mt-react-tabs/package.json b/modules/mt-react-tabs/package.json new file mode 100644 index 0000000..d968b59 --- /dev/null +++ b/modules/mt-react-tabs/package.json @@ -0,0 +1,8 @@ +{ + "name": "@morningtrain/react-tabs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/modules/quill-tenor/format-tenor.js b/modules/quill-tenor/format-tenor.js new file mode 100644 index 0000000..684a862 --- /dev/null +++ b/modules/quill-tenor/format-tenor.js @@ -0,0 +1,47 @@ +import Quill from 'quill' + +const Embed = Quill.import('blots/embed') + +class TenorBlot extends Embed { + static create (value) { + const node = super.create() + if (typeof value === 'object') { + TenorBlot.buildSpan(value, node) + } else if (typeof value === 'string') { + const valueObj = value + if (valueObj) { + TenorBlot.buildSpan(valueObj, node) + } + } + + return node + } + + static value (node) { + return { src: node.dataset.src, id: node.dataset.id } + } + + static buildSpan (value, node) { + value.src = value.src || value.media[0].gif.url + + node.setAttribute('data-id', value.id) + node.setAttribute('data-src', value.src) + const tenorElement = document.createElement('img') + + tenorElement.setAttribute('src', value.src) + + node.appendChild(tenorElement) + } + + static parseUnicode (string) { + return string.split('-').map(str => parseInt(str, 16)) + } +} + +TenorBlot.blotName = 'tenor' +TenorBlot.className = 'ql-tenorblot' +TenorBlot.tagName = 'span' +TenorBlot.emojiClass = 'ap' +TenorBlot.emojiPrefix = 'ap-' + +export default TenorBlot diff --git a/modules/quill-tenor/index.js b/modules/quill-tenor/index.js new file mode 100644 index 0000000..e5a211f --- /dev/null +++ b/modules/quill-tenor/index.js @@ -0,0 +1,10 @@ +import Quill from 'quill' +import FormatTenor from './format-tenor' +import ToolbarTenor from './module-toolbar-tenor' + +Quill.register({ + 'formats/tenor': FormatTenor, + 'modules/tenor-toolbar': ToolbarTenor +}, false) + +export default { ToolbarTenor, FormatTenor } diff --git a/modules/quill-tenor/module-toolbar-tenor.js b/modules/quill-tenor/module-toolbar-tenor.js new file mode 100644 index 0000000..6d32bf5 --- /dev/null +++ b/modules/quill-tenor/module-toolbar-tenor.js @@ -0,0 +1,157 @@ +import Quill from 'quill' +import ReactDOM from 'react-dom' +import ReactTenor from 'react-tenor' +import 'react-tenor/dist/styles.css' + +const Delta = Quill.import('delta') +const Module = Quill.import('core/module') +let token = null +const tenor = null +let tenorOptions = {} + +class ToolbarTenor extends Module { + constructor (quill, options) { + super(quill, options) + + this.quill = quill + this.toolbar = quill.getModule('toolbar') + tenorOptions = options + + token = options.apiKey + + if (typeof this.toolbar !== 'undefined') { this.toolbar.addHandler('tenor', this.checkPaletteExist) } + + const tenorBtns = document.getElementsByClassName('ql-tenor') + + if (tenorBtns) { + [].slice.call(tenorBtns).forEach(function (tenorBtns) { + tenorBtns.innerHTML = options.buttonIcon + }) + } + } + + checkPaletteExist () { + const quill = this.quill + fn_checkDialogOpen(quill) + this.quill.on('text-change', function (delta, oldDelta, source) { + if (source === 'user') { + fn_close() + fn_updateRange(quill) + } + }) + } +} + +ToolbarTenor.DEFAULTS = { + buttonIcon: '\n' + + '\n' + + '\t\t\n' + + '\t\t\t\n' + + '\t\t\t\n' + + '\t\t\t\n' + + '\t\t\n' + + '\n' +} + +function fn_close () { + const ele_emoji_plate = document.getElementById('tenor-palette') + document.getElementById('tenor-close-div').style.display = 'none' + if (ele_emoji_plate) { ele_emoji_plate.remove() } +} + +function fn_checkDialogOpen (quill) { + const elementExists = document.getElementById('tenor-palette') + if (elementExists) { + elementExists.remove() + } else { + fn_showTenorPalatte(quill) + } +} + +function fn_updateRange (quill) { + const range = quill.getSelection() + return range +} + +function fn_showTenorPalatte (quill) { + const ele_area = document.createElement('div') + const toolbar_container = document.querySelector('.ql-toolbar') + + const range = quill.getSelection() + const atSignBounds = quill.getBounds(range.index) + + quill.container.appendChild(ele_area) + + ele_area.id = 'tenor-palette' + ele_area.style.top = '0px' + ele_area.style.left = '15px' + ele_area.style.position = 'absolute' + ele_area.style.zIndex = 999 + + const tabToolbar = document.createElement('div') + tabToolbar.id = 'tab-toolbar' + ele_area.appendChild(tabToolbar) + + // panel + const panel = document.createElement('div') + panel.id = 'tab-panel' + ele_area.appendChild(panel) + const tabElementHolder = document.createElement('ul') + tabToolbar.appendChild(tabElementHolder) + + if (document.getElementById('tenor-close-div') === null) { + const closeDiv = document.createElement('div') + closeDiv.id = 'tenor-close-div' + closeDiv.setAttribute('style', 'width: 100%; height: 100%; position: fixed; top: 0; left: 0;') + closeDiv.addEventListener('click', fn_close, false) + document.getElementsByTagName('body')[0].appendChild(closeDiv) + } else { + document.getElementById('tenor-close-div').style.display = 'block' + } + + const props = { + token: token, + autoFocus: true, + contentFilter: tenorOptions.ContentFilter || 'off', + mediaFilter: tenorOptions.MediaFilter || 'minimal', + locale: tenorOptions.Locale || 'en_US', + initialSearch: tenorOptions.InitialSearch || 'cat', + searchPlaceholder: tenorOptions.SearchPlaceholder || 'Search Tenor', + onSelect: result => { + fn_embedTenorResult(quill, result) + } + } + + ReactDOM.render((), ele_area) + + fn_tenorPanelInit(panel, quill) +} + +function fn_embedTenorResult (quill, result) { + quill.focus() + const range = fn_updateRange(quill) + + quill.insertEmbed(range.index, 'tenor', result, Quill.sources.USER) + setTimeout(() => quill.setSelection(range.index + 1), 0) + fn_close() +} + +function fn_tenorPanelInit (panel, quill) { + document.querySelector('.filter-people').classList.add('active') +} + +function makeElement (tag, attrs, ...children) { + const elem = document.createElement(tag) + Object.keys(attrs).forEach(key => elem[key] = attrs[key]) + children.forEach(child => { + if (typeof child === 'string') { child = document.createTextNode(child) } + elem.appendChild(child) + }) + return elem +} + +export default ToolbarTenor diff --git a/modules/quill-tenor/package.json b/modules/quill-tenor/package.json new file mode 100644 index 0000000..62c8407 --- /dev/null +++ b/modules/quill-tenor/package.json @@ -0,0 +1,8 @@ +{ + "name": "quill-tenor", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/modules/react-twitch/Embed.js b/modules/react-twitch/Embed.js new file mode 100644 index 0000000..8b04cf1 --- /dev/null +++ b/modules/react-twitch/Embed.js @@ -0,0 +1,78 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default class Embed extends React.Component { + static get propTypes () { + return { + allowfullscreen: PropTypes.bool + } + } + + static get defaultProps () { + return { + width: '100%', + height: 480, + allowfullscreen: true, + channel: null, + video: null, + collection: null, + autoplay: false, + chat: false + } + } + + componentDidUpdate (prevProps, prevState, snapshot) { + console.log('embed', this.element) + } + + componentDidMount () { + const options = { + allowfullscreen: this.props.allowFullScreen, + width: this.props.width, + height: this.props.height, + autoplay: this.props.autoplay + } + + if (this.props.channel) { + options.channel = this.props.channel + } + + if (this.props.collection) { + options.collection = this.props.collection + } + + if (this.props.video) { + options.video = this.props.video + } + + this.embed = new Twitch.Player(this.element, options) + } + + renderChat () { + if (!this.props.chat) { + return null + } + + const source = 'https://www.twitch.tv/embed/' + this.props.channel + '/chat' + + return ( +