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 @@
-
-
-
+
@@ -18,90 +12,84 @@ tr {
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ this.statusText }}
-
-
-
Header Field
-
Import Field
-
Sample Value
-
-
+
+
+
+
Header Field
+
Import Field
+
Sample Value
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ activeFile.first_row[index] }}
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- {{ this.statusText }}
+
+
{{ activeFile.first_row[index] }}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ this.statusText }}
+
+
+
+
+
+
\ 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 @@
-
+
Create Client
@@ -151,7 +151,7 @@
-
+
Edit Client
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 = '