Compare commits

...

98 Commits

Author SHA1 Message Date
snipe
fa07f21d11 Commiting this so I don’t lose the history, but will open a smaller PR
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 10:53:04 +01:00
snipe
f3bfbc888a Nicer formatting for gallery view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 03:53:59 +01:00
snipe
5c5405dbd5 Nicer custom view
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:23:55 +01:00
snipe
b0451fc552 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:23:43 +01:00
snipe
94e6e5e210 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:04:54 +01:00
snipe
27c31312d6 Fixed multiple upload for users GUI controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:04:45 +01:00
snipe
98eaa9e2da Updated webpack for new custom view library
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:57 +01:00
snipe
82723a3eb6 Added custom view library
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:44 +01:00
snipe
f93089904a Added generic file delete translation
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:30 +01:00
snipe
03efa06f90 Added custom view
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:19 +01:00
snipe
16f316967e Passed object to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:04 +01:00
snipe
35cbd58dad Fixed route name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:51 +01:00
snipe
d50bcd6f55 Change spaces to dashes in filename
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:39 +01:00
snipe
83565b2a1e Removed weird conditional
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:23 +01:00
snipe
cc01f15f28 Updated translation
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:05 +01:00
snipe
e38ea126d5 Switched order
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:45:37 +01:00
snipe
a9a6a80e04 Switched order in API
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:45:30 +01:00
snipe
53f97680a4 Removed asset files controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:18:29 +01:00
snipe
9be02c62c8 Added use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:16:17 +01:00
snipe
1e4d452a03 Added file upload translations
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:15:47 +01:00
snipe
0a4d6e3631 Added trailing slashes, standardized translation strings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:15:34 +01:00
snipe
d862da2345 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:18 +01:00
snipe
73ffa3d751 Comments for routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:10 +01:00
snipe
f77ed72111 Added method formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:02 +01:00
snipe
702f1d0343 Consolidated file type display
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:06:12 +01:00
snipe
0bacbe175a Fixed path name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:06:01 +01:00
snipe
851cb29fc6 Set note to null if blank
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:05:51 +01:00
snipe
e3e164fa3b Added additional object types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:04:38 +01:00
snipe
7ef83dee56 Fixed routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 12:15:31 +01:00
snipe
7183cec43f Allow gifs as inlineable
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 22:06:40 +01:00
snipe
3456c99255 Changed route name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 22:06:30 +01:00
snipe
966745e852 Added download column to presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:05:52 +01:00
snipe
f9372df48f Use new action log methods for file path and url
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:04:51 +01:00
snipe
3e26acd5cb Added loggable to user model
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:03:39 +01:00
snipe
6cc10b9992 Added two helper methods on action log model
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:03:30 +01:00
snipe
732e125a1d Removed repeated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:59 +01:00
snipe
32f7eae6a3 New uploads controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:48 +01:00
snipe
bd635cedba Updated blades
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:40 +01:00
snipe
676f94175e Added fields to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:02:39 +02:00
snipe
35b47cd57f Added method to check file extension
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:02:16 +02:00
snipe
5e1ee05e19 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:59 +02:00
snipe
1413ba0b0b Pass uploads list route
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:52 +02:00
snipe
a182c43d8e Added icon to presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:18 +02:00
snipe
dbd864bf32 Added a search to the listing
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:09 +02:00
snipe
0009142b76 Added route comments
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:00:52 +02:00
snipe
4058312eb9 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 13:42:19 +02:00
snipe
b65cb967be Added catch for validation exception specifically
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:41:22 +02:00
snipe
b5dc39e70e Removed update_at
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:29:20 +02:00
snipe
b81416456a Nicer error messages on failure for bulk files
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:28:49 +02:00
snipe
2671e8197a Added validation error handler
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:28:35 +02:00
snipe
9cf23d18e2 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:27:21 +02:00
snipe
c5ecc8f839 Use uploads presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:05:05 +01:00
snipe
c1c5814000 Route model binding, gather payload
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:04:24 +01:00
snipe
b68c2e0f11 Route model binding
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:04:07 +01:00
snipe
9e3e04521e Merge pull request #16900 from marcusmoore/fixes/user-full-name-accessor
Handle settings not being available in full name accessor
2025-05-10 12:26:19 +01:00
snipe
65dfbd02fe Use develop branch
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:55 +01:00
snipe
649ab53320 Updated codacy link
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:18 +01:00
snipe
9250624f79 Merge pull request #16909 from grokability/fixes-#16554-category-delete
Fixed #16554 - Added models to deletable check
2025-05-09 19:23:35 +01:00
snipe
995e2090f5 Added/updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:53 +01:00
snipe
9b91584776 Added models to deletable check
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:48 +01:00
snipe
8fd97ea501 Merge pull request #16908 from grokability/bug/sc-28724
Fixed #16535 - more info to side rail in accessories
2025-05-09 18:03:05 +01:00
snipe
a80b9ab362 Fixed #16535 - more info to side rail in accessories
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:48:02 +01:00
snipe
556e1081b3 Added two more selectors for byod
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:23:08 +01:00
snipe
b070916f0b Merge pull request #16907 from grokability/add_ids_to_menus
Fixed #16456 - added ids to sidenav options and bod
2025-05-09 17:22:19 +01:00
snipe
940caf14b0 Added ids to menu items
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:09:59 +01:00
snipe
76da1d6663 Added class to checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:04:19 +01:00
snipe
bafff9020a Merge pull request #16611 from Godmartinz/MS_teams_deprecation_update
Reworked MS Teams deprecation warnings and notifications visibility
2025-05-09 16:38:25 +01:00
snipe
0d5dca6456 Fixed #16690 - fallback to category image if no model image is present
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 14:46:27 +01:00
snipe
0f9b7119c0 Merge pull request #16905 from grokability/fixes_#16901
Fixed #16901 - use default currency for asset maintenance cost
2025-05-09 12:45:46 +01:00
snipe
d4181549e8 Fixes #16901 - use default currency for maintenance cost display
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 12:43:49 +01:00
Marcus Moore
d57f56e44f Handle settings not being available 2025-05-08 16:20:26 -07:00
snipe
7d9b87f059 Merge pull request #16898 from marcusmoore/chore/form-radio-replacement
Replaced Form::radio helpers
2025-05-08 20:50:29 +01:00
Marcus Moore
c157f4190e Replace Form::radio in location partial 2025-05-08 12:25:48 -07:00
Marcus Moore
9357eca1cd Replace Form::radio on asset checkin page 2025-05-08 12:16:55 -07:00
snipe
40c65a07a4 Merge pull request #16896 from grokability/removed_seat_number
Removed seat "name" from licenses seats API/UI response
2025-05-08 17:57:40 +01:00
snipe
13521bcf75 Removed seat “name” from license seats API/UI
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 17:37:27 +01:00
snipe
1c09dc139a Undo previous change
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:40:24 +01:00
snipe
d5f955b1e0 License seats are not numbered correctly [sc-29113]
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:25:49 +01:00
snipe
9e6e8f0931 Moved incomplete test marker
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:22:09 +01:00
snipe
c93ef30801 Ignore flaky test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:43:40 +01:00
snipe
3e0dec4856 Fixed #16893 - more specific upload failure text
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:38:38 +01:00
snipe
0b167f5f6f Grab location uploads from backup
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:22:26 +01:00
snipe
f6b21fdb82 Merge pull request #16895 from grokability/fixed_#16863_custom_fields_validation
Fixed #16863 - better handle custom fields validation when unique but not required
2025-05-08 15:09:04 +01:00
snipe
f151628808 Merge pull request #16894 from grokability/resolve-webserver-permissions
Fix webserver/user file permissions issue
2025-05-08 15:08:40 +01:00
snipe
e44aad0328 Fixed typeos 2025-05-08 15:08:14 +01:00
snipe
1881054c92 Fixed #16863 - better handle unique not required custom field redirects
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:00:43 +01:00
Jeremy Price
f7533c5e41 Fix webserver/user file permissions issue
Fixes https://github.com/grokability/snipe-it/issues/16777

We weren't adding the webserver user to the app-user's group, which was
a problem for the webserver trying to write to the log file if it had
been created by a user-owned process (like a cron) or the installation
script chown-ing everything... even though the log file was created 664

This would often present in mysterious ways. In the linked case, trying
to upload a cvs for import would fail with an unhelpful message, because
the actual error is swallowed in the generic error handler for the page.

I've filed an issue to hopefully help with that: https://github.com/grokability/snipe-it/issues/16893

Used this opportunity to condense some logic that was
identical between architectures,
2025-05-08 13:55:23 +02:00
snipe
f181e0fa55 Merge pull request #16877 from marcusmoore/bug/sc-29012
Allow updating asset model image via api
2025-05-08 06:27:49 +01:00
snipe
b04efdfefc Merge pull request #16889 from grokability/add_updated_range_to_custom_report
Added #16887 - last updated date range for custom report
2025-05-08 06:27:32 +01:00
snipe
352b935dee Merge pull request #16884 from marcusmoore/bug/sc-29097
Removed `2fa_authed` from session upon logout
2025-05-08 06:23:26 +01:00
snipe
0ba3b9975a Added #16887 - last updated date range for custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 06:21:06 +01:00
Marcus Moore
cc06187f31 Remove 2fa_authed from session upon logout 2025-05-07 14:04:33 -07:00
Marcus Moore
d75de73867 Allow updating asset model image via api 2025-05-06 17:13:23 -07:00
Godfrey M
e3a2397b74 removed hiding the notifications icon 2025-05-05 10:28:08 -07:00
Godfrey M
3b34654dd7 Merge branch 'develop' into MS_teams_deprecation_update
# Conflicts:
#	resources/lang/en-US/admin/settings/message.php
2025-05-05 09:33:32 -07:00
Godfrey M
4b6437854c swapped out hard coded text with translation 2025-05-05 09:31:23 -07:00
Godfrey M
4ef161214d notification icon only appears when there are notifications 2025-04-01 12:01:35 -07:00
Godfrey M
29d0380db3 reword warning messages, remove warning if webhook cleared and saved, deprecations only for superadmins 2025-04-01 11:53:32 -07:00
64 changed files with 3818 additions and 917 deletions

