diff --git a/.env.example b/.env.example index 5ad08a4fa1..b85385d559 100644 --- a/.env.example +++ b/.env.example @@ -77,6 +77,7 @@ ALLOW_IFRAMING=false REFERRER_POLICY=same-origin ENABLE_CSP=false CORS_ALLOWED_ORIGINS=null +ENABLE_HSTS=false # -------------------------------------------- # OPTIONAL: CACHE SETTINGS diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..7ef56a0d5f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,40 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context, providing screenshots where practical. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: +* PHP version: +* MySQL version +* Webserver version +* OS version + + +# Checklist: + +- [ ] I have read the Contributing documentation available here: https://snipe-it.readme.io/docs/contributing-overview +- [ ] I have formatted this PR according to the project guidelines: https://snipe-it.readme.io/docs/contributing-overview#pull-request-guidelines +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes diff --git a/README.md b/README.md index d9b7db5a1c..2e85a4ed60 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/snipe/snipe-it.svg?branch=master)](https://travis-ci.org/snipe/snipe-it) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/snipe/snipe-it?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade) +[![All Contributors](https://img.shields.io/badge/all_contributors-189-orange.svg?style=flat-square)](#contributors) [![Open Source Helpers](https://www.codetriage.com/snipe/snipe-it/badges/users.svg)](https://www.codetriage.com/snipe/snipe-it) [![All Contributors](https://img.shields.io/badge/all_contributors-212-orange.svg?style=flat-square)](#contributors) [![Open Source Helpers](https://www.codetriage.com/snipe/snipe-it/badges/users.svg)](https://www.codetriage.com/snipe/snipe-it) @@ -114,6 +115,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken | [
Martin Stub](http://martinstub.dk)
[🌍](#translation-stubben "Translation") | [
Meyer Flavio](https://github.com/meyerf99)
[🌍](#translation-meyerf99 "Translation") | [
Micael Rodrigues](https://github.com/MicaelRodrigues)
[🌍](#translation-MicaelRodrigues "Translation") | [
Mikael Rasmussen](http://rubixy.com/)
[🌍](#translation-mikaelssen "Translation") | [
IxFail](https://github.com/IxFail)
[🌍](#translation-IxFail "Translation") | [
Mohammed Fota](http://www.mohammedfota.com)
[🌍](#translation-MohammedFota "Translation") | [
Moayad Alserihi](https://github.com/omego)
[🌍](#translation-omego "Translation") | | [
saymd](https://github.com/saymd)
[🌍](#translation-saymd "Translation") | [
Patrik Larsson](https://nordsken.se)
[🌍](#translation-pooot "Translation") | [
drcryo](https://github.com/drcryo)
[🌍](#translation-drcryo "Translation") | [
pawel1615](https://github.com/pawel1615)
[🌍](#translation-pawel1615 "Translation") | [
bodrovics](https://github.com/bodrovics)
[🌍](#translation-bodrovics "Translation") | [
priatna](https://github.com/priatna)
[🌍](#translation-priatna "Translation") | [
Fan Jiang](https://amayume.net)
[🌍](#translation-ProfFan "Translation") | | [
ragnarcx](https://github.com/ragnarcx)
[🌍](#translation-ragnarcx "Translation") | [
Rein van Haaren](http://www.reinvanhaaren.nl/)
[🌍](#translation-reinvanhaaren "Translation") | [
Teguh Dwicaksana](http://dheche.songolimo.net)
[🌍](#translation-dheche "Translation") | [
fraccie](https://github.com/FRaccie)
[🌍](#translation-FRaccie "Translation") | [
vinzruzell](https://github.com/vinzruzell)
[🌍](#translation-vinzruzell "Translation") | [
Kevin Austin](http://kevinaustin.com)
[🌍](#translation-vipsystem "Translation") | [
Wira Sandy](http://azuraweb.xyz)
[🌍](#translation-wira-sandy "Translation") | +| [
Илья](https://github.com/GrayHoax)
[🌍](#translation-GrayHoax "Translation") | [
GodUseVPN](https://github.com/godusevpn)
[🌍](#translation-godusevpn "Translation") | [
周周](https://github.com/EngrZhou)
[🌍](#translation-EngrZhou "Translation") | [
Sam](https://github.com/takuy)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [
Azerothian](https://www.illisian.com.au)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [
Tim Farmer](https://github.com/timothyfarmer)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [
MariΓ‘n Skrip](https://github.com/mskrip)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | +| [
Godfrey Martinez](https://github.com/Godmartinz)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") | [
bigtreeEdo](https://github.com/bigtreeEdo)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [
Colin McNeil](https://colinmcneil.me/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [
JoKneeMo](https://github.com/JoKneeMo)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [
Joshi](http://www.redbridge.se)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [
Anthony Burns](https://github.com/anthonypburns)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [
Alexander Chibrikin](http://phpprofi.ru/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | | [
Илья](https://github.com/GrayHoax)
[🌍](#translation-GrayHoax "Translation") | [
GodUseVPN](https://github.com/godusevpn)
[🌍](#translation-godusevpn "Translation") | [
周周](https://github.com/EngrZhou)
[🌍](#translation-EngrZhou "Translation") | [
Sam](https://github.com/takuy)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [
Azerothian](https://www.illisian.com.au)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [
Wes Hulette](http://macfoo.wordpress.com/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [
patrict](https://github.com/patrict)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=patrict "Code") | | [
Dmitriy Minaev](https://github.com/VELIKII-DIVAN)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [
liquidhorse](https://github.com/liquidhorse)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [
Jordi Boggiano](https://seld.be/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [
Ivan Nieto](https://github.com/inietov)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [
Ben RUBSON](https://github.com/benrubson)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [
NMathar](https://github.com/NMathar)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [
Steffen](https://github.com/smb)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=smb "Code") | | [
Sxderp](https://github.com/Sxderp)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [
fanta8897](https://github.com/fanta8897)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [
Andrey Bolonin](https://andreybolonin.com/phpconsulting/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [
shinayoshi](http://www.shinayoshi.net/)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [
Hubert](https://github.com/reuser)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [
KeenRivals](https://brashear.me)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [
omyno](https://github.com/omyno)
[πŸ’»](https://github.com/snipe/snipe-it/commits?author=omyno "Code") | diff --git a/app/Console/Commands/CheckinLicensesFromAllUsers.php b/app/Console/Commands/CheckinLicensesFromAllUsers.php new file mode 100644 index 0000000000..bcd8583eaf --- /dev/null +++ b/app/Console/Commands/CheckinLicensesFromAllUsers.php @@ -0,0 +1,95 @@ +option('license_id'); + $notify = $this->option('notify'); + + if (!$license_id) { + $this->error('ERROR: License ID is required.'); + return false; + } + + + if (!$license = License::where('id','=',$license_id)->first()) { + $this->error('Invalid license ID'); + return false; + } + + $this->info('Checking in ALL seats for '.$license->name); + + + $licenseSeats = LicenseSeat::where('license_id', '=', $license_id) + ->whereNotNull('assigned_to') + ->with('user') + ->get(); + + $this->info(' There are ' .$licenseSeats->count(). ' seats checked out: '); + + if (!$notify) { + $this->info('No mail will be sent.'); + } + + foreach ($licenseSeats as $seat) { + $this->info($seat->user->username .' has a license seat for '.$license->name); + $seat->assigned_to = null; + + if ($seat->save()) { + + // Override the email address so we don't notify on checkin + if (!$notify) { + $seat->user->email = null; + } + + // Log the checkin + $seat->logCheckin($seat->user, 'Checked in via cli tool'); + } + + + + + } + + + } +} diff --git a/app/Console/Commands/CheckoutLicenseToAllUsers.php b/app/Console/Commands/CheckoutLicenseToAllUsers.php new file mode 100644 index 0000000000..0855e5d084 --- /dev/null +++ b/app/Console/Commands/CheckoutLicenseToAllUsers.php @@ -0,0 +1,112 @@ +option('license_id'); + $notify = $this->option('notify'); + + if (!$license_id) { + $this->error('ERROR: License ID is required.'); + return false; + } + + + if (!$license = License::where('id','=',$license_id)->with('assignedusers')->first()) { + $this->error('Invalid license ID'); + return false; + } + + $users = User::whereNull('deleted_at')->with('licenses')->get(); + + if ($users->count() > $license->getAvailSeatsCountAttribute()) { + $this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. '); + } + + $this->info('Checking out '.$users->count().' of '.$license->getAvailSeatsCountAttribute().' seats for '.$license->name); + + if (!$notify) { + $this->info('No mail will be sent.'); + } + + foreach ($users as $user) { + + // Check to make sure this user doesn't already have this license checked out + // to them + + if ($user->licenses->where('id', '=', $license_id)->count()) { + $this->info($user->username .' already has this license checked out to them. Skipping... '); + continue; + } + + + // If the license is valid, check that there is an available seat + if ($license->availCount()->count() < 1) { + $this->error('ERROR: No available seats'); + return false; + } + + $this->info($license->availCount()->count().' seats left'); + // Get the seat ID + $licenseSeat = $license->freeSeat(); + + + // Update the seat with checkout info, + $licenseSeat->assigned_to = $user->id; + if ($licenseSeat->save()) { + + // Temporarily null the user's email address so we don't send mail if we're not supposed to + if (!$notify) { + $user->email = null; + } + + // Log the checkout + $licenseSeat->logCheckout('Checked out via cli tool', $user); + $this->info('License '.$license_id.' seat '.$licenseSeat->id.' checked out to '.$user->username); + } + + } + + + + } +} diff --git a/app/Console/Commands/MergeUsersByUsername.php b/app/Console/Commands/MergeUsersByUsername.php new file mode 100644 index 0000000000..ded5d1986f --- /dev/null +++ b/app/Console/Commands/MergeUsersByUsername.php @@ -0,0 +1,109 @@ +whereNull('deleted_at')->get(); + + foreach ($users as $user) { + $parts = explode("@", $user->username); + $bad_users = User::where('username', '=', $parts[0])->whereNull('deleted_at')->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')->get(); + + foreach ($bad_users as $bad_user) { + $this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') '); + + // Walk the list of assets + foreach ($bad_user->assets as $asset) { + $this->info( 'Updating asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id); + $asset->assigned_to = $user->id; + $asset->save(); + } + + // Walk the list of licenses + foreach ($bad_user->licenses as $license) { + $this->info( 'Updating license '.$license->name.' '.$license->id.' to user '.$user->id); + $bad_user->licenses()->updateExistingPivot($license->id, ['assigned_to' => $user->id]); + } + + // Walk the list of consumables + foreach ($bad_user->consumables as $consumable) { + $this->info( 'Updating consumable '.$consumable->id.' to user '.$user->id); + $bad_user->consumables()->updateExistingPivot($consumable->id, ['assigned_to' => $user->id]); + } + + // Walk the list of accessories + foreach ($bad_user->accessories as $accessory) { + $this->info( 'Updating accessory '.$accessory->id.' to user '.$user->id); + $bad_user->accessories()->updateExistingPivot($accessory->id, ['assigned_to' => $user->id]); + } + + // Walk the list of logs + foreach ($bad_user->userlog as $log) { + $this->info( 'Updating action log record '.$log->id.' to user '.$user->id); + $log->target_id = $user->id; + $log->save(); + } + + // Update any manager IDs + $this->info( 'Updating managed user records to user '.$user->id); + User::where('manager_id', '=', $bad_user->id)->update(['manager_id' => $user->id]); + + + // Update location manager IDs + foreach ($bad_user->managedLocations as $managedLocation) { + $this->info( 'Updating managed location record '.$managedLocation->name.' to manager '.$user->id); + $managedLocation->manager_id = $user->id; + $managedLocation->save(); + } + + // Mark the user as deleted + $this->info( 'Marking the user as deleted'); + $bad_user->deleted_at = Carbon::now()->timestamp; + $bad_user->save(); + + + } + + } + + + } +} diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 5585f3a695..4c4e2a8a92 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -457,6 +457,7 @@ class AssetsController extends Controller $asset->supplier_id = $request->get('supplier_id', 0); $asset->requestable = $request->get('requestable', 0); $asset->rtd_location_id = $request->get('rtd_location_id', null); + $asset->location_id = $request->get('rtd_location_id', null); if ($request->has('image_source') && $request->input('image_source') != "") { $saved_image_path = Helper::processUploadedImage( diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index a3b785333f..b1d953b975 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -8,7 +8,9 @@ use App\Http\Transformers\ConsumablesTransformer; use App\Http\Transformers\SelectlistTransformer; use App\Models\Company; use App\Models\Consumable; -use Illuminate\Http\Request; +use App\Models\User; +use App\Http\Transformers\ConsumablesTransformer; +use App\Helpers\Helper; class ConsumablesController extends Controller { @@ -158,7 +160,7 @@ class ConsumablesController extends Controller return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.delete.success'))); } - /** + /** * Returns a JSON response containing details on the users associated with this consumable. * * @author [A. Gianotto] [] @@ -198,6 +200,57 @@ class ConsumablesController extends Controller return $data; } + /** + * Checkout a consumable + * + * @author [A. Gutierrez] [] + * @param int $id + * @since [v4.9.5] + * @return JsonResponse + */ + public function checkout(Request $request, $id) + { + // Check if the consumable exists + if (is_null($consumable = Consumable::find($id))) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.does_not_exist'))); + } + + $this->authorize('checkout', $consumable); + + if ($consumable->qty > 0) { + + // Check if the user exists + $assigned_to = $request->input('assigned_to'); + if (is_null($user = User::find($assigned_to))) { + // Return error message + return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found')); + } + + // Update the consumable data + $consumable->assigned_to = e($assigned_to); + + $consumable->users()->attach($consumable->id, [ + 'consumable_id' => $consumable->id, + 'user_id' => $user->id, + 'assigned_to' => $assigned_to + ]); + + // Log checkout event + $logaction = $consumable->logCheckout(e($request->input('note')), $user); + $data['log_id'] = $logaction->id; + $data['eula'] = $consumable->getEula(); + $data['first_name'] = $user->first_name; + $data['item_name'] = $consumable->name; + $data['checkout_date'] = $logaction->created_at; + $data['note'] = $logaction->note; + $data['require_acceptance'] = $consumable->requireAcceptance(); + + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success'))); + } + + return response()->json(Helper::formatStandardApiResponse('error', null, 'No consumables remaining')); + } + /** * Gets a paginated collection for the select2 menus * diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 5f747726c9..a130eaa1db 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -78,6 +78,14 @@ class UsersController extends Controller $users = $users->where('users.location_id', '=', $request->input('location_id')); } + if ($request->filled('email')) { + $users = $users->where('users.email', '=', $request->input('email')); + } + + if ($request->filled('username')) { + $users = $users->where('users.username', '=', $request->input('username')); + } + if ($request->filled('group_id')) { $users = $users->ByGroup($request->get('group_id')); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 2f25deab92..3ed9940118 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -385,6 +385,7 @@ class LoginController extends Controller $request->session()->regenerate(true); + $request->session()->regenerate(true); Auth::logout(); if (!empty($sloRedirectUrl)) { diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index da3c5092b9..0045496791 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -17,15 +17,12 @@ class Kernel extends HttpKernel \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\FrameGuard::class, - \App\Http\Middleware\XssProtectHeader::class, - \App\Http\Middleware\ReferrerPolicyHeader::class, - \App\Http\Middleware\ContentSecurityPolicyHeader::class, - \App\Http\Middleware\NosniffGuard::class, \Fideloper\Proxy\TrustProxies::class, \App\Http\Middleware\CheckForSetup::class, \App\Http\Middleware\CheckForDebug::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + \App\Http\Middleware\SecurityHeaders::class, + ]; /** diff --git a/app/Http/Middleware/ContentSecurityPolicyHeader.php b/app/Http/Middleware/ContentSecurityPolicyHeader.php deleted file mode 100644 index 45c87a59e3..0000000000 --- a/app/Http/Middleware/ContentSecurityPolicyHeader.php +++ /dev/null @@ -1,35 +0,0 @@ -headers->set('Content-Security-Policy', $policy); - return $response; - } -} diff --git a/app/Http/Middleware/FrameGuard.php b/app/Http/Middleware/FrameGuard.php deleted file mode 100644 index beb19f20f1..0000000000 --- a/app/Http/Middleware/FrameGuard.php +++ /dev/null @@ -1,24 +0,0 @@ -headers->set('X-Frame-Options', 'SAMEORIGIN', false); - } - return $response; - - } -} diff --git a/app/Http/Middleware/NosniffGuard.php b/app/Http/Middleware/NosniffGuard.php deleted file mode 100644 index 295f5e75af..0000000000 --- a/app/Http/Middleware/NosniffGuard.php +++ /dev/null @@ -1,21 +0,0 @@ -headers->set('X-Content-Type-Options', 'nosniff', false); - return $response; - } -} diff --git a/app/Http/Middleware/ReferrerPolicyHeader.php b/app/Http/Middleware/ReferrerPolicyHeader.php deleted file mode 100644 index 430ce45af3..0000000000 --- a/app/Http/Middleware/ReferrerPolicyHeader.php +++ /dev/null @@ -1,21 +0,0 @@ -headers->set('Referrer-Policy', config('app.referrer_policy')); - return $response; - } -} diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php new file mode 100644 index 0000000000..ab88ec1b87 --- /dev/null +++ b/app/Http/Middleware/SecurityHeaders.php @@ -0,0 +1,122 @@ +removeUnwantedHeaders($this->unwantedHeaderList); + $response = $next($request); + + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('X-XSS-Protection', '1; mode=block'); + + // Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT, + // since we don't provide any way to IFRAME anything in in the first place. + // There is currently no easy way to default ALL THE THINGS to 'none', but + // security audits will still ding you if you don't have this header, even + // though we don't allow IFRAMING in the first place. + // + // So for security and compliance sake, here we are. Sigh. + // + // See also: + // - https://developers.google.com/web/updates/2018/06/feature-policy + // - https://scotthelme.co.uk/a-new-security-header-feature-policy/ + // - https://github.com/w3c/webappsec-feature-policy/issues/189 + + $feature_policy[] = "accelerometer 'none'"; + $feature_policy[] = "ambient-light-sensor 'none'"; + $feature_policy[] = "animations 'none'"; + $feature_policy[] = "autoplay 'none'"; + $feature_policy[] = "battery 'none'"; + $feature_policy[] = "camera 'none'"; + $feature_policy[] = "display-capture 'none'"; + $feature_policy[] = "document-domain 'none'"; + $feature_policy[] = "encrypted-media 'none'"; + $feature_policy[] = "fullscreen 'none'"; + $feature_policy[] = "geolocation 'none'"; + $feature_policy[] = "gyroscope 'none'"; + $feature_policy[] = "legacy-image-formats 'none'"; + $feature_policy[] = "magnetometer 'none'"; + $feature_policy[] = "microphone 'none'"; + $feature_policy[] = "midi 'none'"; + $feature_policy[] = "oversized-images 'none'"; + $feature_policy[] = "payment 'none'"; + $feature_policy[] = "picture-in-picture 'none'"; + $feature_policy[] = "publickey-credentials 'none'"; + $feature_policy[] = "sync-xhr 'none'"; + $feature_policy[] = "unsized-media 'none'"; + $feature_policy[] = "usb 'none'"; + $feature_policy[] = "vibrate 'none'"; + $feature_policy[] = "wake-lock 'none'"; + $feature_policy[] = "xr-spatial-tracking 'none'"; + + $feature_policy = join(';', $feature_policy); + $response->headers->set('Feature-Policy', $feature_policy); + + + + // Defaults to same-origin if REFERRER_POLICY is not set in the .env + $response->headers->set('Referrer-Policy', config('app.referrer_policy')); + + // The .env var ALLOW_IFRAMING defaults to false (which disallows IFRAMING) + // if not present, but some unique cases require this to be enabled. + // For example, some IT depts have IFRAMED Snipe-IT into their IT portal + // for convenience so while it is normally disallowed, there is + // an override that exists. + + if (config('app.allow_iframing') == false) { + $response->headers->set('X-Frame-Options', 'DENY'); + } + + + // This defaults to false to maintain backwards compatibility for + // people who are not running Snipe-IT over TLS (shame, shame, shame!) + // Seriously though, please run Snipe-IT over TLS. Let's Encrypt is free. + // https://letsencrypt.org + + if (config('app.enable_hsts') === true) { + $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } + + // We have to exclude debug mode here because debugbar pulls from a CDN or two + // and it will break things. + + if ((config('app.debug')!='true') || (config('app.enable_csp')=='true')) { + $csp_policy[] = "default-src 'self'"; + $csp_policy[] = "style-src 'self' 'unsafe-inline'"; + $csp_policy[] = "script-src 'self' 'unsafe-inline' 'unsafe-eval'"; + $csp_policy[] = "connect-src 'self'"; + $csp_policy[] = "object-src 'none'"; + $csp_policy[] = "font-src 'self' data:"; + $csp_policy[] = "img-src 'self' data: gravatar.com maps.google.com maps.gstatic.com *.googleapis.com"; + $csp_policy = join(';', $csp_policy); + $response->headers->set('Content-Security-Policy', $csp_policy); + } + + return $response; + } + + private function removeUnwantedHeaders($headerList) + { + foreach ($headerList as $header) + header_remove($header); + } +} diff --git a/app/Http/Middleware/XssProtectHeader.php b/app/Http/Middleware/XssProtectHeader.php deleted file mode 100644 index 868d100f37..0000000000 --- a/app/Http/Middleware/XssProtectHeader.php +++ /dev/null @@ -1,22 +0,0 @@ -headers->set('X-XSS-Protection', $mode); - return $response; - } -} diff --git a/app/Importer/Importer.php b/app/Importer/Importer.php index d157b16ab0..6466c3b2d1 100644 --- a/app/Importer/Importer.php +++ b/app/Importer/Importer.php @@ -63,6 +63,10 @@ abstract class Importer 'full_name' => 'full name', 'email' => 'email', 'username' => 'username', + 'address' => 'address', + 'city' => 'city', + 'state' => 'state', + 'country' => 'country', 'jobtitle' => 'job title', 'employee_num' => 'employee number', 'phone_number' => 'phone number', diff --git a/app/Importer/UserImporter.php b/app/Importer/UserImporter.php index acbfab54f7..4fcf221cba 100644 --- a/app/Importer/UserImporter.php +++ b/app/Importer/UserImporter.php @@ -48,6 +48,10 @@ class UserImporter extends ItemImporter $this->item['email'] = $this->findCsvMatch($row, 'email'); $this->item['phone'] = $this->findCsvMatch($row, 'phone_number'); $this->item['jobtitle'] = $this->findCsvMatch($row, 'jobtitle'); + $this->item['address'] = $this->findCsvMatch($row, 'address'); + $this->item['city'] = $this->findCsvMatch($row, 'city'); + $this->item['state'] = $this->findCsvMatch($row, 'state'); + $this->item['country'] = $this->findCsvMatch($row, 'country'); $this->item['activated'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'activated')) == 1) ? '1' : 0; $this->item['employee_num'] = $this->findCsvMatch($row, 'employee_num'); $this->item['department_id'] = $this->createOrFetchDepartment($this->findCsvMatch($row, 'department')); diff --git a/app/Importer/import_mappings.md b/app/Importer/import_mappings.md index 0642241525..7899cf679a 100644 --- a/app/Importer/import_mappings.md +++ b/app/Importer/import_mappings.md @@ -7,13 +7,14 @@ | department_id | | User ? All | | item name | item_name | All | | image | image | Asset | -| department_id | | User ? All | +| email | | | | expiration date | expiration_date | License | | location | location | All | | notes | notes | All | | licensed to email | license_email | License | | licensed to name | license_name | License | | maintained | maintained | License | +| manager_id | | User | | manufacturer | manufacturer | All | | model name | asset_model | Asset | | model number | model_number | Asset | @@ -34,4 +35,8 @@ | name | | | | email | | | | username | | | +| address | address | User | +| city | city | User | +| state | state | User | +| country | country | User | diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 2a92c81a5a..9af62593d7 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -1094,7 +1094,7 @@ class Asset extends Depreciable $interval = $settings->audit_warning_days ?? 0; return $query->whereNotNull('assets.next_audit_date') - ->whereRaw("DATE_SUB(assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'") + ->whereRaw("DATE_SUB(".DB::getTablePrefix()."assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'") ->where('assets.archived', '=', 0) ->NotArchived(); } diff --git a/app/Models/Company.php b/app/Models/Company.php index c81655029e..e6bd3dd773 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -126,9 +126,12 @@ final class Company extends SnipeModel } elseif (!static::isFullMultipleCompanySupportEnabled()) { return true; } else { - $current_user_company_id = Auth::user()->company_id; - $companyable_company_id = $companyable->company_id; - return ($current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser()); + if (Auth::user()) { + $current_user_company_id = Auth::user()->company_id; + $companyable_company_id = $companyable->company_id; + return ($current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser()); + } + } } diff --git a/app/Models/Loggable.php b/app/Models/Loggable.php index 1bb05b9298..bff1ac2e10 100644 --- a/app/Models/Loggable.php +++ b/app/Models/Loggable.php @@ -28,8 +28,9 @@ trait Loggable { $log = new Actionlog; $log = $this->determineLogItemType($log); - if(Auth::user()) + if (Auth::user()) { $log->user_id = Auth::user()->id; + } if (!isset($target)) { throw new \Exception('All checkout logs require a target.'); @@ -113,14 +114,45 @@ trait Loggable $log->location_id = null; $log->note = $note; - $log->action_date = $action_date; - - if (!$log->action_date) { - $log->action_date = date('Y-m-d H:i:s'); + if (Auth::user()) { + $log->user_id = Auth::user()->id; } - $log->user_id = Auth::user()->id; + $log->logaction('checkin from'); + $params = [ + 'target' => $target, + 'item' => $log->item, + 'admin' => $log->user, + 'note' => $note, + 'target_type' => $log->target_type, + 'settings' => $settings, + ]; + + + $checkinClass = null; + + if (method_exists($target, 'notify')) { + try { + $target->notify(new static::$checkinClass($params)); + } catch (\Exception $e) { + \Log::debug($e); + } + + } + + // Send to the admin, if settings dictate + $recipient = new \App\Models\Recipients\AdminRecipient(); + + if (($settings->admin_cc_email!='') && (static::$checkinClass!='')) { + try { + $recipient->notify(new static::$checkinClass($params)); + } catch (\Exception $e) { + \Log::debug($e); + } + + } + return $log; } diff --git a/app/Models/User.php b/app/Models/User.php index 109ad4f92d..ff886905fc 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -331,7 +331,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo */ public function userlog() { - return $this->hasMany('\App\Models\Actionlog', 'target_id')->orderBy('created_at', 'DESC')->withTrashed(); + return $this->hasMany('\App\Models\Actionlog', 'target_id')->where('target_type', '=', 'App\Models\User')->orderBy('created_at', 'DESC')->withTrashed(); } @@ -521,6 +521,18 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo } elseif ($format=='firstname') { $username = str_slug($first_name); } + elseif ($format=='firstinitial.lastname') { + $username = str_slug(substr($first_name, 0, 1). '.' . str_slug($last_name)); + } + elseif ($format=='lastname_firstinitial') { + $username = str_slug($last_name).'_'.str_slug(substr($first_name, 0, 1)); + } + elseif ($format=='firstnamelastname') { + $username = str_slug($first_name) . str_slug($last_name); + } + elseif ($format=='firstnamelastinitial') { + $username = str_slug(($first_name.substr($last_name, 0, 1))); + } } $user['first_name'] = $first_name; diff --git a/app/Notifications/CheckinLicenseSeatNotification.php b/app/Notifications/CheckinLicenseSeatNotification.php index 07d5455b13..f9d4914c37 100644 --- a/app/Notifications/CheckinLicenseSeatNotification.php +++ b/app/Notifications/CheckinLicenseSeatNotification.php @@ -67,10 +67,18 @@ class CheckinLicenseSeatNotification extends Notification $botname = ($this->settings->slack_botname) ? $this->settings->slack_botname : 'Snipe-Bot' ; - $fields = [ - 'To' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>', - 'By' => '<'.$admin->present()->viewUrl().'|'.$admin->present()->fullName().'>', - ]; + if ($admin) { + $fields = [ + 'To' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>', + 'By' => '<'.$admin->present()->viewUrl().'|'.$admin->present()->fullName().'>', + ]; + } else { + $fields = [ + 'To' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>', + 'By' => 'CLI tool', + ]; + } + diff --git a/app/Presenters/AssetPresenter.php b/app/Presenters/AssetPresenter.php index 4b791be0c1..5e009ebfa1 100644 --- a/app/Presenters/AssetPresenter.php +++ b/app/Presenters/AssetPresenter.php @@ -268,8 +268,11 @@ class AssetPresenter extends Presenter "searchable" => true, "sortable" => true, "switchable" => true, - "title" => ($field->field_encrypted=='1') ?' '.$field->name : $field->name, - "formatter" => "customFieldsFormatter" + "title" => $field->name, + "formatter"=> 'customFieldsFormatter', + "escape" => true, + "class" => ($field->field_encrypted=='1') ? 'css-padlock' : '', + "visible" => true, ]; } diff --git a/config/app.php b/config/app.php index b4c21884b3..9f1f03129b 100755 --- a/config/app.php +++ b/config/app.php @@ -197,19 +197,33 @@ return [ /* - |-------------------------------------------------------------------------- - | ALLOW I-FRAMING - |-------------------------------------------------------------------------- - | - | Normal users will never need to edit this. This option lets you run - | Snipe-IT within an I-Frame, which is normally disabled by default for - | security reasons, to prevent clickjacking. It should normally be set to false. - | - */ + |-------------------------------------------------------------------------- + | ALLOW I-FRAMING + |-------------------------------------------------------------------------- + | + | Normal users will never need to edit this. This option lets you run + | Snipe-IT within an I-Frame, which is normally disabled by default for + | security reasons, to prevent clickjacking. It should normally be set to false. + | + */ 'allow_iframing' => env('ALLOW_IFRAMING', false), + /* + |-------------------------------------------------------------------------- + | ENABLE HTTP Strict Transport Security (HSTS) + |-------------------------------------------------------------------------- + | + | This is set to default false for backwards compatibilty but should be + | set to true if the hosting environment allows it. + | + | See https://scotthelme.co.uk/hsts-the-missing-link-in-tls/ + | + */ + + 'enable_hsts' => env('ENABLE_HSTS', false), + /* |-------------------------------------------------------------------------- | REFERRER-POLICY diff --git a/public/css/overrides.css b/public/css/overrides.css index 0c23ba211b..50c9619ae8 100644 Binary files a/public/css/overrides.css and b/public/css/overrides.css differ diff --git a/public/css/overrides.css.map b/public/css/overrides.css.map index 8f69f597f4..bfec6b1f14 100644 Binary files a/public/css/overrides.css.map and b/public/css/overrides.css.map differ diff --git a/resources/assets/js/components/importer/importer-file.vue b/resources/assets/js/components/importer/importer-file.vue index 4bcad9cce0..8dba9fe5d9 100644 --- a/resources/assets/js/components/importer/importer-file.vue +++ b/resources/assets/js/components/importer/importer-file.vue @@ -1,15 +1,9 @@ - - + \ No newline at end of file diff --git a/resources/assets/js/components/passport/Clients.vue b/resources/assets/js/components/passport/Clients.vue index 936786a87a..ee5db67742 100644 --- a/resources/assets/js/components/passport/Clients.vue +++ b/resources/assets/js/components/passport/Clients.vue @@ -83,7 +83,7 @@ @@ -151,7 +151,7 @@ diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index 21f8fc5012..d189a90796 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -466,6 +466,74 @@ h4 { display: table-cell; } + + + +/** + + COLUMN SELECTOR ICONS + ----------------------------- + This is kind of weird, but it is necessary to prevent the column-selector code from barfing, since + any HTML used in the UserPresenter "title" attribute breaks the column selector HTML. + + Instead, we use CSS to add the icon into the table header, which leaves the column selector + "title" text as-is. + + See https://github.com/snipe/snipe-it/issues/7989 + + */ + +th.css-barcode > .th-inner, +th.css-license > .th-inner, +th.css-consumable > .th-inner, +th.css-accessory > .th-inner +{ + font-size: 0px; + line-height: 4!important; + text-align: left; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + + +th.css-padlock > .th-inner::before, +th.css-barcode > .th-inner::before, +th.css-license > .th-inner::before, +th.css-consumable > .th-inner::before, +th.css-accessory > .th-inner::before + +{ + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: 20px; +} + +th.css-padlock > .th-inner::before +{ + content: "\f023"; + padding-right: 2px; +} + +th.css-barcode > .th-inner::before +{ + content: "\f02a"; +} + +th.css-license > .th-inner::before +{ + content: "\f0c7"; +} + +th.css-consumable > .th-inner::before +{ + content: "\f043"; +} + +th.css-accessory > .th-inner::before +{ + content: "\f11c"; +} .small-box .inner { padding-left: 15px; padding-right: 15px; diff --git a/resources/lang/en-GB/general.php b/resources/lang/en-GB/general.php index 6efb25c645..8a4e332ad8 100644 --- a/resources/lang/en-GB/general.php +++ b/resources/lang/en-GB/general.php @@ -88,6 +88,9 @@ 'firstname_lastname_underscore_format' => 'First Name Last Name (jane_smith@example.com)', 'lastnamefirstinitial_format' => 'Last Name First Initial (smithj@example.com)', 'first' => 'First', + 'firstnamelastname' => 'First Name Last Name (janesmith@example.com)', + 'lastname_firstinitial' => 'Last Name First Initial (smith_j@example.com)', + 'firstinitial.lastname' => 'First Initial Last Name (j.smith@example.com)', 'first' => 'First', 'first_name' => 'First Name', 'first_name_format' => 'First Name (jane@example.com)', 'files' => 'Files', diff --git a/resources/lang/en/admin/licenses/message.php b/resources/lang/en/admin/licenses/message.php index 3e7ee486c7..87a7c3d0b9 100644 --- a/resources/lang/en/admin/licenses/message.php +++ b/resources/lang/en/admin/licenses/message.php @@ -8,6 +8,7 @@ return array( 'owner_doesnt_match_asset' => 'The asset you are trying to associate with this license is owned by somene other than the person selected in the assigned to dropdown.', 'assoc_users' => 'This license is currently checked out to a user and cannot be deleted. Please check the license in first, and then try deleting again. ', 'select_asset_or_person' => 'You must select an asset or a user, but not both.', + 'not_found' => 'License not found', 'create' => array( diff --git a/resources/lang/en/general.php b/resources/lang/en/general.php index 962bfb8341..27e456042a 100644 --- a/resources/lang/en/general.php +++ b/resources/lang/en/general.php @@ -91,6 +91,11 @@ 'lastnamefirstinitial_format' => 'Last Name First Initial (smithj@example.com)', 'firstintial_dot_lastname_format' => 'First Initial Last Name (j.smith@example.com)', 'first' => 'First', + 'firstnamelastname' => 'First Name Last Name (janesmith@example.com)', + 'lastname_firstinitial' => 'Last Name First Initial (smith_j@example.com)', + 'firstinitial.lastname' => 'First Initial Last Name (j.smith@example.com)', + 'firstnamelastinitial' => 'First Name Last Initial (janes@example.com)', + 'first' => 'First', 'first_name' => 'First Name', 'first_name_format' => 'First Name (jane@example.com)', 'files' => 'Files', diff --git a/resources/macros/macros.php b/resources/macros/macros.php index 40b0921c6d..5474db204a 100644 --- a/resources/macros/macros.php +++ b/resources/macros/macros.php @@ -464,11 +464,14 @@ Form::macro('username_format', function ($name = "username_format", $selected = $formats = array( 'firstname.lastname' => trans('general.firstname_lastname_format'), - 'firstname_lastname' => trans('general.firstname_lastname_underscore_format'), - 'filastname' => trans('general.filastname_format'), - 'firstintial.lastname' => trans('general.firstintial_dot_lastname_format'), 'firstname' => trans('general.first_name_format'), + 'filastname' => trans('general.filastname_format'), 'lastnamefirstinitial' => trans('general.lastnamefirstinitial_format'), + 'firstname_lastname' => trans('general.firstname_lastname_underscore_format'), + 'firstinitial.lastname' => trans('general.firstinitial.lastname'), + 'lastname_firstinitial' => trans('general.lastname_firstinitial'), + 'firstnamelastname' => trans('general.firstnamelastname'), + 'firstnamelastinitial' => trans('general.firstnamelastinitial') ); $select = ' - Generate + Generate
-
+
diff --git a/resources/views/reports/activity.blade.php b/resources/views/reports/activity.blade.php index ac01ddfe16..a580386b8c 100644 --- a/resources/views/reports/activity.blade.php +++ b/resources/views/reports/activity.blade.php @@ -37,7 +37,7 @@ - Icon + Icon {{ trans('general.date') }} {{ trans('general.admin') }} {{ trans('general.action') }} diff --git a/resources/views/users/bulk-edit.blade.php b/resources/views/users/bulk-edit.blade.php index b62a73b2a2..6f401c8767 100644 --- a/resources/views/users/bulk-edit.blade.php +++ b/resources/views/users/bulk-edit.blade.php @@ -55,6 +55,15 @@ + +
+ +
+ + {!! $errors->first('city', '') !!} +
+
+
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 4ca154a596..b28a3fdc9b 100755 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -15,66 +15,6 @@ @section('header_right') - - @can('create', \App\Models\User::class) @if ($snipeSettings->ldap_enabled == 1) LDAP Sync @@ -153,6 +93,7 @@ @section('moar_scripts') + @include ('partials.bootstrap-table') diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index 3194a90bbf..6fb5dec20f 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -142,11 +142,31 @@ {{ trans('admin/users/table.name') }} {{ $user->present()->fullName() }} + {{ trans('admin/users/table.username') }} {{ $user->username }} + @if (($user->address) || ($user->city) || ($user->state) || ($user->country)) + + {{ trans('general.address') }} + + @if ($user->address) + {{ $user->address }}
+ @endif + @if ($user->city) + {{ $user->city }} + @endif + @if ($user->state) + {{ $user->state }} + @endif + @if ($user->country) + {{ $user->country }} + @endif + + + @endif {{ trans('general.groups') }} @@ -244,6 +264,13 @@ {{ trans('general.login_enabled') }} {{ ($user->activated=='1') ? trans('general.yes') : trans('general.no') }} + @if ($user->ldap_import!='1') + + LDAP + {{ trans('general.yes') }} + + @endif + @if ($user->activated=='1') @@ -278,6 +305,13 @@ @endif + @if ($user->notes) + + {{ trans('admin/users/table.notes') }} + {{ $user->notes }} + + @endif +
@@ -541,7 +575,7 @@ }'> - Icon + Icon {{ trans('general.date') }} {{ trans('general.admin') }} {{ trans('general.action') }} diff --git a/routes/api.php b/routes/api.php index 058c52bbb3..ee5fe63191 100644 --- a/routes/api.php +++ b/routes/api.php @@ -233,12 +233,21 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api'] ] ); // Consumables resource - Route::get('consumables/view/{id}/users', - [ - 'as' => 'api.consumables.showUsers', - 'uses' => 'ConsumablesController@getDataView' - ] - ); + Route::group(['prefix' => 'consumables'], function () { + Route::get('view/{id}/users', + [ + 'as' => 'api.consumables.showUsers', + 'uses' => 'ConsumablesController@getDataView' + ] + ); + + Route::post('{consumable}/checkout', + [ + 'as' => 'api.consumables.checkout', + 'uses' => 'ConsumablesController@checkout' + ] + ); + }); /*--- Depreciations API ---*/ diff --git a/routes/web/fields.php b/routes/web/fields.php index d95f8e5546..a2fedcfd93 100644 --- a/routes/web/fields.php +++ b/routes/web/fields.php @@ -17,7 +17,6 @@ Route::group([ 'prefix' => 'fields','middleware' => ['auth'] ], function () { 'as' => 'fields.optional'] ); - Route::get('{field_id}/fieldset/{fieldset_id}/disassociate', ['uses' => 'CustomFieldsController@deleteFieldFromFieldset', 'as' => 'fields.disassociate'] diff --git a/snipeit.sh b/snipeit.sh index d70365e4f0..bd4b1b80e8 100755 --- a/snipeit.sh +++ b/snipeit.sh @@ -438,7 +438,7 @@ case $distro in fi ;; ubuntu) - if [ "$version" == "18.04" ]; then + if [ "$version" -ge "18.04" ]; then # Install for Ubuntu 18.04 tzone=$(cat /etc/timezone) diff --git a/tests/unit/UserTest.php b/tests/unit/UserTest.php index 6624363b6d..85ae9054d0 100644 --- a/tests/unit/UserTest.php +++ b/tests/unit/UserTest.php @@ -33,7 +33,7 @@ class UserTest extends BaseTest $fullname = "Natalia Allanovna Romanova-O'Shostakova"; $expected_firstname = 'Natalia'; $expected_lastname = "Allanovna Romanova-O'Shostakova"; - $user = User::generateFormattedNameFromFullName($fullname, 'firstname'); + $user = User::generateFormattedNameFromFullName('firstname', $fullname); $this->assertEquals($expected_firstname, $user['first_name']); $this->assertEquals($expected_lastname, $user['last_name']); } @@ -74,7 +74,7 @@ class UserTest extends BaseTest public function testFirstInitialUnderscoreLastName() { $fullname = "Natalia Allanovna Romanova-O'Shostakova"; - $expected_username = 'natalia_allanovna-romanova-oshostakova'; + $expected_username = 'n_allanovna-romanova-oshostakova'; $user = User::generateFormattedNameFromFullName($fullname, 'firstname_lastname'); $this->assertEquals($expected_username, $user['username']); } @@ -83,9 +83,36 @@ class UserTest extends BaseTest { $fullname = "Natalia"; $expected_username = 'natalia'; - $user = User::generateFormattedNameFromFullName($fullname, 'firstname_lastname'); + $user = User::generateFormattedNameFromFullName('firstname_lastname', $fullname); + $this->assertEquals($expected_username, $user['username']); + } + public function firstInitialDotLastname() + { + $fullname = "Natalia Allanovna Romanova-O'Shostakova"; + $expected_username = 'n.allanovnaromanovaoshostakova'; + $user = User::generateFormattedNameFromFullName($fullname, 'firstinitial.lastname'); + $this->assertEquals($expected_username, $user['username']); + } + public function lastNameUnderscoreFirstInitial() + { + $fullname = "Natalia Allanovna Romanova-O'Shostakova"; + $expected_username = 'allanovnaromanovaoshostakova_n'; + $user = User::generateFormattedNameFromFullName($fullname, 'lastname_firstinitial'); + $this->assertEquals($expected_username, $user['username']); + } + public function firstNameLastName() + { + $fullname = "Natalia Allanovna Romanova-O'Shostakova"; + $expected_username = 'nataliaallanovnaromanovaoshostakova'; + $user = User::generateFormattedNameFromFullName($fullname, 'firstnamelastname)'; + $this->assertEquals($expected_username, $user['username']); + } + public function firstNameLastInitial() + { + $fullname = "Natalia Allanovna Romanova-O'Shostakova"; + $expected_username = 'nataliaa'; + $user = User::generateFormattedNameFromFullName($fullname, 'firstnamelastinitial'); $this->assertEquals($expected_username, $user['username']); } - }