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 2690 additions and 1198 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');
} }
@ -158,11 +304,11 @@ class RestoreFromBackup extends Command
} }
foreach (array_merge($private_dirs, $public_dirs) as $dir) { foreach (array_merge($private_dirs, $public_dirs) as $dir) {
$last_pos = strrpos($raw_path, $dir.'/'); $last_pos = strrpos($raw_path, $dir . '/');
if ($last_pos !== false) { if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n"); //print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path? //print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i]; $interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
continue 2; continue 2;
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) { if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
// we don't care about that; we just want files with the appropriate prefix // we don't care about that; we just want files with the appropriate prefix
@ -171,7 +317,7 @@ class RestoreFromBackup extends Command
} }
} }
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt', $good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', ]; 'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
foreach (array_merge($private_files, $public_files) as $file) { foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false); $has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) { if ($has_wildcard) {
@ -180,8 +326,8 @@ class RestoreFromBackup extends Command
$last_pos = strrpos($raw_path, $file); // no trailing slash! $last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) { if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION)); $extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (! in_array($extension, $good_extensions)) { if (!in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file '.$raw_path.' is being skipped'); $this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path; $boring_files[] = $raw_path;
continue 2; continue 2;
} }
@ -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,20 +406,27 @@ 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) {
$bytes_read += strlen($buffer); // "Legacy" direct-piping
// \Log::debug("Buffer is: '$buffer'"); $bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer); $bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) { if ($bytes_written === false) {
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,17 +292,9 @@ 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,31 +522,33 @@ 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)) {
if (file_exists($barcode_file)) { if (file_exists($barcode_file)) {
$header = ['Content-type' => 'image/png']; $header = ['Content-type' => 'image/png'];
return response()->file($barcode_file, $header); return response()->file($barcode_file, $header);
} else { } else {
// Calculate barcode width in pixel based on label width (inch) // Calculate barcode width in pixel based on label width (inch)
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001; $barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
$barcode = new \Com\Tecnick\Barcode\Barcode(); $barcode = new \Com\Tecnick\Barcode\Barcode();
try { try {
$barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50); $barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
file_put_contents($barcode_file, $barcode_obj->getPngData()); file_put_contents($barcode_file, $barcode_obj->getPngData());
return response($barcode_obj->getPngData())->header('Content-type', 'image/png'); return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
} catch (\Exception $e) { } catch (\Exception $e) {
Log::debug('The barcode format is invalid.'); Log::debug('The barcode format is invalid.');
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif'); return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
}
} }
} }
} }
return null;
} }
/** /**

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,65 +426,64 @@ 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')) {
Storage::disk('public')->delete($setting->logo); Storage::disk('public')->delete($setting->logo);
$setting->logo = null; $setting->logo = null;
$setting->brand = 1; $setting->brand = 1;
}
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// If the user wants to clear the favicon...
if ($request->hasFile('favicon')) {
$favicon_image = $favicon_upload = $request->file('favicon');
$favicon_ext = $favicon_image->getClientOriginalExtension();
$setting->favicon = $favicon_file_name = 'favicon-uploaded.'.$favicon_ext;
if (($favicon_image->getClientOriginalExtension() != 'ico') && ($favicon_image->getClientOriginalExtension() != 'svg')) {
$favicon_upload = Image::make($favicon_image->getRealPath())->resize(null, 36, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($favicon_file_name, (string) $favicon_upload->encode());
} else {
Storage::disk('public')->put($favicon_file_name, file_get_contents($request->file('favicon')));
} }
// Remove Current image if exists $setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if (($setting->favicon) && (file_exists($favicon_file_name))) {
Storage::disk('public')->delete($favicon_file_name);
}
} elseif ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->clear_favicon);
$setting->favicon = null;
// If they are uploading an image, validate it and upload it
} if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// If the user wants to clear the favicon...
if ($request->hasFile('favicon')) {
$favicon_image = $favicon_upload = $request->file('favicon');
$favicon_ext = $favicon_image->getClientOriginalExtension();
$setting->favicon = $favicon_file_name = 'favicon-uploaded.'.$favicon_ext;
if (($favicon_image->getClientOriginalExtension() != 'ico') && ($favicon_image->getClientOriginalExtension() != 'svg')) {
$favicon_upload = Image::make($favicon_image->getRealPath())->resize(null, 36, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($favicon_file_name, (string) $favicon_upload->encode());
} else {
Storage::disk('public')->put($favicon_file_name, file_get_contents($request->file('favicon')));
}
// Remove Current image if exists
if (($setting->favicon) && (file_exists($favicon_file_name))) {
Storage::disk('public')->delete($favicon_file_name);
}
} elseif ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->clear_favicon);
$setting->favicon = null;
// 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));
// Batch update the dates. We have to use this method to avoid time limit exceeded errors on very large datasets,
// 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');
// Grab all of the assets that have an existing next_audit_date
$assets = Asset::whereNotNull('next_audit_date')->get();
// 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

@ -37,23 +37,33 @@ class SlackSettingsForm extends Component
public function mount() { public function mount() {
$this->webhook_text= [ $this->webhook_text= [
"slack" => array( "slack" => array(
"name" => trans('admin/settings/general.slack') , "name" => trans('admin/settings/general.slack') ,
"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,7 +206,35 @@ class SlackSettingsForm extends Component
} }
} }
public function msTeamTestWebhook(){ 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(){
$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,13 +90,9 @@ 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)) { $assetData->put('title', $title);
$title = str_replace('{COMPANY}', $asset->company->name, $settings->label2_title);
$settings->qr_text;
$assetData->put('title', $title);
}
} }
if ($template->getSupportLogo()) { if ($template->getSupportLogo()) {
@ -216,4 +212,4 @@ class Label implements View
return self::NAME; return self::NAME;
} }
} }

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"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View file

@ -15,12 +15,6 @@
* Date: 2020-03-14 * Date: 2020-03-14
*/ */
/*!
* Vue.js v2.4.4
* (c) 2014-2017 Evan You
* Released under the MIT License.
*/
/*! /*!
* jQuery JavaScript Library v3.5.1 * jQuery JavaScript Library v3.5.1
* https://jquery.com/ * https://jquery.com/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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)
@ -994,7 +992,7 @@
container: 'body', container: 'body',
animation: true, animation: true,
}); });
$('[data-toggle="popover"]').popover(); $('[data-toggle="popover"]').popover();
$('.select2 span').addClass('needsclick'); $('.select2 span').addClass('needsclick');
$('.select2 span').removeAttr('title'); $('.select2 span').removeAttr('title');

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,23 +90,25 @@
<!-- Webhook channel --> <!-- Webhook channel -->
<div class="form-group{{ $errors->has('webhook_channel') ? ' error' : '' }}"> @if($webhook_selected != 'microsoft' && $webhook_selected!= 'google')
<div class="col-md-2"> <div class="form-group{{ $errors->has('webhook_channel') ? ' error' : '' }}">
{{ Form::label('webhook_channel', trans('admin/settings/general.webhook_channel',['app' => $webhook_name ])) }} <div class="col-md-2">
</div> {{ Form::label('webhook_channel', trans('admin/settings/general.webhook_channel',['app' => $webhook_name ])) }}
<div class="col-md-9 required"> </div>
<input type="text" wire:model.lazy="webhook_channel" class="form-control" placeholder="#IT-Ops" value="{{ old('webhook_channel', $webhook_channel) }}"{{ Helper::isDemoMode() ? ' disabled' : ''}}> <div class="col-md-9 required">
<input type="text" wire:model.lazy="webhook_channel" class="form-control" placeholder="#IT-Ops" value="{{ old('webhook_channel', $webhook_channel) }}"{{ Helper::isDemoMode() ? ' disabled' : ''}}>
{!! $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