View File

@@ -10,10 +10,10 @@ name: Codacy Security Scan
on:
push:
branches: [ master ]
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [ develop ]
schedule:
- cron: '36 23 * * 3'

View File

@@ -1,6 +1,6 @@
![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System

View File

@@ -249,6 +249,7 @@ class RestoreFromBackup extends Command
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/locations',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',

View File

@@ -61,14 +61,12 @@ class Handler extends ExceptionHandler
public function render($request, Throwable $e)
{
// CSRF token mismatch error
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
return redirect()->back()->with('error', trans('general.token_expired'));
}
// Invalid JSON exception
// TODO: don't understand why we have to do this when we have the invalidJson() method, below, but, well, whatever
if ($e instanceof JsonException) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Invalid JSON'), 422);
}
@@ -88,8 +86,9 @@ class Handler extends ExceptionHandler
return redirect()->back()->withInput()->with('error', trans('validation.date', ['attribute' => 'date']));
}
// Handle API requests that fail
if ($request->ajax() || $request->wantsJson()) {
if ($request->ajax() || $request->wantsJson() || ($request->route() && $request->route()->named('api.files.*'))) {
// Handle API requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
if ($e instanceof InvalidFormatException) {
@@ -102,6 +101,7 @@ class Handler extends ExceptionHandler
return response()->json(Helper::formatStandardApiResponse('error', null, $className . ' not found'), 200);
}
// Handle API requests that fail because of an HTTP status code and return a useful error message
if ($this->isHttpException($e)) {
@@ -116,12 +116,24 @@ class Handler extends ExceptionHandler
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
}
}
// This handles API validation exceptions that happen at the Form Request level, so they
// never even get to the controller where we normally nicely format JSON responses
if ($e instanceof ValidationException) {
$response = $this->invalidJson($request, $e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
// return response()->json(Helper::formatStandardApiResponse('error', null, 'Undefined exception'), 200);
}
// This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {

View File

@@ -722,8 +722,8 @@ class Helper
// The check and message that the user is still using the deprecated version
$deprecations = [
'ms_teams_deprecated' => array(
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
];
// if item of concern is being used and its being used with the deprecated values return the notification array.

View File

@@ -48,17 +48,29 @@ class StorageHelper
'avif',
'webp',
'png',
'gif',
];
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path) && (in_array(pathinfo($file_with_path, PATHINFO_EXTENSION), $allowed_inline))) {
return true;
}
return false;
}
public static function getFiletype($file_with_path) {
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
return pathinfo($file_with_path, PATHINFO_EXTENSION);
}
return null;
}
/**
* Decide whether to show the file inline or download it.
*/

View File

@@ -1,200 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use App\Http\Transformers\UploadedFilesTransformer;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Http\Request;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list(Asset $asset, Request $request) : JsonResponse | array
{
$this->authorize('view', $asset);
$allowed_columns =
[
'id',
'filename',
'eol',
'notes',
'created_at',
'updated_at',
];
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
if ($request->filled('search')) {
$files = $files->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $files->count()) ? $files->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$files = $files->orderBy($sort, $order);
$files = $files->skip($offset)->take($limit)->get();
return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy(Asset $asset, $fileId = null) : JsonResponse
{
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}

View File

@@ -1,184 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\AssetModelsTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetmodel
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetmodel_id) : JsonResponse | array
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetmodel_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
$this->authorize('view', $assetmodel);
return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View File

@@ -56,7 +56,7 @@ class CategoriesController extends Controller
'notes',
])
->with('adminuser')
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
/*
@@ -212,7 +212,7 @@ class CategoriesController extends Controller
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
if (! $category->isDeletable()) {
return response()->json(

View File

@@ -34,7 +34,7 @@ class LicenseSeatsController extends Controller
if ($request->input('sort') == 'department') {
$seats->OrderDepartments($order);
} else {
$seats->orderBy('id', $order);
$seats->orderBy('updated_at', $order);
}
$total = $seats->count();

View File

@@ -0,0 +1,242 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class UploadedFilesController extends Controller
{
static $map_object_type = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/**
* List the files for an object.
*
* @since [v7.0.12]
* @author [r-xyz]
*/
public function index(Request $request, $object_type, $id) : JsonResponse | array
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Columns allowed for sorting
$allowed_columns =
[
'id',
'filename',
'action_type',
'note',
'created_at',
];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at';
// Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations
// And we generally won't need that here
if ($request->filled('search')) {
$uploads->where(function ($query) use ($request) {
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
});
}
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
}
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type],self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
}
// No files were submitted
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
}
/**
* Check for permissions and display the file.
*
* @param AssetModel $model
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')
->where('item_type', AssetModel::class)
->where('item_id', $object->id)->find($file_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 404);
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param AssetModel $model
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($object_type, $id, $file_id) : JsonResponse
{
\Log::error('destroy called for '.$object_type.' with id '.$id.' and file_id '.$file_id);
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
}
}

View File

@@ -42,12 +42,11 @@ class AssetCheckinController extends Controller
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Validate custom fields on existing asset
$validator = Validator::make($asset->toArray(), $asset->customFieldValidationRules());
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($validator->fails()) {
return redirect()->route('hardware.edit', $asset)
->withErrors($validator);
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
$target_option = match ($asset->assigned_type) {

View File

@@ -37,13 +37,13 @@ class AssetCheckoutController extends Controller
->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Validate custom fields on existing asset
$validator = Validator::make($asset->toArray(), $asset->customFieldValidationRules());
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($validator->fails()) {
return redirect()->route('hardware.edit', $asset)
->withErrors($validator);
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'))

View File

@@ -45,7 +45,7 @@ class AssetFilesController extends Controller
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
return redirect()->back()->with('error', trans('general.file_upload_status.nofiles'));
}
/**

View File

@@ -882,12 +882,12 @@ class AssetsController extends Controller
$this->authorize('audit', Asset::class);
$settings = Setting::getSettings();
// Validate custom fields on existing asset
$validator = Validator::make($asset->toArray(), $asset->customFieldValidationRules());
if ($validator->fails()) {
return redirect()->route('hardware.edit', $asset)
->withErrors($validator);
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();

View File

@@ -484,6 +484,7 @@ class LoginController extends Controller
}
$request->session()->regenerate(true);
$request->session()->forget('2fa_authed');
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());

View File

@@ -145,7 +145,7 @@ class CategoriesController extends Controller
{
$this->authorize('delete', Category::class);
// Check if the category exists
if (is_null($category = Category::findOrFail($categoryId))) {
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
}
@@ -155,7 +155,6 @@ class CategoriesController extends Controller
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
// Redirect to the locations management page
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
}

View File

@@ -737,6 +737,11 @@ class ReportsController extends Controller
if (($request->filled('next_audit_start')) && ($request->filled('next_audit_end'))) {
$assets->whereBetween('assets.next_audit_date', [$request->input('next_audit_start'), $request->input('next_audit_end')]);
}
if (($request->filled('last_updated_start')) && ($request->filled('last_updated_end'))) {
$assets->whereBetween('assets.updated_at', [$request->input('last_updated_start'), $request->input('last_updated_end')]);
}
if ($request->filled('exclude_archived')) {
$assets->notArchived();
}

View File

@@ -25,32 +25,25 @@ class UserFilesController extends Controller
public function store(UploadFileRequest $request, User $user)
{
$this->authorize('update', $user);
$files = $request->file('file');
if (is_null($files)) {
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
foreach ($files as $file) {
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
if ($request->hasFile('file')) {
//Log the uploaded file to the log
$logAction = new Actionlog();
$logAction->item_id = $user->id;
$logAction->item_type = User::class;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('notes');
$logAction->target_id = null;
$logAction->created_at = date("Y-m-d H:i:s");
$logAction->filename = $file_name;
$logAction->action_type = 'uploaded';
if (! $logAction->save()) {
return JsonResponse::create(['error' => 'Failed validation: '.print_r($logAction->getErrors(), true)], 500);
if (! Storage::exists('private_uploads/users')) {
Storage::makeDirectory('private_uploads/users', 775);
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
$file_count = 0;
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/users/','hardware-'.$user->id, $file);
$user->logUpload($file_name, $request->get('notes'));
$file_count++;
}
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.upload.success', $file_count));
}
return redirect()->back()->with('error', trans('general.file_upload_status.nofiles'));
}

View File

@@ -19,6 +19,7 @@ class StoreAssetModelRequest extends ImageUploadRequest
public function prepareForValidation(): void
{
parent::prepareForValidation();
if ($this->category_id) {
if ($category = Category::find($this->category_id)) {

View File

@@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use \App\Helpers\Helper;
class UploadFileRequest extends Request
{
@@ -27,7 +28,7 @@ class UploadFileRequest extends Request
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
$max_file_size = 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,avif|max:'.$max_file_size,
@@ -37,34 +38,52 @@ class UploadFileRequest extends Request
/**
* 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();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_replace(' ', '-', $file->getClientOriginalName());
// 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);
}
$uploaded_file = $this->handleSVG($file);
} else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file));
$uploaded_file = file_get_contents($file);
}
try {
Storage::put($dirname.$file_name, $uploaded_file);
} catch (\Exception $e) {
Log::debug($e);
}
return $file_name;
}
public function handleSVG($file) {
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
return $sanitizer->sanitize($dirtySVG);
}
/**
* Get the validation error messages that apply to the request, but
* replace the attribute name with the name of the file that was attempted and failed
* to make it clearer to the user which file is the bad one.
* @return array
*/
public function attributes(): array
{
$attributes = [];
if ($this->file) {
for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
}
return $attributes;
}
}

View File

