Merge branch 'develop' into snipeit_v7_laravel10

Had to re-generate composer.lock, and re-do package.json and rebuild assets as well.
This commit is contained in:
Brady Wetherington 2024-02-21 20:22:28 +00:00
commit 8f2843bfcf
124 changed files with 2684 additions and 1186 deletions

View file

@ -36,7 +36,7 @@ jobs:
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI - name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.3.0 uses: codacy/codacy-analysis-cli-action@v4.4.0
with: with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations # You can also omit the token and run the tools that support default configurations

View file

@ -1,4 +1,4 @@
FROM alpine:3.18.5 FROM alpine:3.18.6
# Apache + PHP # Apache + PHP
RUN apk add --no-cache \ RUN apk add --no-cache \
apache2 \ apache2 \
@ -29,6 +29,7 @@ RUN apk add --no-cache \
php81-sodium \ php81-sodium \
php81-redis \ php81-redis \
php81-pecl-memcached \ php81-pecl-memcached \
php81-exif \
curl \ curl \
wget \ wget \
vim \ vim \

View file

@ -45,8 +45,21 @@ DB_PASSWORD={}
Now you are ready to run the entire test suite from your terminal: Now you are ready to run the entire test suite from your terminal:
`php artisan test` ```shell
php artisan test
````
To run individual test files, you can pass the path to the test that you want to run: To run individual test files, you can pass the path to the test that you want to run:
`php artisan test tests/Unit/AccessoryTest.php` ```shell
php artisan test tests/Unit/AccessoryTest.php
```
Some tests, like ones concerning LDAP, are marked with the `@group` annotation. Those groups can be run, or excluded, using the `--group` or `--exclude-group` flags:
```shell
php artisan test --group=ldap
php artisan test --exclude-group=ldap
```
This can be helpful if a set of tests are failing because you don't have an extension, like LDAP, installed.

View file

@ -5,6 +5,151 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use ZipArchive; use ZipArchive;
class SQLStreamer {
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
private bool $reading_beginning_of_line = true;
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
if($this->statement_is_permitted && $line[0] === ' ') {
return $line;
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
// ^^^^^^ that bit should *exit* the 'perimitted' black
];
foreach($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line;
}
}
// all that is not allowed is denied.
return "";
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// \Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command class RestoreFromBackup extends Command
{ {
/** /**
@ -12,10 +157,13 @@ class RestoreFromBackup extends Command
* *
* @var string * @var string
*/ */
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"} {--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated} {filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar}'; {--no-progress : Don\'t show a progress bar}
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
/** /**
* The console command description. * The console command description.
@ -34,8 +182,6 @@ class RestoreFromBackup extends Command
parent::__construct(); parent::__construct();
} }
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
/** /**
* Execute the console command. * Execute the console command.
* *
@ -55,7 +201,7 @@ class RestoreFromBackup extends Command
return $this->error('Missing required filename'); return $this->error('Missing required filename');
} }
if (! $this->option('force') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) { if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
return $this->error('Data loss not confirmed'); return $this->error('Data loss not confirmed');
} }
@ -196,7 +342,6 @@ class RestoreFromBackup extends Command
} }
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file $boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop } // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1); // print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) { if (count($sqlfiles) != 1) {
@ -208,6 +353,17 @@ class RestoreFromBackup extends Command
//older Snipe-IT installs don't have the db-dumps subdirectory component //older Snipe-IT installs don't have the db-dumps subdirectory component
} }
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
//how to invoke the restore? //how to invoke the restore?
$pipes = []; $pipes = [];
@ -228,6 +384,7 @@ class RestoreFromBackup extends Command
return $this->error('Unable to invoke mysql via CLI'); return $this->error('Unable to invoke mysql via CLI');
} }
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
@ -238,9 +395,9 @@ class RestoreFromBackup extends Command
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy! //$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
$sql_stat = $za->statIndex($sqlfile_indices[0]); // FIXME - this feels like it wants to go somewhere else?
//$this->info("SQL Stat is: ".print_r($sql_stat,true)); // and it doesn't seem 'right' - if you can't get a stream to the .sql file,
$sql_contents = $za->getStream($sql_stat['name']); // why do we care what's happening with pipes and stdout and stderr?!
if ($sql_contents === false) { if ($sql_contents === false) {
$stdout = fgets($pipes[1]); $stdout = fgets($pipes[1]);
$this->info($stdout); $this->info($stdout);
@ -249,10 +406,12 @@ class RestoreFromBackup extends Command
return false; return false;
} }
$bytes_read = 0;
try { try {
while (($buffer = fgets($sql_contents, self::$buffer_size)) !== false) { if ( $this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer); $bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'"); // \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer); $bytes_written = fwrite($pipes[0], $buffer);
@ -261,8 +420,13 @@ class RestoreFromBackup extends Command
throw new Exception("Unable to write to pipe"); throw new Exception("Unable to write to pipe");
} }
} }
} else {
$sql_importer = new SQLStreamer($sql_contents, $pipes[0], $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
}
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error("Error during restore!!!! ".$e->getMessage()); \Log::error("Error during restore!!!! ".$e->getMessage());
// FIXME - put these back and/or put them in the right places?!
$err_out = fgets($pipes[1]); $err_out = fgets($pipes[1]);
$err_err = fgets($pipes[2]); $err_err = fgets($pipes[2]);
\Log::error("Error OUTPUT: ".$err_out); \Log::error("Error OUTPUT: ".$err_out);
@ -271,7 +435,6 @@ class RestoreFromBackup extends Command
$this->error($err_err); $this->error($err_err);
throw $e; throw $e;
} }
if (!feof($sql_contents) || $bytes_read == 0) { if (!feof($sql_contents) || $bytes_read == 0) {
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!"); return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
} }
@ -303,7 +466,7 @@ class RestoreFromBackup extends Command
$fp = $za->getStream($ugly_file_name); $fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true)); //$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w'); $migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, self::$buffer_size)) !== false) { while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer); fwrite($migrated_file, $buffer);
} }
fclose($migrated_file); fclose($migrated_file);

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Accessories;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Accessory; use App\Models\Accessory;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Accessory\HttpFoundation\JsonResponse; use Symfony\Accessory\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class AccessoriesFilesController extends Controller class AccessoriesFilesController extends Controller
{ {
/** /**
* Validates and stores files associated with a accessory. * Validates and stores files associated with a accessory.
* *
* @todo Switch to using the AssetFileRequest form request validator. * @param UploadFileRequest $request
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param int $accessoryId * @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/ */
public function store(AssetFileRequest $request, $accessoryId = null) public function store(UploadFileRequest $request, $accessoryId = null)
{ {
if (config('app.lock_passwords')) { if (config('app.lock_passwords')) {
@ -45,30 +44,7 @@ class AccessoriesFilesController extends Controller
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension(); $file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
$file_name = 'accessory-'.$accessory->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/accessories/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/accessories/'.$file_name, file_get_contents($file));
}
//Log the upload to the log //Log the upload to the log
$accessory->logUpload($file_name, e($request->input('notes'))); $accessory->logUpload($file_name, e($request->input('notes')));
} }

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\AccessoriesTransformer; use App\Http\Transformers\AccessoriesTransformer;
@ -278,7 +279,7 @@ class AccessoriesController extends Controller
public function checkout(Request $request, $accessoryId) public function checkout(Request $request, $accessoryId)
{ {
// Check if the accessory exists // Check if the accessory exists
if (is_null($accessory = Accessory::find($accessoryId))) { if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
} }
@ -302,7 +303,7 @@ class AccessoriesController extends Controller
'note' => $request->get('note'), 'note' => $request->get('note'),
]); ]);
$accessory->logCheckout($request->input('note'), $user); event(new CheckoutableCheckedOut($accessory, $user, Auth::user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
} }

View file

@ -36,7 +36,8 @@ class AssetMaintenancesController extends Controller
{ {
$this->authorize('view', Asset::class); $this->authorize('view', Asset::class);
$maintenances = AssetMaintenance::select('asset_maintenances.*')->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'admin'); $maintenances = AssetMaintenance::select('asset_maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'admin');
if ($request->filled('search')) { if ($request->filled('search')) {
$maintenances = $maintenances->TextSearch($request->input('search')); $maintenances = $maintenances->TextSearch($request->input('search'));
@ -47,7 +48,7 @@ class AssetMaintenancesController extends Controller
} }
if ($request->filled('supplier_id')) { if ($request->filled('supplier_id')) {
$maintenances->where('supplier_id', '=', $request->input('supplier_id')); $maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
} }
if ($request->filled('asset_maintenance_type')) { if ($request->filled('asset_maintenance_type')) {
@ -70,10 +71,13 @@ class AssetMaintenancesController extends Controller
'notes', 'notes',
'asset_tag', 'asset_tag',
'asset_name', 'asset_name',
'serial',
'user_id', 'user_id',
'supplier', 'supplier',
'is_warranty', 'is_warranty',
'status_label',
]; ];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc'; $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at'; $sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
@ -90,6 +94,12 @@ class AssetMaintenancesController extends Controller
case 'asset_name': case 'asset_name':
$maintenances = $maintenances->OrderByAssetName($order); $maintenances = $maintenances->OrderByAssetName($order);
break; break;
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
default: default:
$maintenances = $maintenances->orderBy($sort, $order); $maintenances = $maintenances->orderBy($sort, $order);
break; break;

View file

@ -906,6 +906,13 @@ class AssetsController extends Controller
$originalValues['action_date'] = $checkin_at; $originalValues['action_date'] = $checkin_at;
} }
if(!empty($asset->licenseseats->all())){
foreach ($asset->licenseseats as $seat){
$seat->assigned_to = null;
$seat->save();
}
}
if ($asset->save()) { if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues)); event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));

View file

@ -40,7 +40,9 @@ class CompaniesController extends Controller
'components_count', 'components_count',
]; ];
$companies = Company::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count'); $companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow();
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
if ($request->filled('search')) { if ($request->filled('search')) {
$companies->TextSearch($request->input('search')); $companies->TextSearch($request->input('search'));

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\ConsumablesTransformer; use App\Http\Transformers\ConsumablesTransformer;
@ -11,6 +12,7 @@ use App\Models\Consumable;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Auth;
class ConsumablesController extends Controller class ConsumablesController extends Controller
{ {
@ -290,15 +292,7 @@ class ConsumablesController extends Controller
] ]
); );
// Log checkout event event(new CheckoutableCheckedOut($consumable, $user, Auth::user(), $request->input('note')));
$logaction = $consumable->logCheckout($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('success', null, trans('admin/consumables/message.checkout.success')));

View file

@ -25,9 +25,27 @@ class LocationsController extends Controller
{ {
$this->authorize('view', Location::class); $this->authorize('view', Location::class);
$allowed_columns = [ $allowed_columns = [
'id', 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', 'created_at', 'id',
'updated_at', 'manager_id', 'image', 'name',
'assigned_assets_count', 'users_count', 'assets_count','assigned_assets_count', 'assets_count', 'rtd_assets_count', 'currency', 'ldap_ou', ]; 'address',
'address2',
'city',
'state',
'country',
'zip',
'created_at',
'updated_at',
'manager_id',
'image',
'assigned_assets_count',
'users_count',
'assets_count',
'assigned_assets_count',
'assets_count',
'rtd_assets_count',
'currency',
'ldap_ou',
];
$locations = Location::with('parent', 'manager', 'children')->select([ $locations = Location::with('parent', 'manager', 'children')->select([
'locations.id', 'locations.id',
@ -50,6 +68,7 @@ class LocationsController extends Controller
])->withCount('assignedAssets as assigned_assets_count') ])->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count'); ->withCount('users as users_count');
if ($request->filled('search')) { if ($request->filled('search')) {
@ -80,6 +99,10 @@ class LocationsController extends Controller
$locations->where('locations.country', '=', $request->input('country')); $locations->where('locations.country', '=', $request->input('country'));
} }
if ($request->filled('manager_id')) {
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits // Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value'); $offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
$limit = app('api_limit_value'); $limit = app('api_limit_value');

View file

@ -33,7 +33,12 @@ class ReportsController extends Controller
if (($request->filled('item_type')) && ($request->filled('item_id'))) { if (($request->filled('item_type')) && ($request->filled('item_id'))) {
$actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id')) $actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type'))); ->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')))
->orWhere(function($query) use ($request)
{
$query->where('target_id', '=', $request->input('item_id'))
->where('target_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
});
} }
if ($request->filled('action_type')) { if ($request->filled('action_type')) {

View file

@ -148,7 +148,7 @@ class SettingsController extends Controller
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return Redirect * @return JsonResponse
*/ */
public function ajaxTestEmail() public function ajaxTestEmail()
{ {
@ -170,7 +170,7 @@ class SettingsController extends Controller
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0] * @since [v5.0.0]
* @return Response * @return JsonResponse
*/ */
public function purgeBarcodes() public function purgeBarcodes()
{ {
@ -211,7 +211,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0] * @since [v5.0.0]
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return array * @return array | JsonResponse
*/ */
public function showLoginAttempts(Request $request) public function showLoginAttempts(Request $request)
{ {
@ -229,6 +229,12 @@ class SettingsController extends Controller
} }
/**
* Lists backup files
*
* @author [A. Gianotto]
* @return array | JsonResponse
*/
public function listBackups() { public function listBackups() {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$path = 'app/backups'; $path = 'app/backups';
@ -249,12 +255,12 @@ class SettingsController extends Controller
'filesize' => Setting::fileSizeConvert(Storage::size($backup_files[$f])), 'filesize' => Setting::fileSizeConvert(Storage::size($backup_files[$f])),
'modified_value' => $file_timestamp, 'modified_value' => $file_timestamp,
'modified_display' => date($settings->date_display_format.' '.$settings->time_display_format, $file_timestamp), 'modified_display' => date($settings->date_display_format.' '.$settings->time_display_format, $file_timestamp),
'backup_url' => config('app.url').'/settings/backups/download/'.basename($backup_files[$f]),
]; ];
$count++; $count++;
} }
} }
} }
@ -264,15 +270,56 @@ class SettingsController extends Controller
} }
/**
* Downloads a backup file.
* We use response()->download() here instead of Storage::download() because Storage::download()
* exhausts memory on larger files.
*
* @author [A. Gianotto]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadBackup($file) { public function downloadBackup($file) {
$path = 'app/backups'; $path = storage_path('app/backups');
if (Storage::exists($path.'/'.$file)) {
if (Storage::exists('app/backups/'.$file)) {
$headers = ['ContentType' => 'application/zip']; $headers = ['ContentType' => 'application/zip'];
return Storage::download($path.'/'.$file, $file, $headers); return response()->download($path.'/'.$file, $file, $headers);
} else { } else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
} }
} }
/**
* Determines and downloads the latest backup
*
* @author [A. Gianotto]
* @since [v6.3.1]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadLatestBackup() {
$fileData = collect();
foreach (Storage::files('app/backups') as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) == 'zip') {
$fileData->push([
'file' => $file,
'date' => Storage::lastModified($file)
]);
}
}
$newest = $fileData->sortByDesc('date')->first();
if (Storage::exists($newest['file'])) {
$headers = ['ContentType' => 'application/zip'];
return response()->download(storage_path($newest['file']), basename($newest['file']), $headers);
} else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
}
}
} }

View file

@ -277,11 +277,6 @@ class UsersController extends Controller
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value'); $offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
$limit = app('api_limit_value'); $limit = app('api_limit_value');
\Log::debug('Requested offset: '. $request->input('offset'));
\Log::debug('App offset: '. app('api_offset_value'));
\Log::debug('Actual offset: '. $offset);
\Log::debug('Limit: '. $limit);
$total = $users->count(); $total = $users->count();
$users = $users->skip($offset)->take($limit)->get(); $users = $users->skip($offset)->take($limit)->get();

View file

@ -148,30 +148,20 @@ class AssetMaintenancesController extends Controller
*/ */
public function edit($assetMaintenanceId = null) public function edit($assetMaintenanceId = null)
{ {
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
// Check if the asset maintenance exists // Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) { if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the improvement management page // Redirect to the asset maintenance management page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
->with('error', trans('admin/asset_maintenances/message.not_found')); } elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
} elseif (! $assetMaintenance->asset) { // Redirect to the asset maintenance management page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
->with('error', 'The asset associated with this maintenance does not exist.');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) { } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect(); return static::getInsufficientPermissionsRedirect();
} }
if ($assetMaintenance->completion_date == '0000-00-00') {
$assetMaintenance->completion_date = null;
}
if ($assetMaintenance->start_date == '0000-00-00') {
$assetMaintenance->start_date = null;
}
if ($assetMaintenance->cost == '0.00') {
$assetMaintenance->cost = null;
}
// Prepare Improvement Type List // Prepare Improvement Type List
$assetMaintenanceType = [ $assetMaintenanceType = [
@ -203,8 +193,10 @@ class AssetMaintenancesController extends Controller
// Check if the asset maintenance exists // Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) { if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page // Redirect to the asset maintenance management page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
->with('error', trans('admin/asset_maintenances/message.not_found')); } elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) { } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect(); return static::getInsufficientPermissionsRedirect();
} }

View file

@ -442,7 +442,6 @@ class AssetModelsController extends Controller
$del_count = 0; $del_count = 0;
foreach ($models as $model) { foreach ($models as $model) {
\Log::debug($model->id);
if ($model->assets_count > 0) { if ($model->assets_count > 0) {
$del_error_count++; $del_error_count++;
@ -452,8 +451,6 @@ class AssetModelsController extends Controller
} }
} }
\Log::debug($del_count);
\Log::debug($del_error_count);
if ($del_error_count == 0) { if ($del_error_count == 0) {
return redirect()->route('models.index') return redirect()->route('models.index')

View file

@ -3,26 +3,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\AssetModel; use App\Models\AssetModel;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use enshrined\svgSanitize\Sanitizer;
class AssetModelsFilesController extends Controller class AssetModelsFilesController extends Controller
{ {
/** /**
* Upload a file to the server. * Upload a file to the server.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @param UploadFileRequest $request
* @param AssetFileRequest $request
* @param int $modelId * @param int $modelId
* @return Redirect * @return Redirect
* @since [v1.0]
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/ */
public function store(AssetFileRequest $request, $modelId = null) public function store(UploadFileRequest $request, $modelId = null)
{ {
if (! $model = AssetModel::find($modelId)) { if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist')); return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
@ -37,27 +36,7 @@ class AssetModelsFilesController extends Controller
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension(); $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$file_name = 'model-'.$model->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension=='svg') {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/assetmodels/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/assetmodels/'.$file_name, file_get_contents($file));
}
$model->logUpload($file_name, e($request->get('notes'))); $model->logUpload($file_name, e($request->get('notes')));
} }

View file

@ -4,26 +4,25 @@ namespace App\Http\Controllers\Assets;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use enshrined\svgSanitize\Sanitizer;
class AssetFilesController extends Controller class AssetFilesController extends Controller
{ {
/** /**
* Upload a file to the server. * Upload a file to the server.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @param UploadFileRequest $request
* @param AssetFileRequest $request
* @param int $assetId * @param int $assetId
* @return Redirect * @return Redirect
* @since [v1.0]
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/ */
public function store(AssetFileRequest $request, $assetId = null) public function store(UploadFileRequest $request, $assetId = null)
{ {
if (! $asset = Asset::find($assetId)) { if (! $asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist')); return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
@ -37,28 +36,7 @@ class AssetFilesController extends Controller
} }
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$extension = $file->getClientOriginalExtension();
$file_name = 'hardware-'.$asset->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension=='svg') {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/assets/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/assets/'.$file_name, file_get_contents($file));
}
$asset->logUpload($file_name, e($request->get('notes'))); $asset->logUpload($file_name, e($request->get('notes')));
} }

View file

@ -146,7 +146,8 @@ class AssetsController extends Controller
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); $asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
} }
if ($asset->assigned_to == '') { // Set location_id to rtd_location_id ONLY if the asset isn't being checked out
if (!request('assigned_user') && !request('assigned_asset') && !request('assigned_location')) {
$asset->location_id = $request->input('rtd_location_id', null); $asset->location_id = $request->input('rtd_location_id', null);
} }
@ -521,7 +522,7 @@ class AssetsController extends Controller
public function getBarCode($assetId = null) public function getBarCode($assetId = null)
{ {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$asset = Asset::find($assetId); if ($asset = Asset::withTrashed()->find($assetId)) {
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png'; $barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png';
if (isset($asset->id, $asset->asset_tag)) { if (isset($asset->id, $asset->asset_tag)) {
@ -547,6 +548,8 @@ class AssetsController extends Controller
} }
} }
} }
return null;
}
/** /**
* Return a label for an individual asset. * Return a label for an individual asset.

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Components;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Component; use App\Models\Component;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class ComponentsFilesController extends Controller class ComponentsFilesController extends Controller
{ {
/** /**
* Validates and stores files associated with a component. * Validates and stores files associated with a component.
* *
* @todo Switch to using the AssetFileRequest form request validator. * @param UploadFileRequest $request
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param int $componentId * @param int $componentId
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/ */
public function store(AssetFileRequest $request, $componentId = null) public function store(UploadFileRequest $request, $componentId = null)
{ {
if (config('app.lock_passwords')) { if (config('app.lock_passwords')) {
@ -43,30 +42,7 @@ class ComponentsFilesController extends Controller
} }
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
$extension = $file->getClientOriginalExtension();
$file_name = 'component-'.$component->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/components/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/components/'.$file_name, file_get_contents($file));
}
//Log the upload to the log //Log the upload to the log
$component->logUpload($file_name, e($request->input('notes'))); $component->logUpload($file_name, e($request->input('notes')));

View file

@ -76,7 +76,6 @@ class ConsumableCheckoutController extends Controller
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.checkout.unavailable')); return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.checkout.unavailable'));
} }
$admin_user = Auth::user(); $admin_user = Auth::user();
$assigned_to = e($request->input('assigned_to')); $assigned_to = e($request->input('assigned_to'));

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Consumables;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Consumable; use App\Models\Consumable;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Consumable\HttpFoundation\JsonResponse; use Symfony\Consumable\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class ConsumablesFilesController extends Controller class ConsumablesFilesController extends Controller
{ {
/** /**
* Validates and stores files associated with a consumable. * Validates and stores files associated with a consumable.
* *
* @todo Switch to using the AssetFileRequest form request validator. * @param UploadFileRequest $request
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param int $consumableId * @param int $consumableId
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/ */
public function store(AssetFileRequest $request, $consumableId = null) public function store(UploadFileRequest $request, $consumableId = null)
{ {
if (config('app.lock_passwords')) { if (config('app.lock_passwords')) {
return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled')); return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
@ -42,30 +41,7 @@ class ConsumablesFilesController extends Controller
} }
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
$extension = $file->getClientOriginalExtension();
$file_name = 'consumable-'.$consumable->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/consumables/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/consumables/'.$file_name, file_get_contents($file));
}
//Log the upload to the log //Log the upload to the log
$consumable->logUpload($file_name, e($request->input('notes'))); $consumable->logUpload($file_name, e($request->input('notes')));

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Licenses;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\License; use App\Models\License;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class LicenseFilesController extends Controller class LicenseFilesController extends Controller
{ {
/** /**
* Validates and stores files associated with a license. * Validates and stores files associated with a license.
* *
* @todo Switch to using the AssetFileRequest form request validator. * @param UploadFileRequest $request
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param int $licenseId * @param int $licenseId
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/ */
public function store(AssetFileRequest $request, $licenseId = null) public function store(UploadFileRequest $request, $licenseId = null)
{ {
$license = License::find($licenseId); $license = License::find($licenseId);
@ -38,30 +37,7 @@ class LicenseFilesController extends Controller
} }
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/licenses/','license-'.$license->id, $file);
$extension = $file->getClientOriginalExtension();
$file_name = 'license-'.$license->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/licenses/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/licenses/'.$file_name, file_get_contents($file));
}
//Log the upload to the log //Log the upload to the log
$license->logUpload($file_name, e($request->input('notes'))); $license->logUpload($file_name, e($request->input('notes')));

View file

@ -8,6 +8,7 @@ use App\Models\Location;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
/** /**
* This controller handles all actions related to Locations for * This controller handles all actions related to Locations for
@ -168,7 +169,7 @@ class LocationsController extends Controller
{ {
$this->authorize('delete', Location::class); $this->authorize('delete', Location::class);
if (is_null($location = Location::find($locationId))) { if (is_null($location = Location::find($locationId))) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.not_found')); return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
} }
if ($location->users()->count() > 0) { if ($location->users()->count() > 0) {
@ -238,7 +239,7 @@ class LocationsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $locationId * @param int $locationId
* @since [v6.0.14] * @since [v6.0.14]
* @return View * @return \Illuminate\Contracts\View\View
*/ */
public function getClone($locationId = null) public function getClone($locationId = null)
{ {
@ -272,8 +273,97 @@ class LocationsController extends Controller
} }
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist')); return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
/**
* Returns a view that allows the user to bulk delete locations
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.3.1]
* @return \Illuminate\Contracts\View\View
*/
public function postBulkDelete(Request $request)
{
$locations_raw_array = $request->input('ids');
// Make sure some IDs have been selected
if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) {
$locations = Location::whereIn('id', $locations_raw_array)
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')->get();
$valid_count = 0;
foreach ($locations as $location) {
if ($location->isDeletable()) {
$valid_count++;
}
}
return view('locations/bulk-delete', compact('locations'))->with('valid_count', $valid_count);
}
return redirect()->route('models.index')
->with('error', 'You must select at least one model to edit.');
}
/**
* Checks that locations can be deleted and deletes them if they can
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.3.1]
* @return \Illuminate\Http\RedirectResponse
*/
public function postBulkDeleteStore(Request $request) {
$locations_raw_array = $request->input('ids');
if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) {
$locations = Location::whereIn('id', $locations_raw_array)->get();
$success_count = 0;
$error_count = 0;
foreach ($locations as $location) {
// Can we delete this location?
if ($location->isDeletable()) {
$location->delete();
$success_count++;
} else {
$error_count++;
}
}
\Log::debug('Success count: '.$success_count);
\Log::debug('Error count: '.$error_count);
// Complete success
if ($success_count == count($locations_raw_array)) {
return redirect()
->route('locations.index')
->with('success', trans_choice('general.bulk.delete.success', $success_count,
['object_type' => trans_choice('general.location_plural', $success_count), 'count' => $success_count]
));
}
// Partial success
if ($error_count > 0) {
return redirect()
->route('locations.index')
->with('warning', trans('general.bulk.partial_success',
['success' => $success_count, 'error' => $error_count, 'object_type' => trans('general.locations')]
));
}
}
// Nothing was selected - return to the index
return redirect()
->route('locations.index')
->with('error', trans('general.bulk.nothing_selected',
['object_type' => trans('general.locations')]
));
} }
} }

View file

@ -295,9 +295,9 @@ class ReportsController extends Controller
$actionlog->present()->actionType(), $actionlog->present()->actionType(),
e($actionlog->itemType()), e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name, ($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
($actionlog->item->serial) ? $actionlog->item->serial : null, ($actionlog->item) ? $actionlog->item->serial : null,
($actionlog->item->model) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null, (($actionlog->item) && ($actionlog->item->model)) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null,
($actionlog->item->model) ? $actionlog->item->model->model_number : null, (($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null,
$target_name, $target_name,
($actionlog->note) ? e($actionlog->note) : '', ($actionlog->note) ? e($actionlog->note) : '',
$actionlog->log_meta, $actionlog->log_meta,
@ -616,7 +616,7 @@ class ReportsController extends Controller
} }
if ($request->filled('url')) { if ($request->filled('url')) {
$header[] = trans('admin/manufacturers/table.url'); $header[] = trans('general.url');
} }

View file

@ -28,6 +28,7 @@ use App\Http\Requests\SlackSettingsRequest;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
/** /**
* This controller handles all actions related to Settings for * This controller handles all actions related to Settings for
@ -356,6 +357,7 @@ class SettingsController extends Controller
} }
$setting->default_eula_text = $request->input('default_eula_text'); $setting->default_eula_text = $request->input('default_eula_text');
$setting->load_remote = $request->input('load_remote', 0);
$setting->thumbnail_max_h = $request->input('thumbnail_max_h'); $setting->thumbnail_max_h = $request->input('thumbnail_max_h');
$setting->privacy_policy_link = $request->input('privacy_policy_link'); $setting->privacy_policy_link = $request->input('privacy_policy_link');
@ -424,8 +426,6 @@ class SettingsController extends Controller
$request->validate(['site_name' => 'required']); $request->validate(['site_name' => 'required']);
$setting->site_name = $request->input('site_name'); $setting->site_name = $request->input('site_name');
$setting->custom_css = $request->input('custom_css'); $setting->custom_css = $request->input('custom_css');
}
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo'); $setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ('1' == $request->input('clear_logo')) { if ('1' == $request->input('clear_logo')) {
@ -445,8 +445,8 @@ class SettingsController extends Controller
} }
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) { if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo); Storage::disk('public')->delete($setting->label_logo);
@ -483,6 +483,7 @@ class SettingsController extends Controller
// If they are uploading an image, validate it and upload it // If they are uploading an image, validate it and upload it
} }
}
if ($setting->save()) { if ($setting->save()) {
return redirect()->route('settings.index') return redirect()->route('settings.index')
@ -636,21 +637,21 @@ class SettingsController extends Controller
// Check if the audit interval has changed - if it has, we want to update ALL of the assets audit dates // Check if the audit interval has changed - if it has, we want to update ALL of the assets audit dates
if ($request->input('audit_interval') != $setting->audit_interval) { if ($request->input('audit_interval') != $setting->audit_interval) {
// Be careful - this could be a negative number // This could be a negative number if the user is trying to set the audit interval to a lower number than it was before
$audit_diff_months = ((int)$request->input('audit_interval') - (int)($setting->audit_interval)); $audit_diff_months = ((int)$request->input('audit_interval') - (int)($setting->audit_interval));
// Grab all of the assets that have an existing next_audit_date // Batch update the dates. We have to use this method to avoid time limit exceeded errors on very large datasets,
$assets = Asset::whereNotNull('next_audit_date')->get(); // but it DOES mean this change doesn't get logged in the action logs, since it skips the observer.
// @see https://stackoverflow.com/questions/54879160/laravel-observer-not-working-on-bulk-insert
$affected = Asset::whereNotNull('next_audit_date')
->whereNull('deleted_at')
->update(
['next_audit_date' => DB::raw('DATE_ADD(next_audit_date, INTERVAL '.$audit_diff_months.' MONTH)')]
);
\Log::debug($affected .' assets affected by audit interval update');
// Update all of the assets' next_audit_date values
foreach ($assets as $asset) {
if ($asset->next_audit_date != '') {
$old_next_audit = new \DateTime($asset->next_audit_date);
$asset->next_audit_date = $old_next_audit->modify($audit_diff_months.' month')->format('Y-m-d');
$asset->forceSave();
}
}
} }
$alert_email = rtrim($request->input('alert_email'), ','); $alert_email = rtrim($request->input('alert_email'), ',');

View file

@ -4,14 +4,13 @@ namespace App\Http\Controllers\Users;
use App\Helpers\StorageHelper; use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class UserFilesController extends Controller class UserFilesController extends Controller
@ -19,14 +18,14 @@ class UserFilesController extends Controller
/** /**
* Return JSON response with a list of user details for the getIndex() view. * Return JSON response with a list of user details for the getIndex() view.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @param UploadFileRequest $request
* @since [v1.6]
* @param AssetFileRequest $request
* @param int $userId * @param int $userId
* @return string JSON * @return string JSON
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
*/ */
public function store(AssetFileRequest $request, $userId = null) public function store(UploadFileRequest $request, $userId = null)
{ {
$user = User::find($userId); $user = User::find($userId);
$destinationPath = config('app.private_uploads').'/users'; $destinationPath = config('app.private_uploads').'/users';
@ -41,31 +40,7 @@ class UserFilesController extends Controller
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles')); return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
} }
foreach ($files as $file) { foreach ($files as $file) {
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
$extension = $file->getClientOriginalExtension();
$file_name = 'user-'.$user->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/users/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/users/'.$file_name, file_get_contents($file));
}
//Log the uploaded file to the log //Log the uploaded file to the log
$logAction = new Actionlog(); $logAction = new Actionlog();

View file

@ -42,18 +42,28 @@ class SlackSettingsForm extends Component
"icon" => 'fab fa-slack', "icon" => 'fab fa-slack',
"placeholder" => "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXX", "placeholder" => "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXX",
"link" => 'https://api.slack.com/messaging/webhooks', "link" => 'https://api.slack.com/messaging/webhooks',
"test" => "testWebhook"
), ),
"general"=> array( "general"=> array(
"name" => trans('admin/settings/general.general_webhook'), "name" => trans('admin/settings/general.general_webhook'),
"icon" => "fab fa-hashtag", "icon" => "fab fa-hashtag",
"placeholder" => trans('general.url'), "placeholder" => trans('general.url'),
"link" => "", "link" => "",
"test" => "testWebhook"
),
"google" => array(
"name" => trans('admin/settings/general.google_workspaces'),
"icon" => "fa-brands fa-google",
"placeholder" => "https://chat.googleapis.com/v1/spaces/xxxxxxxx/messages?key=xxxxxx",
"link" => "https://developers.google.com/chat/how-tos/webhooks#register_the_incoming_webhook",
"test" => "googleWebhookTest"
), ),
"microsoft" => array( "microsoft" => array(
"name" => trans('admin/settings/general.ms_teams'), "name" => trans('admin/settings/general.ms_teams'),
"icon" => "fa-brands fa-microsoft", "icon" => "fa-brands fa-microsoft",
"placeholder" => "https://abcd.webhook.office.com/webhookb2/XXXXXXX", "placeholder" => "https://abcd.webhook.office.com/webhookb2/XXXXXXX",
"link" => "https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1", "link" => "https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1",
"test" => "msTeamTestWebhook"
), ),
]; ];
@ -64,10 +74,14 @@ class SlackSettingsForm extends Component
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"]; $this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"]; $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"]; $this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint; $this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel; $this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname; $this->webhook_botname = $this->setting->webhook_botname;
$this->webhook_options = $this->setting->webhook_selected; $this->webhook_options = $this->setting->webhook_selected;
if($this->webhook_selected == 'microsoft' || $this->webhook_selected == 'google'){
$this->webhook_channel = '#NA';
}
if($this->setting->webhook_endpoint != null && $this->setting->webhook_channel != null){ if($this->setting->webhook_endpoint != null && $this->setting->webhook_channel != null){
@ -87,10 +101,14 @@ class SlackSettingsForm extends Component
$this->webhook_placeholder = $this->webhook_text[$this->webhook_selected]["placeholder"]; $this->webhook_placeholder = $this->webhook_text[$this->webhook_selected]["placeholder"];
$this->webhook_endpoint = null; $this->webhook_endpoint = null;
$this->webhook_link = $this->webhook_text[$this->webhook_selected]["link"]; $this->webhook_link = $this->webhook_text[$this->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->webhook_selected]["test"];
if($this->webhook_selected != 'slack'){ if($this->webhook_selected != 'slack'){
$this->isDisabled= ''; $this->isDisabled= '';
$this->save_button = trans('general.save'); $this->save_button = trans('general.save');
} }
if($this->webhook_selected == 'microsoft' || $this->webhook_selected == 'google'){
$this->webhook_channel = '#NA';
}
} }
@ -151,6 +169,7 @@ class SlackSettingsForm extends Component
} }
public function clearSettings(){ public function clearSettings(){
if (Helper::isDemoMode()) { if (Helper::isDemoMode()) {
@ -187,6 +206,34 @@ class SlackSettingsForm extends Component
} }
} }
public function googleWebhookTest(){
$payload = [
"text" => trans('general.webhook_test_msg', ['app' => $this->webhook_name]),
];
try {
$response = Http::withHeaders([
'content-type' => 'applications/json',
])->post($this->webhook_endpoint,
$payload)->throw();
if (($response->getStatusCode() == 302) || ($response->getStatusCode() == 301)) {
return session()->flash('error', trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint]));
}
$this->isDisabled='';
$this->save_button = trans('general.save');
return session()->flash('success' , trans('admin/settings/message.webhook.success', ['webhook_name' => $this->webhook_name]));
} catch (\Exception $e) {
$this->isDisabled='disabled';
$this->save_button = trans('admin/settings/general.webhook_presave');
return session()->flash('error' , trans('admin/settings/message.webhook.error', ['error_message' => $e->getMessage(), 'app' => $this->webhook_name]));
}
}
public function msTeamTestWebhook(){ public function msTeamTestWebhook(){
$payload = $payload =

View file

@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests;
class AssetFileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp|max:'.$max_file_size,
];
}
}

View file

@ -97,22 +97,41 @@ class ImageUploadRequest extends Request
if (!config('app.lock_passwords')) { if (!config('app.lock_passwords')) {
$ext = $image->getClientOriginalExtension(); $ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext; $file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
\Log::info('File name will be: '.$file_name); \Log::info('File name will be: '.$file_name);
\Log::debug('File extension is: '.$ext); \Log::debug('File extension is: '.$ext);
if (($image->getClientOriginalExtension() !== 'webp') && ($image->getClientOriginalExtension() !== 'svg')) { if ($image->getMimeType() == 'image/webp') {
// If the file is a webp, we need to just move it since webp support
// needs to be compiled into gd for resizing to be available
\Log::debug('This is a webp, just move it');
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
} elseif($image->getMimeType() == 'image/svg+xml') {
// If the file is an SVG, we need to clean it and NOT encode it
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::disk('public')->put($path . '/' . $file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug($e);
}
} else {
\Log::debug('Not an SVG or webp - resize'); \Log::debug('Not an SVG or webp - resize');
\Log::debug('Trying to upload to: '.$path.'/'.$file_name); \Log::debug('Trying to upload to: '.$path.'/'.$file_name);
try { try {
$upload = Image::make($image->getRealPath())->resize(null, $w, function ($constraint) { $upload = Image::make($image->getRealPath())->setFileInfoFromPath($image->getRealPath())->resize(null, $w, function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
$constraint->upsize(); $constraint->upsize();
}); })->orientate();
} catch(NotReadableException $e) { } catch(NotReadableException $e) {
\Log::debug($e); \Log::debug($e);
$validator = \Validator::make([], []); $validator = \Validator::make([], []);
@ -124,27 +143,6 @@ class ImageUploadRequest extends Request
// This requires a string instead of an object, so we use ($string) // This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($path.'/'.$file_name, (string) $upload->encode()); Storage::disk('public')->put($path.'/'.$file_name, (string) $upload->encode());
} else {
// If the file is a webp, we need to just move it since webp support
// needs to be compiled into gd for resizing to be available
if ($image->getClientOriginalExtension() == 'webp') {
\Log::debug('This is a webp, just move it');
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
// If the file is an SVG, we need to clean it and NOT encode it
} else {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
\Log::debug('Trying to upload to: '.$path.'/'.$file_name);
Storage::disk('public')->put($path.'/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
}
} }
// Remove Current image if exists // Remove Current image if exists

View file

@ -0,0 +1,70 @@
<?php
namespace App\Http\Requests;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
class UploadFileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp|max:'.$max_file_size,
];
}
/**
* Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename
*
* TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
* a way to merge them or extend one into the other?
*/
public function handleFile(string $dirname, string $name_prefix, $file): string
{
$extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
\Log::debug("Your filetype IS: ".$file->getMimeType());
// Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put($dirname.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file));
\Log::debug("Here are the '$put_results' (should be 0 or 1 or true or false or something?)");
}
return $file_name;
}
}

View file

@ -28,12 +28,20 @@ class AssetMaintenancesTransformer
'id' => (int) $assetmaintenance->asset->id, 'id' => (int) $assetmaintenance->asset->id,
'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null, 'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null,
'asset_tag'=> e($assetmaintenance->asset->asset_tag), 'asset_tag'=> e($assetmaintenance->asset->asset_tag),
'serial'=> e($assetmaintenance->asset->serial),
'deleted_at'=> e($assetmaintenance->asset->deleted_at),
'created_at'=> e($assetmaintenance->asset->created_at),
] : null, ] : null,
'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [ 'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
'id' => (int) $assetmaintenance->asset->model->id, 'id' => (int) $assetmaintenance->asset->model->id,
'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null, 'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
] : null, ] : null,
'status_label' => ($assetmaintenance->asset->assetstatus) ? [
'id' => (int) $assetmaintenance->asset->assetstatus->id,
'name'=> e($assetmaintenance->asset->assetstatus->name),
'status_type'=> e($assetmaintenance->asset->assetstatus->getStatuslabelType()),
'status_meta' => e($assetmaintenance->asset->present()->statusMeta),
] : null,
'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [ 'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [
'id' => (int) $assetmaintenance->asset->company->id, 'id' => (int) $assetmaintenance->asset->company->id,
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null, 'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
@ -64,7 +72,7 @@ class AssetMaintenancesTransformer
]; ];
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [
'update' => Gate::allows('update', Asset::class), 'update' => (Gate::allows('update', Asset::class) && ($assetmaintenance->asset->deleted_at=='')) ? true : false,
'delete' => Gate::allows('delete', Asset::class), 'delete' => Gate::allows('delete', Asset::class),
]; ];

View file

@ -65,6 +65,9 @@ class LocationsTransformer
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [
'update' => Gate::allows('update', Location::class) ? true : false, 'update' => Gate::allows('update', Location::class) ? true : false,
'delete' => $location->isDeletable(), 'delete' => $location->isDeletable(),
'bulk_selectable' => [
'delete' => $location->isDeletable()
],
'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')), 'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')),
]; ];

View file

@ -46,10 +46,9 @@ class AccessoryImporter extends ItemImporter
$this->item['min_amt'] = $this->findCsvMatch($row, "min_amt"); $this->item['min_amt'] = $this->findCsvMatch($row, "min_amt");
$accessory->fill($this->sanitizeItemForStoring($accessory)); $accessory->fill($this->sanitizeItemForStoring($accessory));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything. // This sets an attribute on the Loggable trait for the action log
// $accessory->unsetEventDispatcher(); $accessory->setImported(true);
if ($accessory->save()) { if ($accessory->save()) {
$accessory->logCreate('Imported using CSV Importer');
$this->log('Accessory '.$this->item['name'].' was created'); $this->log('Accessory '.$this->item['name'].' was created');
return; return;

View file

@ -135,10 +135,10 @@ class AssetImporter extends ItemImporter
$asset->{$custom_field} = $val; $asset->{$custom_field} = $val;
} }
} }
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
if ($asset->save()) { if ($asset->save()) {
$asset->logCreate(trans('general.importer.import_note'));
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created'); $this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// If we have a target to checkout to, lets do so. // If we have a target to checkout to, lets do so.

View file

@ -48,10 +48,10 @@ class ComponentImporter extends ItemImporter
$this->log('No matching component, creating one'); $this->log('No matching component, creating one');
$component = new Component; $component = new Component;
$component->fill($this->sanitizeItemForStoring($component)); $component->fill($this->sanitizeItemForStoring($component));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
$component->unsetEventDispatcher(); // This sets an attribute on the Loggable trait for the action log
$component->setImported(true);
if ($component->save()) { if ($component->save()) {
$component->logCreate('Imported using CSV Importer');
$this->log('Component '.$this->item['name'].' was created'); $this->log('Component '.$this->item['name'].' was created');
// If we have an asset tag, checkout to that asset. // If we have an asset tag, checkout to that asset.

View file

@ -45,10 +45,10 @@ class ConsumableImporter extends ItemImporter
$this->item['item_no'] = trim($this->findCsvMatch($row, 'item_number')); $this->item['item_no'] = trim($this->findCsvMatch($row, 'item_number'));
$this->item['min_amt'] = trim($this->findCsvMatch($row, "min_amt")); $this->item['min_amt'] = trim($this->findCsvMatch($row, "min_amt"));
$consumable->fill($this->sanitizeItemForStoring($consumable)); $consumable->fill($this->sanitizeItemForStoring($consumable));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
$consumable->unsetEventDispatcher(); // This sets an attribute on the Loggable trait for the action log
$consumable->setImported(true);
if ($consumable->save()) { if ($consumable->save()) {
$consumable->logCreate('Imported using CSV Importer');
$this->log('Consumable '.$this->item['name'].' was created'); $this->log('Consumable '.$this->item['name'].' was created');
return; return;

View file

@ -85,10 +85,10 @@ class LicenseImporter extends ItemImporter
} else { } else {
$license->fill($this->sanitizeItemForStoring($license)); $license->fill($this->sanitizeItemForStoring($license));
} }
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
// $license->unsetEventDispatcher(); // This sets an attribute on the Loggable trait for the action log
$license->setImported(true);
if ($license->save()) { if ($license->save()) {
$license->logCreate('Imported using csv importer');
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created'); $this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// Lets try to checkout seats if the fields exist and we have seats. // Lets try to checkout seats if the fields exist and we have seats.

View file

@ -60,16 +60,18 @@ class CheckoutableListener
if ($this->shouldSendWebhookNotification()) { if ($this->shouldSendWebhookNotification()) {
//slack doesn't include the url in its messaging format so this is needed to hit the endpoint //slack doesn't include the url in its messaging format so this is needed to hit the endpoint
if(Setting::getSettings()->webhook_selected =='slack') {
if(Setting::getSettings()->webhook_selected =='slack' || Setting::getSettings()->webhook_selected =='general') {
Notification::route('slack', Setting::getSettings()->webhook_endpoint) Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event)); ->notify($this->getCheckoutNotification($event));
} }
} }
} catch (ClientException $e) { } catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage()); Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) { } catch (Exception $e) {
Log::error("Exception caught during checkout notification: " . $e->getMessage()); Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} }
} }
@ -113,7 +115,7 @@ class CheckoutableListener
); );
} }
//slack doesn't include the url in its messaging format so this is needed to hit the endpoint //slack doesn't include the url in its messaging format so this is needed to hit the endpoint
if(Setting::getSettings()->webhook_selected =='slack') { if(Setting::getSettings()->webhook_selected =='slack' || Setting::getSettings()->webhook_selected =='general') {
if ($this->shouldSendWebhookNotification()) { if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint) Notification::route('slack', Setting::getSettings()->webhook_endpoint)
@ -122,9 +124,9 @@ class CheckoutableListener
} }
} catch (ClientException $e) { } catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage()); Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) { } catch (Exception $e) {
Log::error("Exception caught during checkin notification: " . $e->getMessage()); Log::warning("Exception caught during checkin notification: " . $e->getMessage());
} }
} }

View file

@ -19,6 +19,9 @@ class Actionlog extends SnipeModel
{ {
use HasFactory; use HasFactory;
// This is to manually set the source (via setActionSource()) for determineActionSource()
protected ?string $source = null;
protected $presenter = \App\Presenters\ActionlogPresenter::class; protected $presenter = \App\Presenters\ActionlogPresenter::class;
use SoftDeletes; use SoftDeletes;
use Presentable; use Presentable;
@ -341,7 +344,12 @@ class Actionlog extends SnipeModel
* @since v6.3.0 * @since v6.3.0
* @return string * @return string
*/ */
public function determineActionSource() { public function determineActionSource(): string
{
// This is a manually set source
if($this->source) {
return $this->source;
}
// This is an API call // This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json')) if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
@ -358,4 +366,10 @@ class Actionlog extends SnipeModel
return 'cli/unknown'; return 'cli/unknown';
} }
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{
$this->source = $source;
}
} }

View file

@ -1560,7 +1560,7 @@ class Asset extends Depreciable
* *
* In short, this set of statements tells the query builder to ONLY query against an * In short, this set of statements tells the query builder to ONLY query against an
* actual field that's being passed if it doesn't meet known relational fields. This * actual field that's being passed if it doesn't meet known relational fields. This
* allows us to query custom fields directly in the assetsv table * allows us to query custom fields directly in the assets table
* (regardless of their name) and *skip* any fields that we already know can only be * (regardless of their name) and *skip* any fields that we already know can only be
* searched through relational searches that we do earlier in this method. * searched through relational searches that we do earlier in this method.
* *

View file

@ -62,7 +62,15 @@ class AssetMaintenance extends Model implements ICompanyableChild
* *
* @var array * @var array
*/ */
protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date']; protected $searchableAttributes =
[
'title',
'notes',
'asset_maintenance_type',
'cost',
'start_date',
'completion_date'
];
/** /**
* The relations and their attributes that should be included when searching the model. * The relations and their attributes that should be included when searching the model.
@ -70,9 +78,10 @@ class AssetMaintenance extends Model implements ICompanyableChild
* @var array * @var array
*/ */
protected $searchableRelations = [ protected $searchableRelations = [
'asset' => ['name', 'asset_tag'], 'asset' => ['name', 'asset_tag', 'serial'],
'asset.model' => ['name', 'model_number'], 'asset.model' => ['name', 'model_number'],
'asset.supplier' => ['name'], 'asset.supplier' => ['name'],
'asset.assetstatus' => ['name'],
'supplier' => ['name'], 'supplier' => ['name'],
]; ];
@ -197,6 +206,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
->orderBy('suppliers_maintenances.name', $order); ->orderBy('suppliers_maintenances.name', $order);
} }
/** /**
* Query builder scope to order on admin user * Query builder scope to order on admin user
* *
@ -239,4 +249,33 @@ class AssetMaintenance extends Model implements ICompanyableChild
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id') return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order); ->orderBy('assets.name', $order);
} }
/**
* Query builder scope to order on serial
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderByAssetSerial($query, $order)
{
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order);
}
/**
* Query builder scope to order on status label name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderStatusName($query, $order)
{
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order);
}
} }

View file

@ -113,6 +113,14 @@ final class Company extends SnipeModel
} }
} }
/**
* Get the company id for the current user taking into
* account the full multiple company support setting
* and if the current user is a super user.
*
* @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForCurrentUser($unescaped_input) public static function getIdForCurrentUser($unescaped_input)
{ {
if (! static::isFullMultipleCompanySupportEnabled()) { if (! static::isFullMultipleCompanySupportEnabled()) {

View file

@ -95,7 +95,10 @@ class Location extends SnipeModel
/** /**
* Determine whether or not this location can be deleted * Determine whether or not this location can be deleted.
*
* This method requires the eager loading of the relationships in order to determine whether
* it can be deleted. It's tempting to load those here, but that increases the query load considerably.
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v3.0] * @since [v3.0]
@ -104,9 +107,10 @@ class Location extends SnipeModel
public function isDeletable() public function isDeletable()
{ {
return Gate::allows('delete', $this) return Gate::allows('delete', $this)
&& ($this->assignedAssets()->count() === 0) && ($this->assets_count === 0)
&& ($this->assets()->count() === 0) && ($this->assigned_assets_count === 0)
&& ($this->users()->count() === 0); && ($this->children_count === 0)
&& ($this->users_count === 0);
} }
/** /**

View file

@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Auth;
trait Loggable trait Loggable
{ {
// an attribute for setting whether or not the item was imported
public ?bool $imported = false;
/** /**
* @author Daniel Meltzer <dmeltzer.devel@gmail.com> * @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @since [v3.4] * @since [v3.4]
@ -18,6 +21,11 @@ trait Loggable
return $this->morphMany(Actionlog::class, 'item'); return $this->morphMany(Actionlog::class, 'item');
} }
public function setImported(bool $bool): void
{
$this->imported = $bool;
}
/** /**
* @author Daniel Meltzer <dmeltzer.devel@gmail.com> * @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @since [v3.4] * @since [v3.4]

View file

@ -352,7 +352,6 @@ class Setting extends Model
'ldap_client_tls_cert', 'ldap_client_tls_cert',
'ldap_default_group', 'ldap_default_group',
'ldap_dept', 'ldap_dept',
'ldap_emp_num',
'ldap_phone_field', 'ldap_phone_field',
'ldap_jobtitle', 'ldap_jobtitle',
'ldap_manager', 'ldap_manager',

View file

@ -9,6 +9,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -38,13 +43,17 @@ class CheckinAccessoryNotification extends Notification
public function via() public function via()
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -54,34 +63,8 @@ class CheckinAccessoryNotification extends Notification
if ($this->target instanceof User && $this->target->email != '') { if ($this->target instanceof User && $this->target->email != '') {
\Log::debug('The target is a user'); \Log::debug('The target is a user');
/**
* Send an email if the asset requires acceptance,
* so the user can accept or decline the asset
*/
if (($this->item->requireAcceptance()) || ($this->item->getEula()) || ($this->item->checkin_email())) {
$notifyBy[] = 'mail';
}
/**
* Send an email if the asset requires acceptance,
* so the user can accept or decline the asset
*/
if ($this->item->requireAcceptance()) {
\Log::debug('This accessory requires acceptance');
}
/**
* Send an email if the item has a EULA, since the user should always receive it
*/
if ($this->item->getEula()) {
\Log::debug('This accessory has a EULA');
}
/**
* Send an email if an email should be sent at checkin/checkout
*/
if ($this->item->checkin_email()) { if ($this->item->checkin_email()) {
\Log::debug('This accessory has a checkin_email()'); $notifyBy[] = 'mail';
} }
} }
@ -132,6 +115,32 @@ class CheckinAccessoryNotification extends Notification
->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining())
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Accessory_Checkin_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.checked_into').': '.$item->location->name ? $item->location->name : '',
trans('admin/consumables/general.remaining').': '.$item->numRemaining(),
trans('admin/hardware/form.notes').": ".$note ?: '',
)
->onClick(route('accessories.show', $item->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -10,6 +10,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -46,12 +51,16 @@ class CheckinAssetNotification extends Notification
public function via() public function via()
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
\Log::debug('use webhook'); \Log::debug('use webhook');
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -108,6 +117,33 @@ class CheckinAssetNotification extends Notification
->fact(trans('admin/hardware/form.status'), $item->assetstatus->name) ->fact(trans('admin/hardware/form.status'), $item->assetstatus->name)
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Asset_Checkin_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.checked_into') ?: '',
$item->location->name ? $item->location->name : '',
trans('admin/hardware/form.status').": ".$item->assetstatus->name,
)
->onClick(route('hardware.show', $item->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -9,6 +9,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -43,12 +48,16 @@ class CheckinLicenseSeatNotification extends Notification
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -113,6 +122,34 @@ class CheckinLicenseSeatNotification extends Notification
->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count()) ->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count())
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.License_Checkin_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.checkedin_from') ?: '',
$target->present()->fullName() ?: '',
trans('admin/consumables/general.remaining').': '.$item->availCount()->count(),
)
->onClick(route('licenses.show', $item->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -9,6 +9,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -37,13 +42,17 @@ class CheckoutAccessoryNotification extends Notification
public function via() public function via()
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -123,6 +132,34 @@ class CheckoutAccessoryNotification extends Notification
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Accessory_Checkout_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.assigned_to') ?: '',
$target->present()->name ?: '',
trans('admin/consumables/general.remaining').": ". $item->numRemaining(),
)
->onClick(route('users.show', $target->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -11,6 +11,13 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\Enums\Icon;
use NotificationChannels\GoogleChat\Enums\ImageStyle;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -54,13 +61,20 @@ class CheckoutAssetNotification extends Notification
*/ */
public function via() public function via()
{ {
$notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
return [MicrosoftTeamsChannel::class]; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
$notifyBy = [];
if ((Setting::getSettings()) && (Setting::getSettings()->webhook_selected == 'slack')) {
if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
\Log::debug('use webhook'); \Log::debug('use webhook');
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -143,6 +157,33 @@ class CheckoutAssetNotification extends Notification
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Asset_Checkout_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.assigned_to') ?: '',
$target->present()->name ?: '',
$note ?: '',
)
->onClick(route('users.show', $target->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -9,6 +9,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -44,13 +49,17 @@ class CheckoutConsumableNotification extends Notification
public function via() public function via()
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -128,6 +137,33 @@ class CheckoutConsumableNotification extends Notification
->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining())
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Consumable_checkout_notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.assigned_to') ?: '',
$target->present()->fullName() ?: '',
trans('admin/consumables/general.remaining').': '.$item->numRemaining(),
)
->onClick(route('users.show', $target->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -9,6 +9,11 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
@ -43,15 +48,18 @@ class CheckoutLicenseSeatNotification extends Notification
*/ */
public function via() public function via()
{ {
$notifyBy = []; $notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google'){
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft'){ if (Setting::getSettings()->webhook_selected == 'microsoft'){
$notifyBy[] = MicrosoftTeamsChannel::class; $notifyBy[] = MicrosoftTeamsChannel::class;
} }
if (Setting::getSettings()->webhook_selected == 'slack') { if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = 'slack'; $notifyBy[] = 'slack';
} }
@ -129,6 +137,33 @@ class CheckoutLicenseSeatNotification extends Notification
->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count()) ->fact(trans('admin/consumables/general.remaining'), $item->availCount()->count())
->fact(trans('mail.notes'), $note ?: ''); ->fact(trans('mail.notes'), $note ?: '');
} }
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.License_Checkout_Notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.assigned_to') ?: '',
$target->present()->name ?: '',
trans('admin/consumables/general.remaining').': '.$item->availCount()->count(),
)
->onClick(route('users.show', $target->id))
)
)
);
}
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.

View file

@ -38,6 +38,9 @@ class AccessoryObserver
$logAction->item_id = $accessory->id; $logAction->item_id = $accessory->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id(); $logAction->user_id = Auth::id();
if($accessory->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create'); $logAction->logaction('create');
} }

View file

@ -109,6 +109,9 @@ class AssetObserver
$logAction->item_id = $asset->id; $logAction->item_id = $asset->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id(); $logAction->user_id = Auth::id();
if($asset->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create'); $logAction->logaction('create');
} }

View file

@ -38,6 +38,9 @@ class ComponentObserver
$logAction->item_id = $component->id; $logAction->item_id = $component->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id(); $logAction->user_id = Auth::id();
if($component->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create'); $logAction->logaction('create');
} }

View file

@ -38,6 +38,9 @@ class ConsumableObserver
$logAction->item_id = $consumable->id; $logAction->item_id = $consumable->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id(); $logAction->user_id = Auth::id();
if($consumable->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create'); $logAction->logaction('create');
} }

View file

@ -38,6 +38,9 @@ class LicenseObserver
$logAction->item_id = $license->id; $logAction->item_id = $license->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id(); $logAction->user_id = Auth::id();
if($license->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create'); $logAction->logaction('create');
} }

View file

@ -41,6 +41,19 @@ class AssetMaintenancesPresenter extends Presenter
'sortable' => true, 'sortable' => true,
'title' => trans('admin/hardware/table.asset_tag'), 'title' => trans('admin/hardware/table.asset_tag'),
'formatter' => 'assetTagLinkFormatter', 'formatter' => 'assetTagLinkFormatter',
], [
'field' => 'serial',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/hardware/table.serial'),
'formatter' => 'assetSerialLinkFormatter',
], [
'field' => 'status_label',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/hardware/table.status'),
'visible' => true,
'formatter' => 'statuslabelsLinkObjFormatter',
], [ ], [
'field' => 'model', 'field' => 'model',
'searchable' => true, 'searchable' => true,

View file

@ -14,7 +14,11 @@ class LocationPresenter extends Presenter
public static function dataTableLayout() public static function dataTableLayout()
{ {
$layout = [ $layout = [
[
'field' => 'bulk_selectable',
'checkbox' => true,
'formatter' => 'checkboxEnabledFormatter',
],
[ [
'field' => 'id', 'field' => 'id',
'searchable' => false, 'searchable' => false,

View file

@ -45,7 +45,7 @@ class ManufacturerPresenter extends Presenter
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,
'title' => trans('admin/manufacturers/table.url'), 'title' => trans('general.url'),
'visible' => true, 'visible' => true,
'formatter' => 'externalLinkFormatter', 'formatter' => 'externalLinkFormatter',
], ],

View file

@ -39,24 +39,12 @@ class SettingsServiceProvider extends ServiceProvider
$limit = abs($int_limit); $limit = abs($int_limit);
} }
// \Log::debug('Max in env: '.config('app.max_results'));
// \Log::debug('Original requested limit: '.request('limit'));
// \Log::debug('Int limit: '.$int_limit);
// \Log::debug('Modified limit: '.$limit);
// \Log::debug('------------------------------');
return $limit; return $limit;
}); });
// Make sure the offset is actually set and is an integer // Make sure the offset is actually set and is an integer
\App::singleton('api_offset_value', function () { \App::singleton('api_offset_value', function () {
$offset = intval(request('offset')); $offset = intval(request('offset'));
// \Log::debug('Original requested offset: '.request('offset'));
// \Log::debug('Modified offset: '.$offset);
// \Log::debug('------------------------------');
return $offset; return $offset;
}); });

View file

@ -90,14 +90,10 @@ class Label implements View
$assetData->put('id', $asset->id); $assetData->put('id', $asset->id);
$assetData->put('tag', $asset->asset_tag); $assetData->put('tag', $asset->asset_tag);
if ($template->getSupportTitle()) { if ($template->getSupportTitle() && !empty($settings->label2_title)) {
$title = str_replace('{COMPANY}', data_get($asset, 'company.name'), $settings->label2_title);
if ($asset->company && !empty($settings->label2_title)) {
$title = str_replace('{COMPANY}', $asset->company->name, $settings->label2_title);
$settings->qr_text;
$assetData->put('title', $title); $assetData->put('title', $title);
} }
}
if ($template->getSupportLogo()) { if ($template->getSupportLogo()) {

View file

@ -39,6 +39,7 @@
"intervention/image": "^2.5", "intervention/image": "^2.5",
"javiereguiluz/easyslugger": "^1.0", "javiereguiluz/easyslugger": "^1.0",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel-notification-channels/google-chat": "^3.0",
"laravel-notification-channels/microsoft-teams": "^1.1", "laravel-notification-channels/microsoft-teams": "^1.1",
"laravel/helpers": "^1.4", "laravel/helpers": "^1.4",
"laravel/passport": "^11.0", "laravel/passport": "^11.0",
@ -69,7 +70,8 @@
"watson/validating": "^8.1" "watson/validating": "^8.1"
}, },
"suggest": { "suggest": {
"ext-ldap": "*" "ext-ldap": "*",
"ext-zip": "*"
}, },
"require-dev": { "require-dev": {
"brianium/paratest": "^v6.4.4", "brianium/paratest": "^v6.4.4",

934
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
<?php <?php
return array ( return array (
'app_version' => 'v6.3.0', 'app_version' => 'v6.3.1',
'full_app_version' => 'v6.3.0 - build 12490-g9136415bb', 'full_app_version' => 'v6.3.1 - build 12672-g00cea3eb3',
'build_version' => '12490', 'build_version' => '12672',
'prerelease_version' => '', 'prerelease_version' => '',
'hash_version' => 'g9136415bb', 'hash_version' => 'g00cea3eb3',
'full_hash' => 'v6.3.0-729-g9136415bb', 'full_hash' => 'v6.3.1-180-g00cea3eb3',
'branch' => 'develop', 'branch' => 'master',
); );

View file

@ -8,6 +8,7 @@ use App\Models\Location;
use App\Models\Manufacturer; use App\Models\Manufacturer;
use App\Models\Supplier; use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
class AccessoryFactory extends Factory class AccessoryFactory extends Factory
@ -33,7 +34,7 @@ class AccessoryFactory extends Factory
$this->faker->randomElement(['Keyboard', 'Wired']) $this->faker->randomElement(['Keyboard', 'Wired'])
), ),
'user_id' => User::factory()->superuser(), 'user_id' => User::factory()->superuser(),
'category_id' => Category::factory(), 'category_id' => Category::factory()->forAccessories(),
'model_number' => $this->faker->numberBetween(1000000, 50000000), 'model_number' => $this->faker->numberBetween(1000000, 50000000),
'location_id' => Location::factory(), 'location_id' => Location::factory(),
'qty' => 1, 'qty' => 1,
@ -114,4 +115,42 @@ class AccessoryFactory extends Factory
]; ];
}); });
} }
public function withoutItemsRemaining()
{
return $this->state(function () {
return [
'qty' => 1,
];
})->afterCreating(function ($accessory) {
$user = User::factory()->create();
$accessory->users()->attach($accessory->id, [
'accessory_id' => $accessory->id,
'created_at' => now(),
'user_id' => $user->id,
'assigned_to' => $user->id,
'note' => '',
]);
});
}
public function requiringAcceptance()
{
return $this->afterCreating(function ($accessory) {
$accessory->category->update(['require_acceptance' => 1]);
});
}
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Accessory $accessory) use ($user) {
$accessory->users()->attach($accessory->id, [
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => 1,
'assigned_to' => $user->id ?? User::factory()->create()->id,
]);
});
}
} }

View file

@ -172,4 +172,10 @@ class CategoryFactory extends Factory
]); ]);
} }
public function forAccessories()
{
return $this->state([
'category_type' => 'accessory',
]);
}
} }

View file

@ -91,4 +91,29 @@ class ConsumableFactory extends Factory
]; ];
}); });
} }
public function withoutItemsRemaining()
{
return $this->state(function () {
return [
'qty' => 1,
];
})->afterCreating(function (Consumable $consumable) {
$user = User::factory()->create();
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'user_id' => $user->id,
'assigned_to' => $user->id,
'note' => '',
]);
});
}
public function requiringAcceptance()
{
return $this->afterCreating(function (Consumable $consumable) {
$consumable->category->update(['require_acceptance' => 1]);
});
}
} }

30
package-lock.json generated
View file

@ -10,13 +10,13 @@
"acorn-import-assertions": "^1.9.0", "acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18", "admin-lte": "^2.4.18",
"ajv": "^6.12.6", "ajv": "^6.12.6",
"alpinejs": "^3.13.3", "alpinejs": "^3.13.5",
"blueimp-file-upload": "^9.34.0", "blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1", "bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3", "bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.10.0", "bootstrap-datepicker": "^1.10.0",
"bootstrap-less": "^3.3.8", "bootstrap-less": "^3.3.8",
"bootstrap-table": "1.22.1", "bootstrap-table": "1.22.2",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"css-loader": "^5.0.0", "css-loader": "^5.0.0",
@ -36,7 +36,7 @@
"sheetjs": "^2.0.0", "sheetjs": "^2.0.0",
"tableexport.jquery.plugin": "1.28.0", "tableexport.jquery.plugin": "1.28.0",
"tether": "^1.4.0", "tether": "^1.4.0",
"webpack": "^5.89.0" "webpack": "^5.90.0"
}, },
"devDependencies": { "devDependencies": {
"all-contributors-cli": "^6.26.1", "all-contributors-cli": "^6.26.1",
@ -3058,9 +3058,9 @@
} }
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.13.3", "version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==", "integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
@ -4144,9 +4144,9 @@
"integrity": "sha512-a9MtENtt4r3ttPW5mpIpOFmCaIsm37EGukOgw5cfHlxKvsUSN8AN9JtwKrKuqgEnxs86kUSsMvMn8kqewMorKw==" "integrity": "sha512-a9MtENtt4r3ttPW5mpIpOFmCaIsm37EGukOgw5cfHlxKvsUSN8AN9JtwKrKuqgEnxs86kUSsMvMn8kqewMorKw=="
}, },
"node_modules/bootstrap-table": { "node_modules/bootstrap-table": {
"version": "1.22.1", "version": "1.22.2",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.22.1.tgz", "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.22.2.tgz",
"integrity": "sha512-Nw8p+BmaiMDSfoer/p49YeI3vJQAWhudxhyKMuqnJBb3NRvCRewMk7JDgiN9SQO3YeSejOirKtcdWpM0dtddWg==", "integrity": "sha512-ZjZGcEXm/N7N/wAykmANWKKV+U+7AxgoNuBwWLrKbvAGT8XXS2f0OCiFmuMwpkqg7pDbF+ff9bEf/lOAlxcF1w==",
"peerDependencies": { "peerDependencies": {
"jquery": "3" "jquery": "3"
} }
@ -12183,18 +12183,18 @@
"dev": true "dev": true
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.89.0", "version": "5.90.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0", "@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/ast": "^1.11.5",
"@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5",
"@webassemblyjs/wasm-parser": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5",
"acorn": "^8.7.1", "acorn": "^8.7.1",
"acorn-import-assertions": "^1.9.0", "acorn-import-assertions": "^1.9.0",
"browserslist": "^4.14.5", "browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2", "chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.15.0", "enhanced-resolve": "^5.15.0",
"es-module-lexer": "^1.2.1", "es-module-lexer": "^1.2.1",
@ -12208,7 +12208,7 @@
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^3.2.0",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.7", "terser-webpack-plugin": "^5.3.10",
"watchpack": "^2.4.0", "watchpack": "^2.4.0",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },

View file

@ -30,13 +30,13 @@
"acorn-import-assertions": "^1.9.0", "acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18", "admin-lte": "^2.4.18",
"ajv": "^6.12.6", "ajv": "^6.12.6",
"alpinejs": "^3.13.3", "alpinejs": "^3.13.5",
"blueimp-file-upload": "^9.34.0", "blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1", "bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3", "bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.10.0", "bootstrap-datepicker": "^1.10.0",
"bootstrap-less": "^3.3.8", "bootstrap-less": "^3.3.8",
"bootstrap-table": "1.22.1", "bootstrap-table": "1.22.2",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"css-loader": "^5.0.0", "css-loader": "^5.0.0",
@ -56,6 +56,6 @@
"sheetjs": "^2.0.0", "sheetjs": "^2.0.0",
"tableexport.jquery.plugin": "1.28.0", "tableexport.jquery.plugin": "1.28.0",
"tether": "^1.4.0", "tether": "^1.4.0",
"webpack": "^5.89.0" "webpack": "^5.90.0"
} }
} }

Binary file not shown.

Binary file not shown.

3
public/evil.php Normal file
View file

@ -0,0 +1,3 @@
GIF89a
<?php echo "Hello, the date is: " . date('c');

After

Width:  |  Height:  |  Size: 55 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,5 @@
{ {
"/js/build/app.js": "/js/build/app.js?id=2004100dd5c106d15fa9b6a16d6dd341", "/js/build/app.js": "/js/build/app.js?id=a05df3d0d95cb1cb86b26e858563009f",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb", "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/build/overrides.css": "/css/build/overrides.css?id=77475bffdab35fb2cd9ebbcd3ebe6dd6", "/css/build/overrides.css": "/css/build/overrides.css?id=77475bffdab35fb2cd9ebbcd3ebe6dd6",
@ -18,7 +18,7 @@
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=0ed42b67f9b02a74815e885bfd9e3f66", "/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f", "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460", "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/all.css": "/css/dist/all.css?id=2d918bd9fb07c257fa5a0069a68a6363", "/css/dist/all.css": "/css/dist/all.css?id=a413275c9c27dbbb0aa60a5a5d81ec74",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba", "/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba",
@ -29,11 +29,11 @@
"/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=a0feb384c3c6071947a49708f2b0bc85", "/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=a0feb384c3c6071947a49708f2b0bc85",
"/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=e24ec0b8661f7fa333b29444df39e399", "/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=e24ec0b8661f7fa333b29444df39e399",
"/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=e11465c0eff0549edd4e8ea6bbcf242f", "/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=e11465c0eff0549edd4e8ea6bbcf242f",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=2bd29fa7f9d666800c246a52ce708633", "/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=afa255bf30b2a7c11a97e3165128d183",
"/js/build/vendor.js": "/js/build/vendor.js?id=db2e005808d5a2d2e7f4a82059e5d16f", "/js/build/vendor.js": "/js/build/vendor.js?id=db2e005808d5a2d2e7f4a82059e5d16f",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=1f678160a05960c3087fb8263168ff41", "/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=29340c70d13855fa0165cd4d799c6f5b",
"/js/dist/all.js": "/js/dist/all.js?id=99dcf5ec2b67d0a21ebd3ef2c05b99e4", "/js/dist/all.js": "/js/dist/all.js?id=5f4bdd1b17a98eb4b59085823cf63972",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=7f9a130eda6916eaa32a0a57e81918f3", "/js/dist/all-defer.js": "/js/dist/all-defer.js?id=19ccc62a8f1ea103dede4808837384d4",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f", "/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=0ed42b67f9b02a74815e885bfd9e3f66", "/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b", "/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b",

View file

@ -193,17 +193,12 @@ $(document).ready(function () {
* Select2 * Select2
*/ */
var iOS = /iPhone|iPad|iPod/.test(navigator.userAgent) && !window.MSStream;
if(!iOS)
{
// Vue collision: Avoid overriding a vue select2 instance
// by checking to see if the item has already been select2'd.
$('select.select2:not(".select2-hidden-accessible")').each(function (i,obj) { $('select.select2:not(".select2-hidden-accessible")').each(function (i,obj) {
{ {
$(obj).select2(); $(obj).select2();
} }
}); });
}
// $('.datepicker').datepicker(); // $('.datepicker').datepicker();
// var datepicker = $.fn.datepicker.noConflict(); // return $.fn.datepicker to previously assigned value // var datepicker = $.fn.datepicker.noConflict(); // return $.fn.datepicker to previously assigned value

View file

@ -67,9 +67,10 @@ return [
'footer_text' => 'Additional Footer Text ', 'footer_text' => 'Additional Footer Text ',
'footer_text_help' => 'This text will appear in the right-side footer. Links are allowed using <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>. Line breaks, headers, images, etc may result in unpredictable results.', 'footer_text_help' => 'This text will appear in the right-side footer. Links are allowed using <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>. Line breaks, headers, images, etc may result in unpredictable results.',
'general_settings' => 'General Settings', 'general_settings' => 'General Settings',
'general_settings_keywords' => 'company support, signature, acceptance, email format, username format, images, per page, thumbnail, eula, tos, dashboard, privacy', 'general_settings_keywords' => 'company support, signature, acceptance, email format, username format, images, per page, thumbnail, eula, gravatar, tos, dashboard, privacy',
'general_settings_help' => 'Default EULA and more', 'general_settings_help' => 'Default EULA and more',
'generate_backup' => 'Generate Backup', 'generate_backup' => 'Generate Backup',
'google_workspaces' => 'Google Workspaces',
'header_color' => 'Header Color', 'header_color' => 'Header Color',
'info' => 'These settings let you customize certain aspects of your installation.', 'info' => 'These settings let you customize certain aspects of your installation.',
'label_logo' => 'Label Logo', 'label_logo' => 'Label Logo',
@ -86,7 +87,6 @@ return [
'ldap_integration' => 'LDAP Integration', 'ldap_integration' => 'LDAP Integration',
'ldap_settings' => 'LDAP Settings', 'ldap_settings' => 'LDAP Settings',
'ldap_client_tls_cert_help' => 'Client-Side TLS Certificate and Key for LDAP connections are usually only useful in Google Workspace configurations with "Secure LDAP." Both are required.', 'ldap_client_tls_cert_help' => 'Client-Side TLS Certificate and Key for LDAP connections are usually only useful in Google Workspace configurations with "Secure LDAP." Both are required.',
'ldap_client_tls_key' => 'LDAP Client-Side TLS key',
'ldap_location' => 'LDAP Location', 'ldap_location' => 'LDAP Location',
'ldap_location_help' => 'The Ldap Location field should be used if <strong>an OU is not being used in the Base Bind DN.</strong> Leave this blank if an OU search is being used.', 'ldap_location_help' => 'The Ldap Location field should be used if <strong>an OU is not being used in the Base Bind DN.</strong> Leave this blank if an OU search is being used.',
'ldap_login_test_help' => 'Enter a valid LDAP username and password from the base DN you specified above to test whether your LDAP login is configured correctly. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.', 'ldap_login_test_help' => 'Enter a valid LDAP username and password from the base DN you specified above to test whether your LDAP login is configured correctly. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.',
@ -121,8 +121,8 @@ return [
'ldap_test' => 'Test LDAP', 'ldap_test' => 'Test LDAP',
'ldap_test_sync' => 'Test LDAP Synchronization', 'ldap_test_sync' => 'Test LDAP Synchronization',
'license' => 'Software License', 'license' => 'Software License',
'load_remote_text' => 'Remote Scripts', 'load_remote' => 'Use Gravatar',
'load_remote_help_text' => 'This Snipe-IT install can load scripts from the outside world.', 'load_remote_help_text' => 'Uncheck this box if your install cannot load scripts from the outside internet. This will prevent Snipe-IT from trying load images from Gravatar.',
'login' => 'Login Attempts', 'login' => 'Login Attempts',
'login_attempt' => 'Login Attempt', 'login_attempt' => 'Login Attempt',
'login_ip' => 'IP Address', 'login_ip' => 'IP Address',

View file

@ -182,6 +182,7 @@ return [
'lock_passwords' => 'This field value will not be saved in a demo installation.', 'lock_passwords' => 'This field value will not be saved in a demo installation.',
'feature_disabled' => 'This feature has been disabled for the demo installation.', 'feature_disabled' => 'This feature has been disabled for the demo installation.',
'location' => 'Location', 'location' => 'Location',
'location_plural' => 'Location|Locations',
'locations' => 'Locations', 'locations' => 'Locations',
'logo_size' => 'Square logos look best with Logo + Text. Logo maximum display size is 50px high x 500px wide. ', 'logo_size' => 'Square logos look best with Logo + Text. Logo maximum display size is 50px high x 500px wide. ',
'logout' => 'Logout', 'logout' => 'Logout',
@ -443,7 +444,6 @@ return [
'sample_value' => 'Sample Value', 'sample_value' => 'Sample Value',
'no_headers' => 'No Columns Found', 'no_headers' => 'No Columns Found',
'error_in_import_file' => 'There was an error reading the CSV file: :error', 'error_in_import_file' => 'There was an error reading the CSV file: :error',
'percent_complete' => ':percent % Complete',
'errors_importing' => 'Some Errors occurred while importing: ', 'errors_importing' => 'Some Errors occurred while importing: ',
'warning' => 'WARNING: :warning', 'warning' => 'WARNING: :warning',
'success_redirecting' => '"Success... Redirecting.', 'success_redirecting' => '"Success... Redirecting.',
@ -459,6 +459,7 @@ return [
'no_autoassign_licenses_help' => 'Do not include user for bulk-assigning through the license UI or cli tools.', 'no_autoassign_licenses_help' => 'Do not include user for bulk-assigning through the license UI or cli tools.',
'modal_confirm_generic' => 'Are you sure?', 'modal_confirm_generic' => 'Are you sure?',
'cannot_be_deleted' => 'This item cannot be deleted', 'cannot_be_deleted' => 'This item cannot be deleted',
'cannot_be_edited' => 'This item cannot be edited.',
'undeployable_tooltip' => 'This item cannot be checked out. Check the quantity remaining.', 'undeployable_tooltip' => 'This item cannot be checked out. Check the quantity remaining.',
'serial_number' => 'Serial Number', 'serial_number' => 'Serial Number',
'item_notes' => ':item Notes', 'item_notes' => ':item Notes',
@ -501,5 +502,17 @@ return [
'action_source' => 'Action Source', 'action_source' => 'Action Source',
'or' => 'or', 'or' => 'or',
'url' => 'URL', 'url' => 'URL',
'edit_fieldset' => 'Edit fieldset fields and options',
'bulk' => [
'delete' =>
[
'header' => 'Bulk Delete :object_type',
'warn' => 'You are about to delete one :object_type|You are about to delete :count :object_type',
'success' => ':object_type successfully deleted|Successfully deleted :count :object_type',
'error' => 'Could not delete :object_type',
'nothing_selected' => 'No :object_type selected - nothing to do',
'partial' => 'Deleted :success_count :object_type, but :error_count :object_type could not be deleted',
],
],
]; ];

View file

@ -42,6 +42,7 @@ return [
'checkin_date' => 'Checkin Date:', 'checkin_date' => 'Checkin Date:',
'checkout_date' => 'Checkout Date:', 'checkout_date' => 'Checkout Date:',
'checkedout_from' => 'Checked out from', 'checkedout_from' => 'Checked out from',
'checkedin_from' => 'Checked in from',
'checked_into' => 'Checked into', 'checked_into' => 'Checked into',
'click_on_the_link_accessory' => 'Please click on the link at the bottom to confirm that you have received the accessory.', 'click_on_the_link_accessory' => 'Please click on the link at the bottom to confirm that you have received the accessory.',
'click_on_the_link_asset' => 'Please click on the link at the bottom to confirm that you have received the asset.', 'click_on_the_link_asset' => 'Please click on the link at the bottom to confirm that you have received the asset.',

View file

@ -21,7 +21,7 @@
<i class="fas fa-barcode" aria-hidden="true"></i> <i class="fas fa-barcode" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.assets') }} <span class="hidden-xs hidden-sm">{{ trans('general.assets') }}
{!! (($company->assets) && ($company->assets()->AssetsForShow()->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($company->assets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($company->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($company->assets()->AssetsForShow()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
@ -33,7 +33,7 @@
<i class="far fa-save"></i> <i class="far fa-save"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.licenses') }} <span class="hidden-xs hidden-sm">{{ trans('general.licenses') }}
{!! (($company->licenses) && ($company->licenses->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($company->licenses->count()).'</badge>' : '' !!} {!! ($company->licenses->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($company->licenses->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -43,7 +43,7 @@
<span class="hidden-lg hidden-md"> <span class="hidden-lg hidden-md">
<i class="far fa-keyboard"></i> <i class="far fa-keyboard"></i>
</span> <span class="hidden-xs hidden-sm">{{ trans('general.accessories') }} </span> <span class="hidden-xs hidden-sm">{{ trans('general.accessories') }}
{!! (($company->accessories) && ($company->accessories->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($company->accessories->count()).'</badge>' : '' !!} {!! ($company->accessories->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($company->accessories->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -53,7 +53,7 @@
<span class="hidden-lg hidden-md"> <span class="hidden-lg hidden-md">
<i class="fas fa-tint"></i></span> <i class="fas fa-tint"></i></span>
<span class="hidden-xs hidden-sm">{{ trans('general.consumables') }} <span class="hidden-xs hidden-sm">{{ trans('general.consumables') }}
{!! (($company->consumables) && ($company->consumables->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($company->consumables->count()).'</badge>' : '' !!} {!! ($company->consumables->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($company->consumables->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>

View file

@ -279,7 +279,7 @@
</strong> </strong>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
{!! nl2br(e($component->notes)) !!} {!! nl2br(Helper::parseEscapedMarkedownInline($component->notes)) !!}
</div> </div>
</div> </div>
@endif @endif

View file

@ -77,7 +77,7 @@
@can('update', $custom_fieldset) @can('update', $custom_fieldset)
<form method="post" action="{{ route('fields.disassociate', [$field, $custom_fieldset->id]) }}"> <form method="post" action="{{ route('fields.disassociate', [$field, $custom_fieldset->id]) }}">
@csrf @csrf
<button type="submit" class="btn btn-sm btn-danger">{{ trans('button.remove') }}</button> <button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-trash icon-white" aria-hidden="true"></i></button>
</form> </form>
@endcan @endcan
</td> </td>
@ -90,35 +90,34 @@
<td colspan="8"> <td colspan="8">
{{ Form::open(['route' => {{ Form::open(['route' =>
["fieldsets.associate",$custom_fieldset->id], ["fieldsets.associate",$custom_fieldset->id],
'class'=>'form-horizontal', 'class'=>'form-inline',
'id' => 'ordering']) }} 'id' => 'ordering']) }}
<div class="form-group col-md-4"> <div class="form-group">
<label for="field_id" class="sr-only"> <label for="field_id" class="sr-only">
{{ trans('admin/custom-field/general.add_field_to_fieldset')}} {{ trans('admin/custom-field/general.add_field_to_fieldset')}}
</label> </label>
{{ Form::select("field_id",$custom_fields_list,"",['aria-label'=>'field_id', 'class'=>'select2']) }} {{ Form::select("field_id",$custom_fields_list,"",['aria-label'=>'field_id', 'class'=>'select2', 'style' => 'min-width:400px;']) }}
</div> </div>
<div class="form-group col-md-2" style="vertical-align: middle;"> <div class="form-group" style="display: none;">
{{ Form::text('order', $maxid, array('aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<label class="form-control">
{{ Form::checkbox('required', 'on', old('required'), array('aria-label'=>'required')) }}
{{ trans('admin/custom_fields/general.required') }}
</label>
</div>
<div class="form-group col-md-2" style="display: none;">
{{ Form::text('order', $maxid, array('class' => 'form-control col-sm-1 col-md-1', 'style'=> 'width: 80px; padding-;right: 10px;', 'aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<label for="order">{{ trans('admin/custom_fields/general.order') }}</label> <label for="order">{{ trans('admin/custom_fields/general.order') }}</label>
</div> </div>
<div class="form-group col-md-3"> <div class="checkbox-inline">
<button type="submit" class="btn btn-primary"> {{ trans('general.save') }}</button> <label>
{{ Form::checkbox('required', 'on', old('required')) }}
<span style="padding-left: 10px;">{{ trans('admin/custom_fields/general.required') }}</span>
</label>
</div> </div>
<span style="padding-left: 10px;">
<button type="submit" class="btn btn-primary"> {{ trans('general.save') }}</button>
</span>
{{ Form::close() }} {{ Form::close() }}
</td> </td>

View file

@ -73,7 +73,14 @@
<nobr> <nobr>
@can('update', $fieldset) @can('update', $fieldset)
<a href="{{ route('fieldsets.edit', $fieldset->id) }}" class="btn btn-warning btn-sm">
<a href="{{ route('fieldsets.show', ['fieldset' => $fieldset->id]) }}" data-tooltip="true" title="{{ trans('general.edit_fieldset') }}">
<button type="submit" class="btn btn-info btn-sm">
<i class="fa-regular fa-rectangle-list"></i>
</button>
</a>
<a href="{{ route('fieldsets.edit', $fieldset->id) }}" class="btn btn-warning btn-sm" data-tooltip="true" title="{{ trans('general.update') }}">
<i class="fas fa-pencil-alt" aria-hidden="true"></i> <i class="fas fa-pencil-alt" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.edit') }}</span> <span class="sr-only">{{ trans('button.edit') }}</span>
</a> </a>
@ -82,9 +89,9 @@
@can('delete', $fieldset) @can('delete', $fieldset)
{{ Form::open(['route' => array('fieldsets.destroy', $fieldset->id), 'method' => 'delete','style' => 'display:inline-block']) }} {{ Form::open(['route' => array('fieldsets.destroy', $fieldset->id), 'method' => 'delete','style' => 'display:inline-block']) }}
@if($fieldset->models->count() > 0) @if($fieldset->models->count() > 0)
<button type="submit" class="btn btn-danger btn-sm disabled" disabled><i class="fas fa-trash"></i></button> <button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled><i class="fas fa-trash"></i></button>
@else @else
<button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button> <button type="submit" class="btn btn-danger btn-sm" data-tooltip="true" title="{{ trans('general.delete') }}"><i class="fas fa-trash"></i></button>
@endif @endif
{{ Form::close() }} {{ Form::close() }}
@endcan @endcan
@ -188,7 +195,7 @@
<nobr> <nobr>
{{ Form::open(array('route' => array('fields.destroy', $field->id), 'method' => 'delete', 'style' => 'display:inline-block')) }} {{ Form::open(array('route' => array('fields.destroy', $field->id), 'method' => 'delete', 'style' => 'display:inline-block')) }}
@can('update', $field) @can('update', $field)
<a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm"> <a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm" data-tooltip="true" title="{{ trans('general.update') }}">
<i class="fas fa-pencil-alt" aria-hidden="true"></i> <i class="fas fa-pencil-alt" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.edit') }}</span> <span class="sr-only">{{ trans('button.edit') }}</span>
</a> </a>
@ -197,11 +204,11 @@
@can('delete', $field) @can('delete', $field)
@if($field->fieldset->count()>0) @if($field->fieldset->count()>0)
<button type="submit" class="btn btn-danger btn-sm disabled" disabled> <button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled>
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span></button> <span class="sr-only">{{ trans('button.delete') }}</span></button>
@else @else
<button type="submit" class="btn btn-danger btn-sm"> <button type="submit" class="btn btn-danger btn-sm" data-tooltip="true" title="{{ trans('general.delete') }}">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span> <span class="sr-only">{{ trans('button.delete') }}</span>
</button> </button>

View file

@ -872,11 +872,13 @@
@can('update', $asset) @can('update', $asset)
@if ($asset->deleted_at=='')
<div class="col-md-12" style="padding-top: 5px;"> <div class="col-md-12" style="padding-top: 5px;">
<a href="{{ route('hardware.edit', $asset->id) }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print"> <a href="{{ route('hardware.edit', $asset->id) }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print">
{{ trans('admin/hardware/general.edit') }} {{ trans('admin/hardware/general.edit') }}
</a> </a>
</div> </div>
@endif
@endcan @endcan
@can('create', $asset) @can('create', $asset)

View file

@ -140,17 +140,15 @@
<div class="navbar-custom-menu"> <div class="navbar-custom-menu">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@can('index', \App\Models\Asset::class) @can('index', \App\Models\Asset::class)
<li aria-hidden="true" <li aria-hidden="true"{!! (Request::is('hardware*') ? ' class="active"' : '') !!}>
{!! (Request::is('hardware*') ? ' class="active"' : '') !!} tabindex="-1">
<a href="{{ url('hardware') }}" accesskey="1" tabindex="-1"> <a href="{{ url('hardware') }}" accesskey="1" tabindex="-1">
<i class="fas fa-barcode fa-fw" aria-hidden="true"></i> <i class="fas fa-barcode fa-fw"></i>
<span class="sr-only">{{ trans('general.assets') }}</span> <span class="sr-only">{{ trans('general.assets') }}</span>
</a> </a>
</li> </li>
@endcan @endcan
@can('view', \App\Models\License::class) @can('view', \App\Models\License::class)
<li aria-hidden="true" <li aria-hidden="true"{!! (Request::is('licenses*') ? ' class="active"' : '') !!}>
{!! (Request::is('licenses*') ? ' class="active"' : '') !!} tabindex="-1">
<a href="{{ route('licenses.index') }}" accesskey="2" tabindex="-1"> <a href="{{ route('licenses.index') }}" accesskey="2" tabindex="-1">
<i class="far fa-save fa-fw"></i> <i class="far fa-save fa-fw"></i>
<span class="sr-only">{{ trans('general.licenses') }}</span> <span class="sr-only">{{ trans('general.licenses') }}</span>
@ -158,8 +156,7 @@
</li> </li>
@endcan @endcan
@can('index', \App\Models\Accessory::class) @can('index', \App\Models\Accessory::class)
<li aria-hidden="true" <li aria-hidden="true"{!! (Request::is('accessories*') ? ' class="active"' : '') !!}>
{!! (Request::is('accessories*') ? ' class="active"' : '') !!} tabindex="-1">
<a href="{{ route('accessories.index') }}" accesskey="3" tabindex="-1"> <a href="{{ route('accessories.index') }}" accesskey="3" tabindex="-1">
<i class="far fa-keyboard fa-fw"></i> <i class="far fa-keyboard fa-fw"></i>
<span class="sr-only">{{ trans('general.accessories') }}</span> <span class="sr-only">{{ trans('general.accessories') }}</span>
@ -233,7 +230,8 @@
<li {!! (Request::is('accessories/create') ? 'class="active"' : '') !!}> <li {!! (Request::is('accessories/create') ? 'class="active"' : '') !!}>
<a href="{{ route('accessories.create') }}" tabindex="-1"> <a href="{{ route('accessories.create') }}" tabindex="-1">
<i class="far fa-keyboard fa-fw" aria-hidden="true"></i> <i class="far fa-keyboard fa-fw" aria-hidden="true"></i>
{{ trans('general.accessory') }}</a> {{ trans('general.accessory') }}
</a>
</li> </li>
@endcan @endcan
@can('create', \App\Models\Consumable::class) @can('create', \App\Models\Consumable::class)

View file

@ -133,8 +133,10 @@
<i class="fa-solid fa-list-check" aria-hidden="true"></i> <i class="fa-solid fa-list-check" aria-hidden="true"></i>
<span class="sr-only">{{ trans('general.import') }}</span> <span class="sr-only">{{ trans('general.import') }}</span>
</button> </button>
<a href="#" wire:click="$set('activeFile',null)">
<button class="btn btn-sm btn-danger" wire:click="destroy({{ $currentFile->id }})"> <button class="btn btn-sm btn-danger" wire:click="destroy({{ $currentFile->id }})">
<i class="fas fa-trash icon-white" aria-hidden="true"></i><span class="sr-only"></span></button> <i class="fas fa-trash icon-white" aria-hidden="true"></i><span class="sr-only"></span></button>
</a>
</td> </td>
</tr> </tr>

View file

@ -61,9 +61,9 @@
<div class="col-md-9 required" wire:ignore> <div class="col-md-9 required" wire:ignore>
@if (Helper::isDemoMode()) @if (Helper::isDemoMode())
{{ Form::select('webhook_selected', array('slack' => trans('admin/settings/general.slack'), 'general' => trans('admin/settings/general.general_webhook'), 'microsoft' => trans('admin/settings/general.ms_teams')), old('webhook_selected', $webhook_selected), array('class'=>'select2 form-control', 'aria-label' => 'webhook_selected', 'id' => 'select2', 'style'=>'width:100%', 'disabled')) }} {{ Form::select('webhook_selected', array('slack' => trans('admin/settings/general.slack'), 'general' => trans('admin/settings/general.general_webhook'),'google' => trans('admin/settings/general.google_workspaces'), 'microsoft' => trans('admin/settings/general.ms_teams')), old('webhook_selected', $webhook_selected), array('class'=>'select2 form-control', 'aria-label' => 'webhook_selected', 'id' => 'select2', 'style'=>'width:100%', 'disabled')) }}
@else @else
{{ Form::select('webhook_selected', array('slack' => trans('admin/settings/general.slack'), 'general' => trans('admin/settings/general.general_webhook'), 'microsoft' => trans('admin/settings/general.ms_teams')), old('webhook_selected', $webhook_selected), array('class'=>'select2 form-control', 'aria-label' => 'webhook_selected', 'id' => 'select2', 'data-minimum-results-for-search' => '-1', 'style'=>'width:100%')) }} {{ Form::select('webhook_selected', array('slack' => trans('admin/settings/general.slack'), 'general' => trans('admin/settings/general.general_webhook'),'google' => trans('admin/settings/general.google_workspaces'), 'microsoft' => trans('admin/settings/general.ms_teams')), old('webhook_selected', $webhook_selected), array('class'=>'select2 form-control', 'aria-label' => 'webhook_selected', 'id' => 'select2', 'data-minimum-results-for-search' => '-1', 'style'=>'width:100%')) }}
@endif @endif
</div> </div>
@ -90,6 +90,7 @@
<!-- Webhook channel --> <!-- Webhook channel -->
@if($webhook_selected != 'microsoft' && $webhook_selected!= 'google')
<div class="form-group{{ $errors->has('webhook_channel') ? ' error' : '' }}"> <div class="form-group{{ $errors->has('webhook_channel') ? ' error' : '' }}">
<div class="col-md-2"> <div class="col-md-2">
{{ Form::label('webhook_channel', trans('admin/settings/general.webhook_channel',['app' => $webhook_name ])) }} {{ Form::label('webhook_channel', trans('admin/settings/general.webhook_channel',['app' => $webhook_name ])) }}
@ -100,13 +101,14 @@
{!! $errors->first('webhook_channel', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} {!! $errors->first('webhook_channel', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div> </div>
</div> </div>
@endif
@if (Helper::isDemoMode()) @if (Helper::isDemoMode())
@include('partials.forms.demo-mode') @include('partials.forms.demo-mode')
@endif @endif
<!-- Webhook botname --> <!-- Webhook botname -->
@if($webhook_selected != 'microsoft') @if($webhook_selected != 'microsoft' && $webhook_selected != 'google')
<div class="form-group{{ $errors->has('webhook_botname') ? ' error' : '' }}"> <div class="form-group{{ $errors->has('webhook_botname') ? ' error' : '' }}">
<div class="col-md-2"> <div class="col-md-2">
{{ Form::label('webhook_botname', trans('admin/settings/general.webhook_botname',['app' => $webhook_name ])) }} {{ Form::label('webhook_botname', trans('admin/settings/general.webhook_botname',['app' => $webhook_name ])) }}
@ -122,14 +124,11 @@
@endif @endif
<!--Webhook Integration Test--> <!--Webhook Integration Test-->
@if($webhook_endpoint != null && $webhook_channel != null) @if($webhook_endpoint != null && $webhook_channel != null)
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-9"> <div class="col-md-offset-2 col-md-9">
@if($webhook_selected == "microsoft") <a href="#" wire:click.prevent="{{$webhook_test}}"
<a href="#" wire:click.prevent="msTeamTestWebhook"
@else
<a href="#" wire:click.prevent="testWebhook"
@endif
class="btn btn-default btn-sm pull-left"> class="btn btn-default btn-sm pull-left">
<i class="{{$webhook_icon}}" aria-hidden="true"></i> <i class="{{$webhook_icon}}" aria-hidden="true"></i>
{!! trans('admin/settings/general.webhook_test',['app' => ucwords($webhook_selected) ]) !!} {!! trans('admin/settings/general.webhook_test',['app' => ucwords($webhook_selected) ]) !!}

View file

@ -0,0 +1,70 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('general.bulk.delete.header', ['object_type' => trans_choice('general.location_plural', $valid_count)]) }}
@parent
@stop
@section('header_right')
<a href="{{ URL::previous() }}" class="btn btn-primary pull-right">
{{ trans('general.back') }}</a>
@stop
{{-- Page content --}}
@section('content')
<div class="row">
<!-- left column -->
<div class="col-md-8 col-md-offset-2">
<form class="form-horizontal" method="post" action="{{ route('locations.bulkdelete.store') }}" autocomplete="off" role="form">
{{csrf_field()}}
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title" style="color: red">{{ trans_choice('general.bulk.delete.warn', $valid_count, ['count' => $valid_count,'object_type' => trans_choice('general.location_plural', $valid_count)]) }}</h2>
</div>
<div class="box-body">
<table class="table table-striped table-condensed">
<thead>
<tr>
<td class="col-md-1">
<label>
<input type="checkbox" id="checkAll" checked="checked">
</label>
</td>
<td class="col-md-10">{{ trans('general.name') }}</td>
</tr>
</thead>
<tbody>
@foreach ($locations as $location)
<tr{!! (($location->assets_count > 0 ) ? ' class="danger"' : '') !!}>
<td>
<input type="checkbox" name="ids[]" class="{ ($location->isDeletable() ? '' : ' disabled') }}" value="{{ $location->id }}" {!! (($location->isDeletable()) ? ' checked="checked"' : ' disabled') !!}>
</td>
<td>{{ $location->name }}</td>
</tr>
@endforeach
</tbody>
</table>
</div><!-- /.box-body -->
<div class="box-footer text-right">
<a class="btn btn-link pull-left" href="{{ URL::previous() }}">{{ trans('button.cancel') }}</a>
<button type="submit" class="btn btn-success" id="submit-button"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.delete') }}</button>
</div><!-- /.box-footer -->
</div><!-- /.box -->
</form>
</div> <!-- .col-md-12-->
</div><!--.row-->
@stop
@section('moar_scripts')
<script>
$("#checkAll").change(function () {
$("input:checkbox").prop('checked', $(this).prop("checked"));
});
</script>
@stop

View file

@ -20,11 +20,17 @@
<div class="box-body"> <div class="box-body">
<div class="table-responsive"> <div class="table-responsive">
@include('partials.locations-bulk-actions')
<table <table
data-columns="{{ \App\Presenters\LocationPresenter::dataTableLayout() }}" data-columns="{{ \App\Presenters\LocationPresenter::dataTableLayout() }}"
data-cookie-id-table="locationTable" data-cookie-id-table="locationTable"
data-click-to-select="true"
data-pagination="true" data-pagination="true"
data-id-table="locationTable" data-id-table="locationTable"
data-toolbar="#locationsBulkEditToolbar"
data-bulk-button-id="#bulkLocationsEditButton"
data-bulk-form-id="#locationsBulkForm"
data-search="true" data-search="true"
data-show-footer="true" data-show-footer="true"
data-side-pagination="server" data-side-pagination="server"

View file

@ -25,7 +25,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.users') }} {{ trans('general.users') }}
{!! (($location->users) && ($location->users->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->users->count()).'</badge>' : '' !!} {!! ($location->users->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->users->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
@ -38,7 +38,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('admin/locations/message.current_location') }} {{ trans('admin/locations/message.current_location') }}
{!! (($location->assets) && ($location->assets()->AssetsForShow()->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->assets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($location->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->assets()->AssetsForShow()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -51,7 +51,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('admin/hardware/form.default_location') }} {{ trans('admin/hardware/form.default_location') }}
{!! (($location->rtd_assets) && ($location->rtd_assets()->AssetsForShow()->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->rtd_assets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($location->rtd_assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->rtd_assets()->AssetsForShow()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -63,7 +63,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('admin/locations/message.assigned_assets') }} {{ trans('admin/locations/message.assigned_assets') }}
{!! (($location->rtd_assets) && ($location->assignedAssets()->AssetsForShow()->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->assignedAssets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($location->assignedAssets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->assignedAssets()->AssetsForShow()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -76,7 +76,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.accessories') }} {{ trans('general.accessories') }}
{!! (($location->accessories) && ($location->accessories->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->accessories->count()).'</badge>' : '' !!} {!! ($location->accessories->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->accessories->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -88,7 +88,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.consumables') }} {{ trans('general.consumables') }}
{!! (($location->consumables) && ($location->consumables->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->consumables->count()).'</badge>' : '' !!} {!! ($location->consumables->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->consumables->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -100,7 +100,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.components') }} {{ trans('general.components') }}
{!! (($location->components) && ($location->components->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($location->components->count()).'</badge>' : '' !!} {!! ($location->components->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($location->components->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>

View file

@ -41,7 +41,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.assets') }} {{ trans('general.assets') }}
{!! (($manufacturer->assets) && ($manufacturer->assets()->AssetsForShow()->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->assets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($manufacturer->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->assets()->AssetsForShow()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
@ -55,7 +55,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.licenses') }} {{ trans('general.licenses') }}
{!! (($manufacturer->licenses) && ($manufacturer->licenses->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->licenses->count()).'</badge>' : '' !!} {!! ($manufacturer->licenses->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->licenses->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
@ -68,7 +68,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.accessories') }} {{ trans('general.accessories') }}
{!! (($manufacturer->accessories) && ($manufacturer->accessories->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->accessories->count()).'</badge>' : '' !!} {!! ($manufacturer->accessories->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->accessories->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
@ -81,7 +81,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.consumables') }} {{ trans('general.consumables') }}
{!! (($manufacturer->consumables) && ($manufacturer->consumables->count() > 0 )) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->consumables->count()).'</badge>' : '' !!} {!! ($manufacturer->consumables->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($manufacturer->consumables->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>

View file

@ -43,7 +43,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.assets') }} {{ trans('general.assets') }}
{!! (($model->assets_count) && ($model->assets_count > 0 )) ? '<badge class="badge badge-secondary">'.number_format($model->assets_count).'</badge>' : '' !!} {!! ($model->assets_count > 0 ) ? '<badge class="badge badge-secondary">'.number_format($model->assets_count).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -309,7 +309,7 @@
@if ($model->notes) @if ($model->notes)
<li> <li>
{{ trans('general.notes') }}: {{ trans('general.notes') }}:
{{ $model->notes }} {!! nl2br(Helper::parseEscapedMarkedownInline($model->notes)) !!}
</li> </li>
@endif @endif

View file

@ -139,12 +139,12 @@
}); });
// Handle whether or not the edit button should be disabled // Handle whether the edit button should be disabled
$('.snipe-table').on('uncheck.bs.table', function () { $('.snipe-table').on('uncheck.bs.table', function () {
var buttonName = $(this).data('bulk-button-id'); var buttonName = $(this).data('bulk-button-id');
if ($(this).bootstrapTable('getSelections').length == 0) { if ($(this).bootstrapTable('getSelections').length == 0) {
$(buttonName).attr('disabled', 'disabled'); $(buttonName).attr('disabled', 'disabled');
} }
}); });
@ -296,6 +296,10 @@
if ((row.available_actions) && (row.available_actions.update === true)) { if ((row.available_actions) && (row.available_actions.update === true)) {
actions += '<a href="{{ config('app.url') }}/' + dest + '/' + row.id + '/edit" class="actions btn btn-sm btn-warning" data-tooltip="true" title="{{ trans('general.update') }}"><i class="fas fa-pencil-alt" aria-hidden="true"></i><span class="sr-only">{{ trans('general.update') }}</span></a>&nbsp;'; actions += '<a href="{{ config('app.url') }}/' + dest + '/' + row.id + '/edit" class="actions btn btn-sm btn-warning" data-tooltip="true" title="{{ trans('general.update') }}"><i class="fas fa-pencil-alt" aria-hidden="true"></i><span class="sr-only">{{ trans('general.update') }}</span></a>&nbsp;';
} else {
if ((row.available_actions) && (row.available_actions.update != true)) {
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_edited') }}"><a class="btn btn-warning btn-sm disabled" onClick="return false;"><i class="fas fa-pencil-alt"></i></a></span>&nbsp;';
}
} }
if ((row.available_actions) && (row.available_actions.delete === true)) { if ((row.available_actions) && (row.available_actions.delete === true)) {
@ -394,17 +398,35 @@
// Convert line breaks to <br> // Convert line breaks to <br>
function notesFormatter(value) { function notesFormatter(value) {
if (value) { if (value) {
return value.replace(/(?:\r\n|\r|\n)/g, '<br />');; return value.replace(/(?:\r\n|\r|\n)/g, '<br />');
} }
} }
// Check if checkbox should be selectable
// Selectability is determined by the API field "selectable" which is set at the Presenter/API Transformer
// However since different bulk actions have different requirements, we have to walk through the available_actions object
// to determine whether to disable it
function checkboxEnabledFormatter (value, row) {
// add some stuff to get the value of the select2 option here?
if ((row.available_actions) && (row.available_actions.bulk_selectable) && (row.available_actions.bulk_selectable.delete !== true)) {
console.log('value for ID ' + row.id + ' is NOT true:' + row.available_actions.bulk_selectable.delete);
return {
disabled:true,
//checked: false, <-- not sure this will work the way we want?
}
}
console.log('value for ID ' + row.id + ' IS true:' + row.available_actions.bulk_selectable.delete);
}
// We need a special formatter for license seats, since they don't work exactly the same // We need a special formatter for license seats, since they don't work exactly the same
// Checkouts need the license ID, checkins need the specific seat ID // Checkouts need the license ID, checkins need the specific seat ID
function licenseSeatInOutFormatter(value, row) { function licenseSeatInOutFormatter(value, row) {
// The user is allowed to check the license seat out and it's available // The user is allowed to check the license seat out and it's available
if ((row.available_actions.checkout == true) && (row.user_can_checkout == true) && ((!row.asset_id) && (!row.assigned_to))) { if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) {
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>'; return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
} else { } else {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check in this license seat.">{{ trans('general.checkin') }}</a>'; return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check in this license seat.">{{ trans('general.checkin') }}</a>';
@ -623,6 +645,9 @@
function assetTagLinkFormatter(value, row) { function assetTagLinkFormatter(value, row) {
if ((row.asset) && (row.asset.id)) { if ((row.asset) && (row.asset.id)) {
if (row.asset.deleted_at!='') {
return '<span style="white-space: nowrap;"><i class="fas fa-times text-danger"></i><span class="sr-only">deleted</span> <del><a href="{{ config('app.url') }}/hardware/' + row.asset.id + '" data-tooltip="true" title="{{ trans('admin/hardware/general.deleted') }}">' + row.asset.asset_tag + '</a></del></span>';
}
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.asset_tag + '</a>'; return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.asset_tag + '</a>';
} }
return ''; return '';
@ -640,7 +665,17 @@
if ((row.asset) && (row.asset.name)) { if ((row.asset) && (row.asset.name)) {
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.name + '</a>'; return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.name + '</a>';
} }
}
function assetSerialLinkFormatter(value, row) {
if ((row.asset) && (row.asset.serial)) {
if (row.asset.deleted_at!='') {
return '<span style="white-space: nowrap;"><i class="fas fa-times text-danger"></i><span class="sr-only">deleted</span> <del><a href="{{ config('app.url') }}/hardware/' + row.asset.id + '" data-tooltip="true" title="{{ trans('admin/hardware/general.deleted') }}">' + row.asset.serial + '</a></del></span>';
}
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.serial + '</a>';
}
return '';
} }
function trueFalseFormatter(value) { function trueFalseFormatter(value) {

View file

@ -36,7 +36,7 @@
{!! $errors->first('image', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} {!! $errors->first('image', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div> </div>
<div class="col-md-4 col-md-offset-3" aria-hidden="true"> <div class="col-md-4 col-md-offset-3" aria-hidden="true">
<img id="uploadFile-imagePreview" style="max-width: 300px; display: none;" alt="{{ trans('partials/forms/general.alt_uploaded_image_thumbnail') }}"> <img id="uploadFile-imagePreview" style="max-width: 300px; display: none;" alt="{{ trans('general.alt_uploaded_image_thumbnail') }}">
</div> </div>
</div> </div>

View file

@ -7,10 +7,10 @@
</label> </label>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<label class="btn btn-default"> <label class="btn btn-default{{ (config('app.lock_passwords')) ? ' disabled' : '' }}">
{{ trans('button.select_file') }} {{ trans('button.select_file') }}
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}" <input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ (isset($allowedTypes) ? $allowedTypes : "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml") }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
style="display:none; max-width: 90%"> style="display:none; max-width: 90%"{{ (config('app.lock_passwords')) ? ' disabled' : '' }}>
</label> </label>
<span class='label label-default' id="{{ $logoId }}-info"></span> <span class='label label-default' id="{{ $logoId }}-info"></span>

Some files were not shown because too many files have changed in this diff Show more