@@ -4,6 +4,10 @@ namespace App\Http\Transformers;
class DatatablesTransformer
{
/**
* Transform data for bootstrap tables and API responses for lists of things
**/
public function transformDatatables($objects, $total = null)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
@@ -11,4 +15,15 @@ class DatatablesTransformer
return $objects_array;
}
/**
* Transform data for returning the status of items within a bulk action
**/
public function transformBulkResponseWithStatusAndObjects($objects, $total)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
}

View File

@@ -13,16 +13,15 @@ class LicenseSeatsTransformer
public function transformLicenseSeats(Collection $seats, $total)
{
$array = [];
$seat_count = 0;
foreach ($seats as $seat) {
$seat_count++;
$array[] = self::transformLicenseSeat($seat, $seat_count);
$array[] = self::transformLicenseSeat($seat);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformLicenseSeat(LicenseSeat $seat, $seat_count = 0)
public function transformLicenseSeat(LicenseSeat $seat)
{
$array = [
'id' => (int) $seat->id,
@@ -55,10 +54,6 @@ class LicenseSeatsTransformer
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
];
if ($seat_count != 0) {
$array['name'] = trans('admin/licenses/general.seat_count', ['count' => $seat_count]);
}
$permissions_array['available_actions'] = [
'checkout' => Gate::allows('checkout', License::class),
'checkin' => Gate::allows('checkin', License::class),

View File

@@ -3,10 +3,10 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class UploadedFilesTransformer
@@ -26,23 +26,26 @@ class UploadedFilesTransformer
{
$snipeModel = $file->item_type;
// This will be used later as we extend out this transformer to handle more types of uploads
if ($file->item_type == Asset::class) {
$file_url = route('show/assetfile', [$file->item_id, $file->id]);
}
$array = [
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
] : null,
'filename' => e($file->filename),
'url' => $file_url,
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [
'id' => (int) $file->adminuser->id,
'name'=> e($file->adminuser->present()->fullName),
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($file->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
];
$permissions_array['available_actions'] = [
@@ -53,4 +56,5 @@ class UploadedFilesTransformer
return $array;
}
}

View File

@@ -71,12 +71,12 @@ class SlackSettingsForm extends Component
$this->setting = Setting::getSettings();
$this->save_button = trans('general.save');
$this->webhook_selected = $this->setting->webhook_selected;
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"];
$this->webhook_selected = $this->setting->webhook_selected ?? 'slack';
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"] ?? $this->webhook_text['slack']["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"] ?? $this->webhook_text['slack']["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"] ?? $this->webhook_text['slack']["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"] ?? $this->webhook_text['slack']["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"] ?? $this->webhook_text['slack']["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname;
@@ -90,7 +90,7 @@ class SlackSettingsForm extends Component
$this->isDisabled= '';
}
if($this->webhook_selected === 'microsoft' && $this->teams_webhook_deprecated) {
session()->flash('warning', 'The selected Microsoft Teams webhook URL will be deprecated Jan 31st, 2025. Please use a workflow URL. Microsofts Documentation on creating a workflow can be found <a href="https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498" target="_blank"> here.</a>');
session()->flash('warning', trans('admin/settings/message.webhook.ms_teams_deprecation'));
}
}
public function updated($field) {
@@ -191,6 +191,7 @@ class SlackSettingsForm extends Component
$this->setting->webhook_endpoint = '';
$this->setting->webhook_channel = '';
$this->setting->webhook_botname = '';
$this->setting->webhook_selected = '';
$this->setting->save();

View File

@@ -444,6 +444,62 @@ class Actionlog extends SnipeModel
}
public function uploads_file_url()
{
switch ($this->item_type) {
case Accessory::class:
return route('show/accessoryfile', [$this->item_id, $this->id]);
case Asset::class:
return route('show/assetfile', [$this->item_id, $this->id]);
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case Component::class:
return route('show/componentsfile', [$this->item_id, $this->id]);
case License::class:
return route('show/licensesfile', [$this->item_id, $this->id]);
case Location::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case User::class:
return route('show/userfile', [$this->item_id, $this->id]);
default:
return null;
}
}
public function uploads_file_path()
{
switch ($this->item_type) {
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
case Asset::class:
return 'private_uploads/assets/'.$this->filename;
case AssetModel::class:
return 'private_uploads/assetmodels/'.$this->filename;
case Consumable::class:
return 'private_uploads/consumables/'.$this->filename;
case Component::class:
return 'private_uploads/components/'.$this->filename;
case License::class:
return 'private_uploads/licenses/'.$this->filename;
case Location::class:
return 'private_uploads/locations/'.$this->filename;
case User::class:
return 'private_uploads/users/'.$this->filename;
default:
return null;
}
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{

View File

@@ -656,6 +656,8 @@ class Asset extends Depreciable
return Storage::disk('public')->url(app('assets_upload_path').e($this->image));
} elseif ($this->model && ! empty($this->model->image)) {
return Storage::disk('public')->url(app('models_upload_path').e($this->model->image));
} elseif ($this->model->category && ! empty($this->model->category->image)) {
return Storage::disk('public')->url(app('categories_upload_path').e($this->model->category->image));
}
return false;

View File

@@ -100,6 +100,14 @@ class Category extends SnipeModel
public function isDeletable()
{
// We have to check for models as well if the category type is asset
if ($this->category_type == 'asset') {
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->models_count == 0)
&& ($this->deleted_at == '');
}
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->deleted_at == '');

View File

@@ -113,7 +113,10 @@ class CustomFieldset extends Model
$rule[] = 'unique_undeleted';
}
array_push($rule, $field->attributes['format']);
if ($field->attributes['format']!='') {
array_push($rule, $field->attributes['format']);
}
$rules[$field->db_column_name()] = $rule;

View File

@@ -34,6 +34,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
use Notifiable;
use Presentable;
use Searchable;
use Loggable;
protected $hidden = ['password', 'remember_token', 'permissions', 'reset_password_code', 'persist_code'];
protected $table = 'users';
@@ -320,7 +321,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{
$setting = Setting::getSettings();
if ($setting->name_display_format=='last_first') {
if ($setting?->name_display_format == 'last_first') {
return ($this->last_name) ? $this->last_name.' '.$this->first_name : $this->first_name;
}
return $this->last_name ? $this->first_name.' '.$this->last_name : $this->first_name;

View File

@@ -298,6 +298,7 @@ class AssetPresenter extends Presenter
'sortable' => true,
'visible' => false,
'title' => trans('general.byod'),
'class' => 'byod',
'formatter' => 'trueFalseFormatter',
],

View File

@@ -230,16 +230,7 @@ class LicensePresenter extends Presenter
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
],
[
'field' => 'name',
'searchable' => false,
'sortable' => false,
'sorter' => 'numericOnly',
'switchable' => true,
'title' => trans('admin/licenses/general.seat'),
'visible' => true,
], [
],[
'field' => 'assigned_user',
'searchable' => false,
'sortable' => false,
@@ -285,7 +276,7 @@ class LicensePresenter extends Presenter
'searchable' => false,
'sortable' => true,
'visible' => false,
'title' => trans('general.date'),
'title' => trans('general.updated_at'),
'formatter' => 'dateDisplayFormatter',
],
[

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Presenters;
/**
* Class AccessoryPresenter
*/
class UploadsPresenter extends Presenter
{
/**
* Json Column Layout for bootstrap table
* @return string
*/
public static function dataTableLayout($object)
{
if ($object =='assets') {
$object = 'hardware';
}
$layout = [
[
'field' => 'id',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
],
[
'field' => 'icon',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('general.type'),
'formatter' => 'iconFormatter',
],
[
'field' => 'image',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.image'),
'formatter' => 'inlineImageFormatter',
],
[
'field' => 'filename',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.file_name'),
'visible' => true,
'formatter' => 'fileUploadNameFormatter',
],
[
'field' => 'download',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.download'),
'visible' => true,
'formatter' => 'downloadOrOpenInNewWindowFormatter',
],
[
'field' => 'note',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.notes'),
'visible' => true,
],
[
'field' => 'created_by',
'searchable' => false,
'sortable' => true,
'title' => trans('general.created_by'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
],
[
'field' => 'created_at',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.created_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'available_actions',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('table.actions'),
'formatter' => 'deleteUploadFormatter',
],
];
return json_encode($layout);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,6 @@
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=54d676a6ea8677dd48f6c4b3041292cf",
"/js/build/vendor.js": "/js/build/vendor.js?id=89dffa552c6e3abe3a2aac6c9c7b466b",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=783d3a8076337744f0176d60e1041ea4",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=03bcd02b9ca5e53e1390ee840cd0a561",
"/js/dist/all.js": "/js/dist/all.js?id=8c6d7286f667eeb62a0a28a09851a6c3"
}

View File

@@ -49,12 +49,11 @@ return [
'error_redirect' => 'ERROR: 301/302 :endpoint returns a redirect. For security reasons, we dont follow redirects. Please use the actual endpoint.',
'error_misc' => 'Something went wrong. :( ',
'webhook_fail' => ' webhook notification failed: Check to make sure the URL is still valid.',
'webhook_channel_not_found' => ' webhook channel not found.'
'webhook_channel_not_found' => ' webhook channel not found.',
'ms_teams_deprecation' => 'The selected Microsoft Teams webhook URL will be deprecated Dec 31st, 2025. Please use a workflow URL. Microsoft\'s documentation on creating a workflow can be found <a href="https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498" target="_blank"> here.</a>',
],
'location_scoping' => [
'not_saved' => 'Your settings were not saved.',
'mismatch' => 'There is 1 item in the database that need your attention before you can enable location scoping.|There are :count items in the database that need your attention before you can enable location scoping.',
],
];

View File

@@ -522,7 +522,7 @@ return [
'checked_out_to_fields' => 'Checked Out To Fields',
'percent_complete' => '% complete',
'uploading' => 'Uploading... ',
'upload_error' => 'Error uploading file. Please check that there are no empty rows and that no column names are duplicated.',
'upload_error' => 'Error uploading file. Please check that you have no empty rows or duplicated column names in your CSV, and that the server permissions allow uploads.',
'copy_to_clipboard' => 'Copy to Clipboard',
'copied' => 'Copied!',
'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.',
@@ -626,4 +626,23 @@ return [
],
],
'file_upload_status' => [
'upload' => [
'success' => 'File successfully uploaded |:count files successfully uploaded',
'error' => 'File upload failed |:count file uploads failed',
],
'delete' => [
'success' => 'File successfully deleted |:count files successfully deleted',
'error' => 'File deletion failed |:count file deletions failed',
],
'file_not_found' => 'The selected file was not found on server',
'invalid_id' => 'That file ID is invalid',
'invalid_object' => 'That object ID is invalid',
'nofiles' => 'No files were included for upload',
'confirm_delete' => 'Are you sure you want to delete this file?',
],
];

View File

@@ -154,6 +154,7 @@
filepath="private_uploads/accessories/"
showfile_routename="show.accessoryfile"
deletefile_routename="delete/accessoryfile"
object_type="accessories"
:object="$accessory" />
</div>
</div>
@@ -178,6 +179,17 @@
</div>
@endif
@if ($accessory->model_number)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>{{ trans('general.model_no')}}</strong>
</div>
<div class="col-md-9">
{{ $accessory->model_number }}
</div>
</div>
@endif
@if ($accessory->company)
<div class="row">
<div class="col-md-3" style="padding-bottom: 15px;">
@@ -189,6 +201,16 @@
</div>
@endif
@if ($accessory->location)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>{{ trans('general.location')}}</strong>
</div>
<div class="col-md-9">
<a href="{{ route('locations.show', $accessory->location->id) }}">{{ $accessory->location->name }} </a>
</div>
</div>
@endif
@if ($accessory->category)
<div class="row">
@@ -201,6 +223,18 @@
</div>
@endif
@if ($accessory->manufacturer)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>{{ trans('general.manufacturer')}}</strong>
</div>
<div class="col-md-9">
<a href="{{ route('manufacturers.show', $accessory->manufacturer->id) }}">{{ $accessory->manufacturer->name }} </a>
</div>
</div>
@endif
@if ($accessory->notes)
<div class="row">
@@ -213,9 +247,33 @@
{!! nl2br(Helper::parseEscapedMarkedownInline($accessory->notes)) !!}
</div>
</div>
@endif
@if ($accessory->purchase_date)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.purchase_date') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ \App\Helpers\Helper::getFormattedDateObject($accessory->purchase_date, 'date')['formatted']}}
</div>
</div>
@endif
@if ($accessory->purchase_cost)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.purchase_cost') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ Helper::formatCurrencyOutput($accessory->purchase_cost) }}
</div>
</div>
@endif
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
@@ -234,6 +292,43 @@
{{ $accessory->checkouts_count }}
</div>
</div>
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.created_at') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ \App\Helpers\Helper::getFormattedDateObject($accessory->created_at, 'datetime')['formatted']}}
</div>
</div>
@if ($accessory->created_at!=$accessory->updated_at)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.updated_at') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ \App\Helpers\Helper::getFormattedDateObject($accessory->updated_at, 'datetime')['formatted']}}
</div>
</div>
@endif
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.created_by') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ $accessory->adminuser->present()->fullName() }}
</div>
</div>
</div>
<div class="col-md-3 pull-right">

View File

@@ -99,7 +99,7 @@ use Carbon\Carbon;
{{ trans('admin/asset_maintenances/form.cost') }}
</div>
<div class="col-md-9">
{{ trans( 'general.currency' ) . Helper::formatCurrencyOutput($assetMaintenance->cost) }}
{{ \App\Models\Setting::getSettings()->default_currency .' '. Helper::formatCurrencyOutput($assetMaintenance->cost) }}
</div>
</div> <!-- /row -->
@endif

View File

@@ -0,0 +1,32 @@
{{-- IMPORTANT!!! Make sure there is no newline at the end of this file, or it will break the loaders for the tables --}}
@props([
'route',
'id',
'method',
])
<div {{ $attributes->merge() }} id="dataConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h2 class="modal-title" id="myModalLabel">&nbsp;</h2>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="{{ isset($id) ?? $id }}" role="form"{!! isset($route) ?? ' action="'.route($route).'"' !!}>
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">
{{ trans('general.cancel') }}
</button>
<button type="submit" class="btn btn-outline" id="dataConfirmOK">
{{ trans('general.yes') }}
</button>
</form>
</div>
</div>
</div>
</div>

View File

@@ -4,139 +4,74 @@
'object',
'showfile_routename',
'deletefile_routename',
'object_type',
])
<!-- begin non-ajaxed file listing table -->
<div class="table-responsive">
<table
data-columns="{{ \App\Presenters\UploadsPresenter::dataTableLayout($object_type) }}"
data-cookie-id-table="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
data-id-table="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
id="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
data-search="true"
data-show-custom-view="true"
data-custom-view="fileGalleryFormatter"
data-show-custom-view-button="true"
data-pagination="true"
data-side-pagination="client"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-toolbar="#upload-toolbar"
data-show-refresh="true"
data-card-view="true"
data-show-toggle="true"
data-sort-order="asc"
data-sort-name="name"
class="table table-striped snipe-table"
data-url="{{ route("api.files.index", ['object_type' => $object_type, 'id' => $object->id]) }}"
data-export-options='{
"fileName": "export-license-uploads-{{ str_slug($object->name) }}-{{ date('Y-m-d') }}",
"fileName": "export-uploads-{{ str_slug($object->name) }}-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","delete","download","icon"]
}'>
<thead>
<tr>
<th data-visible="false" data-field="id" data-sortable="true">
{{trans('general.id')}}
</th>
<th data-visible="true" data-field="type" data-sortable="true">
{{trans('general.file_type')}}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="image">
{{ trans('general.image') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="filename" data-sortable="true">
{{ trans('general.file_name') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="filesize">
{{ trans('general.filesize') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="notes" data-sortable="true">
{{ trans('general.notes') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="download">
{{ trans('general.download') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="created_at" data-sortable="true">
{{ trans('general.created_at') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="created_by" data-sortable="true">
{{ trans('general.created_by') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="actions">
{{ trans('table.actions') }}
</th>
</tr>
</thead>
<tbody>
@foreach ($object->uploads as $file)
<tr>
<td>
{{ $file->id }}
</td>
<td data-sort-value="{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}">
@if (Storage::exists($filepath.$file->filename))
<span class="sr-only">{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}</span>
<i class="{{ Helper::filetype_icon($file->filename) }} icon-med" aria-hidden="true" data-tooltip="true" data-title="{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}"></i>
@endif
</td>
<td>
@if (($file->filename) && (Storage::exists($filepath.$file->filename)))
@if (Helper::checkUploadIsImage($file->get_src(str_plural(strtolower(class_basename(get_class($object)))))))
<a href="{{ route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) }}" data-toggle="lightbox" data-type="image">
<img src="{{ route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) }}" class="img-thumbnail" style="max-width: 50px;">
</a>
@else
{{ trans('general.preview_not_available') }}
@endif
@else
<x-icon type="x" class="text-danger" />
{{ trans('general.file_not_found') }}
@endif
</td>
<td>
{{ $file->filename }}
</td>
<td data-value="{{ (Storage::exists($filepath.$file->filename)) ? Storage::size($filepath.$file->filename) : '' }}">
{{ (Storage::exists($filepath.$file->filename)) ? Helper::formatFilesizeUnits(Storage::size($filepath.$file->filename)) : '' }}
</td>
<td>
@if ($file->note)
{{ $file->note }}
@endif
</td>
<td style="white-space: nowrap;">
@if ($file->filename)
@if (Storage::exists($filepath.$file->filename))
<a href="{{ route($showfile_routename, [$object->id, $file->id]) }}" class="btn btn-sm btn-default">
<x-icon type="download" />
<span class="sr-only">
{{ trans('general.download') }}
</span>
</a>
<a href="{{ StorageHelper::allowSafeInline($filepath.$file->filename) ? route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) : '#' }}" class="btn btn-sm btn-default{{ StorageHelper::allowSafeInline($filepath.$file->filename) ? '' : ' disabled' }}" target="_blank">
<x-icon type="external-link" />
</a>
@endif
@endif
</td>
<td>
{{ $file->created_at }}
</td>
<td>
{{ ($file->adminuser) ? $file->adminuser->present()->getFullNameAttribute() : '' }}
</td>
<td>
<a class="btn delete-asset btn-danger btn-sm hidden-print" href="{{ route($deletefile_routename, [$object->id, $file->id]) }}" data-content="Are you sure you wish to delete this file?" data-title="{{ trans('general.delete') }} {{ $file->filename }}?">
<x-icon type="delete" />
<span class="sr-only">{{ trans('general.delete') }}</span>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- this is used by the bootstrap-table partial to format the file gallery -->
<template id="fileGalleryTemplate">
<div class="col-md-4">
<div class="panel panel-%PANEL_CLASS%">
<div class="panel-heading">
<h3 class="panel-title" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<i class="%ICON%"></i> %FILENAME%
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
%INLINE_IMAGE%
<br>
<p>
<strong>{{ trans('general.created_at') }}:</strong> %CREATED_AT% <br>
<strong>{{ trans('general.created_by') }}:</strong> %CREATED_BY% <br>
<strong>{{ trans('general.notes') }}:</strong> %NOTE%
</p>
</div>
</div>
</div>
<div class="panel-footer">
<div class="pull-right">
%DOWNLOAD_BUTTON% %NEW_WINDOW_BUTTON%
</div>
%DELETE_BUTTON%
</div>
</div>
</div>
</template>
<!-- end non-ajaxed file listing table -->

View File

@@ -146,6 +146,7 @@
filepath="private_uploads/components/"
showfile_routename="show.componentfile"
deletefile_routename="delete/componentfile"
object_type="components"
:object="$component" />
</div>
</div>

View File

@@ -437,6 +437,7 @@
filepath="private_uploads/consumables/"
showfile_routename="show.consumablefile"
deletefile_routename="delete/consumablefile"
object_type="consumables"
:object="$consumable" />
</div>

View File

@@ -106,11 +106,11 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
{{ Form::radio('update_default_location', '1', old('update_default_location'), ['checked'=> 'checked', 'aria-label'=>'update_default_location']) }}
<input name="update_default_location" type="radio" value="1" checked="checked" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location') }}
</label>
<label class="form-control">
{{ Form::radio('update_default_location', '0', old('update_default_location'), ['aria-label'=>'update_default_location']) }}
<input name="update_default_location" type="radio" value="0" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location_update_default_current') }}
</label>
</div>

View File

@@ -137,7 +137,7 @@
<!-- byod checkbox -->
<div class="form-group">
<div class="form-group byod">
<div class="col-md-7 col-md-offset-3">
<label class="form-control">
<input type="checkbox" value="1" name="byod" {{ (old('remote', $item->byod)) == '1' ? ' checked="checked"' : '' }} aria-label="byod">

View File

@@ -699,7 +699,7 @@
</div>
<!-- byod -->
<div class="row">
<div class="row byod">
<div class="col-md-3">
<strong>{{ trans('general.byod') }}</strong>
</div>
@@ -1479,6 +1479,7 @@
filepath="private_uploads/assets/"
showfile_routename="show/assetfile"
deletefile_routename="delete/assetfile"
object_type="assets"
:object="$asset" />
</div> <!-- /.col-md-12 -->
</div> <!-- /.row -->
@@ -1494,6 +1495,7 @@
filepath="private_uploads/assetmodels/"
showfile_routename="show/modelfile"
deletefile_routename="delete/modelfile"
object_type="models"
:object="$asset->model" />
</div> <!-- /.col-md-12 -->

View File

@@ -266,19 +266,21 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan
@can('admin')
@if ($snipeSettings->show_alerts_in_menu=='1')
<!-- Tasks: style can be found in dropdown.less -->
<?php $alert_items = Helper::checkLowInventory(); $deprecations = Helper::deprecationCheck()?>
<!-- Tasks: style can be found in dropdown.less -->
<?php $alert_items = ($snipeSettings->show_alerts_in_menu=='1') ? Helper::checkLowInventory() : [];
$deprecations = Helper::deprecationCheck()
?>
<li class="dropdown tasks-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<x-icon type="alerts" />
<span class="sr-only">{{ trans('general.alerts') }}</span>
@if (count($alert_items) || count($deprecations))
<span class="label label-danger">{{ count($alert_items) + count($deprecations) }}</span>
@endif
</a>
<ul class="dropdown-menu">
<li class="dropdown tasks-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<x-icon type="alerts" />
<span class="sr-only">{{ trans('general.alerts') }}</span>
@if(count($alert_items) + count($deprecations))
<span class="label label-danger">{{ count($alert_items) + count($deprecations)}}</span>
@endif
</a>
<ul class="dropdown-menu">
@can('superadmin')
@if($deprecations)
@foreach ($deprecations as $key => $deprecation)
@if ($deprecation['check'])
@@ -286,11 +288,12 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endif
@endforeach
@endif
<li class="header">{{ trans_choice('general.quantity_minimum', count($alert_items)) }}</li>
@endcan
@if($alert_items)
<li class="header">{{ trans_choice('general.quantity_minimum', count($alert_items)) }}</li>
<li>
<!-- inner menu: contains the actual data -->
<!-- inner menu: contains the actual data -->
<ul class="menu">
@for($i = 0; count($alert_items) > $i; $i++)
<li><!-- Task item -->
@@ -315,13 +318,13 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endfor
</ul>
</li>
{{-- <li class="footer">
<a href="#">{{ trans('general.tasks_view_all') }}</a>
</li> --}}
</ul>
</li>
@endcan
@endif
@endif
{{-- <li class="footer">
<a href="#">{{ trans('general.tasks_view_all') }}</a>
</li> --}}
</ul>
</li>
@endcan
@@ -475,48 +478,48 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endif
<li{!! (Request::query('status') == 'Deployed' ? ' class="active"' : '') !!}>
<li id="deployed-sidenav-option" {!! (Request::query('status') == 'Deployed' ? ' class="active"' : '') !!}>
<a href="{{ url('hardware?status=Deployed') }}">
<x-icon type="circle" class="text-blue fa-fw" />
{{ trans('general.deployed') }}
<span class="badge">{{ (isset($total_deployed_sidebar)) ? $total_deployed_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'RTD' ? ' class="active"' : '') !!}>
<li id="rtd-sidenav-option"{!! (Request::query('status') == 'RTD' ? ' class="active"' : '') !!}>
<a href="{{ url('hardware?status=RTD') }}">
<x-icon type="circle" class="text-green fa-fw" />
{{ trans('general.ready_to_deploy') }}
<span class="badge">{{ (isset($total_rtd_sidebar)) ? $total_rtd_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Pending' ? ' class="active"' : '') !!}><a href="{{ url('hardware?status=Pending') }}">
<li id="pending-sidenav-option"{!! (Request::query('status') == 'Pending' ? ' class="active"' : '') !!}><a href="{{ url('hardware?status=Pending') }}">
<x-icon type="circle" class="text-orange fa-fw" />
{{ trans('general.pending') }}
<span class="badge">{{ (isset($total_pending_sidebar)) ? $total_pending_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Undeployable' ? ' class="active"' : '') !!} ><a
<li id="undeployable-sidenav-option"{!! (Request::query('status') == 'Undeployable' ? ' class="active"' : '') !!} ><a
href="{{ url('hardware?status=Undeployable') }}">
<x-icon type="x" class="text-red fa-fw" />
{{ trans('general.undeployable') }}
<span class="badge">{{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'byod' ? ' class="active"' : '') !!}><a
<li id="byod-sidenav-option"{!! (Request::query('status') == 'byod' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=byod') }}">
<x-icon type="x" class="text-red fa-fw" />
{{ trans('general.byod') }}
<span class="badge">{{ (isset($total_byod_sidebar)) ? $total_byod_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Archived' ? ' class="active"' : '') !!}><a
<li id="archived-sidenav-option"{!! (Request::query('status') == 'Archived' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=Archived') }}">
<x-icon type="x" class="text-red fa-fw" />
{{ trans('admin/hardware/general.archived') }}
<span class="badge">{{ (isset($total_archived_sidebar)) ? $total_archived_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Requestable' ? ' class="active"' : '') !!}><a
<li id="requestable-sidenav-option"{!! (Request::query('status') == 'Requestable' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=Requestable') }}">
<x-icon type="checkmark" class="text-blue fa-fw" />
{{ trans('admin/hardware/general.requestable') }}
@@ -524,7 +527,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@can('audit', \App\Models\Asset::class)
<li{!! (Request::is('hardware/audit/due') ? ' class="active"' : '') !!}>
<li id="audit-due-sidenav-option"{!! (Request::is('hardware/audit/due') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.audit.due') }}">
<x-icon type="audit" class="text-yellow fa-fw"/>
{{ trans('general.audit_due') }}
@@ -534,7 +537,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan
@can('checkin', \App\Models\Asset::class)
<li{!! (Request::is('hardware/checkins/due') ? ' class="active"' : '') !!}>
<li id="checkin-due-sidenav-option"{!! (Request::is('hardware/checkins/due') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.checkins.due') }}">
<x-icon type="due" class="text-orange fa-fw"/>
{{ trans('general.checkin_due') }}
@@ -577,14 +580,14 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('admin')
<li>
<li id="import-history-sidenav-option">
<a href="{{ url('hardware/history') }}">
{{ trans('general.import-history') }}
</a>
</li>
@endcan
@can('audit', \App\Models\Asset::class)
<li>
<li id="bulk-audit-sidenav-option">
<a href="{{ route('assets.bulkaudit') }}">
{{ trans('general.bulkaudit') }}
</a>
@@ -602,7 +605,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('index', \App\Models\Accessory::class)
<li{!! (Request::is('accessories*') ? ' class="active"' : '') !!}>
<li id="accessories-sidenav-option"{!! (Request::is('accessories*') ? ' class="active"' : '') !!}>
<a href="{{ route('accessories.index') }}">
<x-icon type="accessories" class="fa-fw" />
<span>{{ trans('general.accessories') }}</span>
@@ -610,7 +613,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('view', \App\Models\Consumable::class)
<li{!! (Request::is('consumables*') ? ' class="active"' : '') !!}>
<li id="consumables-sidenav-option"{!! (Request::is('consumables*') ? ' class="active"' : '') !!}>
<a href="{{ url('consumables') }}">
<x-icon type="consumables" class="fa-fw" />
<span>{{ trans('general.consumables') }}</span>
@@ -618,7 +621,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('view', \App\Models\Component::class)
<li{!! (Request::is('components*') ? ' class="active"' : '') !!}>
<li id="components-sidenav-option"{!! (Request::is('components*') ? ' class="active"' : '') !!}>
<a href="{{ route('components.index') }}">
<x-icon type="components" class="fa-fw" />
<span>{{ trans('general.components') }}</span>
@@ -626,7 +629,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('view', \App\Models\PredefinedKit::class)
<li{!! (Request::is('kits') ? ' class="active"' : '') !!}>
<li id="kits-sidenav-option"{!! (Request::is('kits') ? ' class="active"' : '') !!}>
<a href="{{ route('kits.index') }}">
<x-icon type="kits" class="fa-fw" />
<span>{{ trans('general.kits') }}</span>
@@ -635,7 +638,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan
@can('view', \App\Models\User::class)
<li{!! (Request::is('users*') ? ' class="active"' : '') !!}>
<li id="users-sidenav-option"{!! (Request::is('users*') ? ' class="active"' : '') !!}>
<a href="{{ route('users.index') }}" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=6" : ''}}>
<x-icon type="users" class="fa-fw" />
<span>{{ trans('general.people') }}</span>
@@ -643,7 +646,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
</li>
@endcan
@can('import')
<li{!! (Request::is('import/*') ? ' class="active"' : '') !!}>
<li id="import-sidenav-option"{!! (Request::is('import/*') ? ' class="active"' : '') !!}>
<a href="{{ route('imports.index') }}">
<x-icon type="import" class="fa-fw" />
<span>{{ trans('general.import') }}</span>
@@ -652,7 +655,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@endcan
@can('backend.interact')
<li class="treeview {!! in_array(Request::route()->getName(),App\Helpers\Helper::SettingUrls()) ? ' active': '' !!}">
<li id="settings-sidenav-option" class="treeview {!! in_array(Request::route()->getName(),App\Helpers\Helper::SettingUrls()) ? ' active': '' !!}">
<a href="#" id="settings">
<x-icon type="settings" class="fa-fw" />
<span>{{ trans('general.settings') }}</span>
@@ -959,55 +962,8 @@ dir="{{ Helper::determineLanguageDirection() }}">
<!-- end main container -->
<div class="modal modal-danger fade" id="dataConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h2 class="modal-title" id="myModalLabel">&nbsp;</h2>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="deleteForm" role="form">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="button" class="btn btn-default pull-left"
data-dismiss="modal">{{ trans('general.cancel') }}</button>
<button type="submit" class="btn btn-outline"
id="dataConfirmOK">{{ trans('general.yes') }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal modal-warning fade" id="restoreConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="confirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="confirmModalLabel">&nbsp;</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="restoreForm" role="form">
{{ csrf_field() }}
{{ method_field('POST') }}
<button type="button" class="btn btn-default pull-left"
data-dismiss="modal">{{ trans('general.cancel') }}</button>
<button type="submit" class="btn btn-outline"
id="dataConfirmOK">{{ trans('general.yes') }}</button>
</form>
</div>
</div>
</div>
</div>
<x-confirm-modal class="modal modal-danger fade" id="deleteModal" method="DELETE"/>
<x-confirm-modal class="modal modal-warning fade" id="restoreModal" method="POST"/>
{{-- Javascript files --}}

View File

@@ -469,6 +469,7 @@
filepath="private_uploads/licenses/"
showfile_routename="show.licensefile"
deletefile_routename="delete/licensefile"
object_type="licenses"
:object="$license" />
</div> <!-- /.tab-pane -->

View File

@@ -414,6 +414,7 @@
filepath="private_uploads/locations/"
showfile_routename="show/locationsfile"
deletefile_routename="delete/locationsfile"
object_type="locations"
:object="$location" />
</div> <!-- /.col-md-12 -->

View File

@@ -121,6 +121,7 @@
filepath="private_uploads/assetmodels/"
showfile_routename="show/modelfile"
deletefile_routename="delete/modelfile"
object_type="models"
:object="$model" />
</div> <!-- /.col-md-12 -->

View File

@@ -344,6 +344,48 @@
return '<a href="{{ config('app.url') }}/hardware/' + row.id + '/audit" class="actions btn btn-sm btn-primary" data-tooltip="true" title="{{ trans('general.audit') }}"><x-icon type="audit" /><span class="sr-only">{{ trans('general.audit') }}</span></a>&nbsp;';
}
function deleteUploadFormatter(value, row) {
var item_type = row.item.type;
if (item_type == 'assetmodel') {
item_type = 'models'
}
if ((row.available_actions) && (row.available_actions.delete === true)) {
return '<a href="{{ config('app.url') }}/' + item_type + '/' + row.item.id + '/showfile/' + row.id + '/delete" '
+ ' class="actions btn btn-danger btn-sm delete-asset" data-tooltip="true" '
+ ' data-toggle="modal" '
+ ' data-content="{{ trans('general.file_upload_status.confirm_delete') }} ' + row.filename + '?" '
+ ' data-title="{{ trans('general.delete') }}" onClick="return false;">'
+ '<x-icon type="delete" /><span class="sr-only">{{ trans('general.delete') }}</span></a>&nbsp;';
}
}
// This handles the custom view for the filestable blade component
window.fileGalleryFormatter = data => {
const template = $('#fileGalleryTemplate').html()
let view = ''
$.each(data, function (i, row) {
view += template.replace('%ID%', row.id)
.replace('%ICON%', row.icon)
.replace('%FILETYPE%', row.filetype)
.replace('%FILE_URL%', row.url)
.replace('%LINK_URL%', row.url)
.replace('%FILENAME%', (row.exists_on_disk === true) ? row.filename : '<x-icon type="x" /> <del>' + row.filename + '</del>')
.replace('%CREATED_AT%', row.created_at.formatted)
.replace('%CREATED_BY%', row.created_by.name)
.replace('%NOTE%', row.note)
.replace('%PANEL_CLASS%', (row.exists_on_disk === true) ? 'default' : 'danger')
.replace('%INLINE_IMAGE%', (row.inline === true) ? '<a href="' + row.url + '" data-toggle="lightbox" data-type="image"><img src="' + row.url + '" alt="" class="img-thumbnail" style="max-height: 50px; width: auto;"></a>' : '')
.replace('%DOWNLOAD_BUTTON%', (row.exists_on_disk === true) ? '<a href="'+ row.url +'" class="btn btn-sm btn-default"><x-icon type="download" /></a> ' : '<span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}"><x-icon type="download" /></span>')
.replace('%NEW_WINDOW_BUTTON%', (row.exists_on_disk === true) ? '<a href="'+ row.url +'?inline=true" class="btn btn-sm btn-default" target="_blank"><x-icon type="external-link" /></a> ' : '<span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}" ><x-icon type="external-link"/></span>')
.replace('%DELETE_BUTTON%', ((row.exists_on_disk === true) && row.available_actions.delete === true) ? '<a href="'+ row.url +'" class="btn btn-sm btn-danger"><x-icon type="delete" /></a> ' : '<span class="btn btn-sm btn-danger disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}" ><x-icon type="delete"/></span>');
})
return `<div class="row">${view}</div>`
}
// Make the edit/delete buttons
function genericActionsFormatter(owner_name, element_name) {
@@ -378,7 +420,8 @@
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') }}"><x-icon type="edit" /><span class="sr-only">{{ trans('general.update') }}</span></a>&nbsp;';
} else {
if ((row.available_actions) && (row.available_actions.update != true)) {
// check that row.available_actions.update is set in the API response - if not, don't even show the button
if ((row.available_actions) && (row.available_actions.update) && (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;"><x-icon type="edit" /></a></span>&nbsp;';
}
}
@@ -785,9 +828,13 @@
}
}
function iconFormatter(value) {
function iconFormatter(value, row) {
if (value) {
if (row.filetype) {
return '<span data-tooltip="true" title=" ' + row.filetype + '"><i class="' + value + ' icon-med"></i></span>';
}
return '<i class="' + value + ' icon-med"></i>';
}
}
@@ -850,6 +897,12 @@
}
}
function inlineImageFormatter(value, row){
if (row.inline) {
return '<a href="' + row.url + '" data-toggle="lightbox" data-type="image"><img src="' + row.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a>'
}
}
function imageFormatter(value, row) {
@@ -868,25 +921,51 @@
return '<a href="' + value + '" data-toggle="lightbox" data-type="image"><img src="' + value + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt="' + altName + '"></a>';
}
}
function downloadFormatter(value) {
if (value) {
return '<a href="' + value + '" target="_blank"><x-icon type="download" /></a>';
function downloadOrOpenInNewWindowFormatter(value, row) {
if (row && row.url)
{
if (row.exists_on_disk) {
return '<span style="white-space: nowrap;"><a href="' + row.url + '" class="btn btn-sm btn-default"><x-icon type="download" /></a> <a href="' + row.url + '?inline=true" class="btn btn-sm btn-default" target="_blank"><x-icon type="external-link" /></a></span>';
} else {
return '<span style="white-space: nowrap;"><span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="download" /></span> <span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="external-link" /></span></span>';
}
}
}
function fileUploadFormatter(value) {
if ((value) && (value.url) && (value.inlineable)) {
return '<a href="' + value.url + '" data-toggle="lightbox" data-type="image"><img src="' + value.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a>';
return '<nowrap><a href="' + value.url + '" data-toggle="lightbox" data-type="image"><img src="' + value.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a></nowrap>';
} else if ((value) && (value.url)) {
return '<a href="' + value.url + '" class="btn btn-default"><x-icon type="download" /></a>';
}
}
function fileUploadNameFormatter(value) {
console.dir(value);
function fileUploadNameFormatter(row, value) {
if ((value) && (value.filename) && (value.url)) {
return '<a href="' + value.url + '">' + value.filename + '</a>';
if (value.exists_on_disk) {
return '<a href="' + value.url + '?inline=true" target="_blank">' + value.filename + '</a>';
} else {
return '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="x" class="text-danger" /> <del>' + value.filename + '</del></span>';
}
}
}
function fileUploadDeleteFormatter(row, value) {
if ((value) && (value.filename) && (value.url)) {
if (value.exists_on_disk) {
return '<a href="' + value.url + '?inline=true" target="_blank">' + value.filename + '</a>';
} else {
return '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="x" class="text-danger" /> <del>' + value.filename + '</del></span>';
}
}
}

View File

@@ -40,11 +40,11 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
{{ Form::radio('update_default_location', '1', old('update_default_location'), ['checked'=> 'checked', 'aria-label'=>'update_default_location']) }}
<input name="update_default_location" type="radio" value="1" checked="checked" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location') }}
</label>
<label class="form-control">
{{ Form::radio('update_default_location', '0', old('update_default_location'), ['aria-label'=>'update_default_location']) }}
<input name="update_default_location" type="radio" value="0" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location_update_default_current') }}
</label>
</div>

View File

@@ -411,7 +411,7 @@
<!-- Purchase Date -->
<div class="form-group purchase-range{{ ($errors->has('purchase_start') || $errors->has('purchase_end')) ? ' has-error' : '' }}">
<label for="purchase_start" class="col-md-3 control-label">{{ trans('general.purchase_date') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="purchase-range-datepicker">
<input type="text" class="form-control" name="purchase_start" aria-label="purchase_start" value="{{ $template->textValue('purchase_start', old('purchase_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="purchase_end" aria-label="purchase_end" value="{{ $template->textValue('purchase_end', old('purchase_end')) }}">
@@ -427,9 +427,9 @@
</div>
<!-- Created Date -->
<div class="form-group purchase-range{{ ($errors->has('created_start') || $errors->has('created_end')) ? ' has-error' : '' }}">
<div class="form-group created-range{{ ($errors->has('created_start') || $errors->has('created_end')) ? ' has-error' : '' }}">
<label for="created_start" class="col-md-3 control-label">{{ trans('general.created_at') }} </label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="created-range-datepicker">
<input type="text" class="form-control" name="created_start" aria-label="created_start" value="{{ $template->textValue('created_start', old('created_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="created_end" aria-label="created_end" value="{{ $template->textValue('created_end', old('created_end')) }}">
@@ -446,7 +446,7 @@
<!-- Checkout Date -->
<div class="form-group checkout-range{{ ($errors->has('checkout_date_start') || $errors->has('checkout_date_end')) ? ' has-error' : '' }}">
<label for="checkout_date" class="col-md-3 control-label">{{ trans('general.checkout') }} </label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="checkout-range-datepicker">
<input type="text" class="form-control" name="checkout_date_start" aria-label="checkout_date_start" value="{{ $template->textValue('checkout_date_start', old('checkout_date_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="checkout_date_end" aria-label="checkout_date_end" value="{{ $template->textValue('checkout_date_end', old('checkout_date_end')) }}">
@@ -464,7 +464,7 @@
<!-- Last Checkin Date -->
<div class="form-group checkin-range{{ ($errors->has('checkin_date_start') || $errors->has('checkin_date_end')) ? ' has-error' : '' }}">
<label for="checkin_date" class="col-md-3 control-label">{{ trans('admin/hardware/table.last_checkin_date') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="checkin-range-datepicker">
<input type="text" class="form-control" name="checkin_date_start" aria-label="checkin_date_start" value="{{ $template->textValue('checkin_date_start', old('checkin_date_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="checkin_date_end" aria-label="checkin_date_end" value="{{ $template->textValue('checkin_date_end', old('checkin_date_end')) }}">
@@ -481,7 +481,7 @@
<!-- Expected Checkin Date -->
<div class="form-group expected_checkin-range{{ ($errors->has('expected_checkin_start') || $errors->has('expected_checkin_end')) ? ' has-error' : '' }}">
<label for="expected_checkin_start" class="col-md-3 control-label">{{ trans('admin/hardware/form.expected_checkin') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="expected_checkin-range-datepicker">
<input type="text" class="form-control" name="expected_checkin_start" aria-label="expected_checkin_start" value="{{ $template->textValue('expected_checkin_start', old('expected_checkin_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="expected_checkin_end" aria-label="expected_checkin_end" value="{{ $template->textValue('expected_checkin_end', old('expected_checkin_end')) }}">
@@ -499,7 +499,7 @@
<!-- EoL Date -->
<div class="form-group asset_eol_date-range {{ ($errors->has('asset_eol_date_start') || $errors->has('asset_eol_date_end')) ? ' has-error' : '' }}">
<label for="asset_eol_date" class="col-md-3 control-label">{{ trans('admin/hardware/form.eol_date') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="asset_eol_date-range-datepicker">
<input type="text" class="form-control" name="asset_eol_date_start" aria-label="asset_eol_date_start" value="{{ $template->textValue('asset_eol_date_start', old('asset_eol_date_start')) }}">
<span class="input-group-addon">to</span>
<input type="text" class="form-control" name="asset_eol_date_end" aria-label="asset_eol_date_end" value="{{ $template->textValue('asset_eol_date_end', old('asset_eol_date_end')) }}">
@@ -516,7 +516,7 @@
<!-- Last Audit Date -->
<div class="form-group last_audit-range{{ ($errors->has('last_audit_start') || $errors->has('last_audit_end')) ? ' has-error' : '' }}">
<label for="last_audit_start" class="col-md-3 control-label">{{ trans('general.last_audit') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="last_audit-range-datepicker">
<input type="text" class="form-control" name="last_audit_start" aria-label="last_audit_start" value="{{ $template->textValue('last_audit_start', old('last_audit_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="last_audit_end" aria-label="last_audit_end" value="{{ $template->textValue('last_audit_end', old('last_audit_end')) }}">
@@ -533,7 +533,7 @@
<!-- Next Audit Date -->
<div class="form-group next_audit-range{{ ($errors->has('next_audit_start') || $errors->has('next_audit_end')) ? ' has-error' : '' }}">
<label for="next_audit_start" class="col-md-3 control-label">{{ trans('general.next_audit_date') }}</label>
<div class="input-daterange input-group col-md-7" id="datepicker">
<div class="input-daterange input-group col-md-7" id="next_audit-range-datepicker">
<input type="text" class="form-control" name="next_audit_start" aria-label="next_audit_start" value="{{ $template->textValue('next_audit_start', old('next_audit_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="next_audit_end" aria-label="next_audit_end" value="{{ $template->textValue('next_audit_end', old('next_audit_end')) }}">
@@ -547,6 +547,24 @@
@endif
</div>
<!-- Last updated Date -->
<div class="form-group last_updated-range{{ ($errors->has('last_updated_start') || $errors->has('last_updated_end')) ? ' has-error' : '' }}">
<label for="last_updated_start" class="col-md-3 control-label">{{ trans('general.updated_at') }}</label>
<div class="input-daterange input-group col-md-7" id="last_updated-range-datepicker">
<input type="text" class="form-control" name="last_updated_start" aria-label="last_updated_start" value="{{ $template->textValue('last_updated_start', old('last_updated_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="text" class="form-control" name="last_updated_end" aria-label="last_updated_end" value="{{ $template->textValue('last_updated_end', old('last_updated_end')) }}">
</div>
@if ($errors->has('last_updated_start') || $errors->has('last_updated_end'))
<div class="col-md-9 col-lg-offset-3">
{!! $errors->first('last_updated_start', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
{!! $errors->first('last_updated_end', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
@endif
</div>
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="exclude_archived" value="1" @checked($template->checkmarkValue('exclude_archived', '0')) />
@@ -759,6 +777,14 @@
format: 'yyyy-mm-dd'
});
$('.last_updated-range .input-daterange').datepicker({
clearBtn: true,
todayHighlight: true,
endDate:'0d',
format: 'yyyy-mm-dd'
});
$("#checkAll").change(function () {
$("#included_fields_wrapper input:checkbox").prop('checked', $(this).prop("checked"));
});

View File

@@ -963,6 +963,8 @@
filepath="private_uploads/users/"
showfile_routename="show/userfile"
deletefile_routename="userfile.destroy"
object_type="users"
data_route="api.files.index"
:object="$user" />
</div>
</div> <!--/ROW-->

View File

@@ -549,34 +549,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.assets.restore');
Route::post('{asset}/files',
[
Api\AssetFilesController::class,
'store'
]
)->name('api.assets.files.store');
Route::get('{asset}/files',
[
Api\AssetFilesController::class,
'list'
]
)->name('api.assets.files.index');
Route::get('{asset_id}/file/{file_id}',
[
Api\AssetFilesController::class,
'show'
]
)->name('api.assets.files.show');
Route::delete('{asset_id}/file/{file_id}',
[
Api\AssetFilesController::class,
'destroy'
]
)->name('api.assets.files.destroy');
/** Begin assigned routes */
@@ -767,6 +739,8 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.locations.assigned_accessories');
/** End assigned routes */
});
Route::resource('locations',
@@ -846,33 +820,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.models.restore');
Route::post('{model_id}/files',
[
Api\AssetModelFilesController::class,
'store'
]
)->name('api.models.files.store');
Route::get('{model_id}/files',
[
Api\AssetModelFilesController::class,
'list'
]
)->name('api.models.files.index');
Route::get('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'show'
]
)->name('api.models.files.show');
Route::delete('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'destroy'
]
)->name('api.models.files.destroy');
});
Route::resource('models',
@@ -1129,12 +1076,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.users.licenselist');
Route::post('{user}/upload',
[
Api\UsersController::class,
'postUpload'
]
)->name('api.users.uploads');
Route::post('{user}/restore',
[
@@ -1143,6 +1084,8 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.users.restore');
});
Route::resource('users',
@@ -1347,4 +1290,46 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
// end generate label routes
/**
* Uploaded files API routes
*/
// List files
Route::get('{object_type}/{id}/files',
[
Api\UploadedFilesController::class,
'index'
]
)->name('api.files.index')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Get a file
Route::get('{object_type}/{id}/files/{file_id}',
[
Api\UploadedFilesController::class,
'show'
]
)->name('api.files.show')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Upload files(s)
Route::post('{object_type}/{id}/files',
[
Api\UploadedFilesController::class,
'store'
]
)->name('api.files.store')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Delete files(s)
Route::delete('{object_type}/{id}/files/{file_id}/delete',
[
Api\UploadedFilesController::class,
'destroy'
]
)->name('api.files.destroy')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
}); // end API routes

View File

@@ -5,7 +5,6 @@ use App\Http\Controllers\Assets\AssetsController;
use App\Http\Controllers\Assets\BulkAssetsController;
use App\Http\Controllers\Assets\AssetCheckoutController;
use App\Http\Controllers\Assets\AssetCheckinController;
use App\Http\Controllers\Assets\AssetFilesController;
use App\Models\Setting;
use Tabuna\Breadcrumbs\Trail;
use Illuminate\Support\Facades\Route;
@@ -141,17 +140,6 @@ Route::group(
[AssetsController::class, 'getRestore']
)->name('restore/hardware')->withTrashed();
Route::post('{asset}/upload',
[AssetFilesController::class, 'store']
)->name('upload/asset')->withTrashed();
Route::get('{asset}/showfile/{fileId}/{download?}',
[AssetFilesController::class, 'show']
)->name('show/assetfile')->withTrashed();
Route::delete('{asset}/showfile/{fileId}/delete',
[AssetFilesController::class, 'destroy']
)->name('delete/assetfile')->withTrashed();
Route::post(
'bulkedit',

View File

@@ -11,6 +11,10 @@ use Tabuna\Breadcrumbs\Trail;
Route::group(['prefix' => 'models', 'middleware' => ['auth']], function () {
Route::delete('{model}/showfile/{fileId}/delete',
[AssetModelsFilesController::class, 'destroy']
)->name('delete/modelfile')->withTrashed();
Route::post('{model}/upload',
[AssetModelsFilesController::class, 'store']
)->name('upload/models')->withTrashed();
@@ -19,9 +23,7 @@ Route::group(['prefix' => 'models', 'middleware' => ['auth']], function () {
[AssetModelsFilesController::class, 'show']
)->name('show/modelfile')->withTrashed();
Route::delete('{model}/showfile/{fileId}/delete',
[AssetModelsFilesController::class, 'destroy']
)->name('delete/modelfile')->withTrashed();
Route::get(
'{model}/clone',

View File

@@ -179,11 +179,22 @@ create_user () {
if [[ "$distro" == "Ubuntu" ]] || [[ "$distro" == "Debian" ]] || [[ "$distro" == "Raspbian" ]] ; then
/usr/sbin/adduser --quiet --disabled-password --gecos 'Snipe-IT User' "$APP_USER"
su -c "/usr/sbin/usermod -a -G "$apache_group" "$APP_USER""
else
adduser "$APP_USER"
usermod -a -G "$apache_group" "$APP_USER"
adduser -c "Snipe-IT User" "$APP_USER"
fi
# Add the user to the apache group so the app can write to any files apache
# creates (eg, if apache process creates the log, but then a an app-user-owned
# cron also tries writing
usermod -a -G "$apache_group" "$APP_USER"
# Now do the reverse -- so apache can write to the log that the user may
# have created. This was actively a problem on new installs, hobbling
# imports
# redefining these variables just for clarity
apache_user="$apache_group"
app_group="$APP_USER"
usermod -a -G "$app_group" "$apache_user"
}
run_as_app_user () {

View File

@@ -14,17 +14,17 @@ class AssetModelFilesTest extends TestCase
// Upload a file to a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
}
@@ -33,20 +33,55 @@ class AssetModelFilesTest extends TestCase
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk()
->assertJsonStructure([
'rows',
'total',
]);
}
public function testAssetModelFailsIfInvalidTypePassedInUrl()
{
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk()
->assertJsonStructure([
'rows',
'total',
]);
route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $model->id]))
->assertStatus(404);
}
public function testAssetModelFailsIfInvalidIdPassedInUrl()
{
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => 100000]))
->assertOk()
->assertStatusMessageIs('error');
}
public function testAssetModelApiDownloadsFile()
@@ -54,70 +89,68 @@ class AssetModelFilesTest extends TestCase
// Download a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file with notes
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
'notes' => 'manual'
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file with notes
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
'notes' => 'manual'
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk()
->assertJsonStructure([
'total',
'rows'=>[
'*' => [
'id',
'filename',
'url',
'created_by',
'created_at',
'updated_at',
'deleted_at',
'note',
'available_actions'
]
]
])
->assertJsonPath('rows.0.note','')
->assertJsonPath('rows.1.note','manual');
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk()
->assertJsonStructure([
'total',
'rows'=>[
'*' => [
'id',
'filename',
'url',
'created_by',
'created_at',
'deleted_at',
'note',
'available_actions'
]
]
])
->assertJsonPath('rows.0.note',null)
->assertJsonPath('rows.1.note','manual');
// Get the file
$this->actingAsForApi($user)
->get(
route('api.models.files.show', [
'model_id' => $model[0]["id"],
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk();
// Get the file
$this->actingAsForApi($user)
->get(
route('api.files.show', [
'object_type' => 'models',
'id' => $model->id,
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk();
}
public function testAssetModelApiDeletesFile()
@@ -125,30 +158,31 @@ class AssetModelFilesTest extends TestCase
// Delete a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk();
// Delete the file
$this->actingAsForApi($user)
// Delete the file
$this->actingAsForApi($user)
->delete(
route('api.models.files.destroy', [
'model_id' => $model[0]["id"],
route('api.files.destroy', [
'object_type' => 'models',
'id' => $model->id,
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk()

View File

@@ -14,7 +14,7 @@ class AssetFilesTest extends TestCase
// Upload a file to an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
@@ -22,7 +22,7 @@ class AssetFilesTest extends TestCase
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.assets.files.store', $asset), [
route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
@@ -33,14 +33,14 @@ class AssetFilesTest extends TestCase
// List all files on an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(route('api.assets.files.index', $asset))
->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk()
->assertJsonStructure([
'rows',
@@ -53,21 +53,21 @@ class AssetFilesTest extends TestCase
// Download a file from an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(route('api.assets.files.store', $asset), [
->post(route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(route('api.assets.files.index', $asset))
->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk();
}
@@ -76,7 +76,7 @@ class AssetFilesTest extends TestCase
// Delete a file from an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
@@ -84,7 +84,7 @@ class AssetFilesTest extends TestCase
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.assets.files.store', $asset), [
route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
@@ -92,7 +92,7 @@ class AssetFilesTest extends TestCase
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.assets.files.index', $asset))
route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk();
}

View File

@@ -32,6 +32,7 @@ class LoginTest extends TestCase
public function testLoginThrottleConfigIsRespected()
{
$this->markTestIncomplete("This test is flaky and needs to be fixed. Passes and fails seemingly at random.");
User::factory()->create(['username' => 'username_here']);
config(['auth.passwords.users.throttle.max_attempts' => 1]);
@@ -62,6 +63,7 @@ class LoginTest extends TestCase
public function testLogsSuccessfulLogin()
{
$this->markTestIncomplete("This test is flaky and needs to be fixed. Passes and fails seemingly at random.");
User::factory()->create(['username' => 'username_here']);
$this->withServerVariables(['REMOTE_ADDR' => '127.0.0.100'])

View File

@@ -3,6 +3,7 @@
namespace Tests\Feature\Categories\Api;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\User;
use Tests\Concerns\TestsPermissionsRequirement;
@@ -21,7 +22,7 @@ class DeleteCategoriesTest extends TestCase implements TestsPermissionsRequireme
$this->assertNotSoftDeleted($category);
}
public function testCannotDeleteCategoryThatStillHasAssociatedItems()
public function testCannotDeleteCategoryThatStillHasAssociatedAssets()
{
$asset = Asset::factory()->create();
$category = $asset->model->category;
@@ -33,6 +34,18 @@ class DeleteCategoriesTest extends TestCase implements TestsPermissionsRequireme
$this->assertNotSoftDeleted($category);
}
public function testCannotDeleteCategoryThatStillHasAssociatedModels()
{
$model = AssetModel::factory()->create();
$category = $model->category;
$this->actingAsForApi(User::factory()->deleteCategories()->create())
->deleteJson(route('api.categories.destroy', $category))
->assertStatusMessageIs('error');
$this->assertNotSoftDeleted($category);
}
public function testCanDeleteCategory()
{
$category = Category::factory()->create();

View File

@@ -0,0 +1,60 @@
<?php
namespace Tests\Feature\Assets\Ui;
use App\Events\CheckoutableCheckedIn;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\User;
use App\Models\Category;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class DeleteCategoriesTest extends TestCase
{
public function testPermissionNeededToDeleteCategory()
{
$this->actingAs(User::factory()->create())
->delete(route('categories.destroy', Category::factory()->create()))
->assertForbidden();
}
public function testCanDeleteCategory()
{
$category = Category::factory()->create();
$this->actingAs(User::factory()->deleteCategories()->create())
->delete(route('categories.destroy', $category))
->assertRedirectToRoute('categories.index')
->assertSessionHas('success');
$this->assertSoftDeleted($category);
}
public function testCannotDeleteCategoryThatStillHasAssociatedModels()
{
$model = AssetModel::factory()->create();
$category = $model->category;
$this->actingAs(User::factory()->deleteCategories()->create())
->delete(route('categories.destroy', $category))
->assertRedirectToRoute('categories.index')
->assertSessionHas('error');
$this->assertNotSoftDeleted($category);
}
public function testCannotDeleteCategoryThatStillHasAssociatedAssets()
{
$asset = Asset::factory()->create();
$category = $asset->model->category;
$this->actingAs(User::factory()->deleteCategories()->create())
->delete(route('categories.destroy', $category))
->assertRedirectToRoute('categories.index')
->assertSessionHas('error');
$this->assertNotSoftDeleted($category);
}
}

View File

@@ -128,7 +128,7 @@ mix.combine(
"./node_modules/ekko-lightbox/dist/ekko-lightbox.js",
"./resources/assets/js/extensions/pGenerator.jquery.js",
"./node_modules/chart.js/dist/Chart.js",
"./resources/assets/js/signature_pad.js", //dupe?
"./resources/assets/js/signature_pad.js", //dupe?
"./node_modules/jquery-validation/dist/jquery.validate.js",
"./node_modules/list.js/dist/list.js",
"./node_modules/clipboard/dist/clipboard.js",
@@ -149,6 +149,7 @@ mix
'./node_modules/bootstrap-table/dist/extensions/cookie/bootstrap-table-cookie.js',
'./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.js',
'./node_modules/bootstrap-table/dist/extensions/addrbar/bootstrap-table-addrbar.js',
'./node_modules/bootstrap-table/dist/extensions/custom-view/bootstrap-table-custom-view.js',
'./resources/assets/js/extensions/jquery.base64.js',
'./node_modules/tableexport.jquery.plugin/tableExport.min.js',
'./node_modules/tableexport.jquery.plugin/libs/jsPDF/jspdf.umd.min.js',