Compare commits

..

8 Commits

Author SHA1 Message Date
snipe
8a206e6c8e Beginning refactor
Signed-off-by: snipe <snipe@snipe.net>
2022-03-19 22:26:23 +00:00
snipe
73acf6e5e8 Squashed commit of the following:
commit d55c176199
Merge: 93ff9524d c0451fe17
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 18:35:42 2022 +0000

    Merge pull request #10831 from snipe/merges/master_down_to_develop_march_16

    v6.0.0-RC-5 - Merges master down to develop

commit c0451fe17a
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 18:10:17 2022 +0000

    Bumped version to v6.0.0-RC-5

    Signed-off-by: snipe <snipe@snipe.net>

commit 0f95802699
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 18:05:47 2022 +0000

    Fixed mis-automerge

    Signed-off-by: snipe <snipe@snipe.net>

commit 9db8bd782d
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 18:02:07 2022 +0000

    Merging master down into develop

    Signed-off-by: snipe <snipe@snipe.net>

commit 93ff9524d6
Merge: 40a947077 89ddbddad
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 10:04:58 2022 -0700

    Merge pull request #10829 from snipe/features/add_statuslabel_filter_by_type_in_api

    Added filter by status_type in StatusLabels API index endpoint

commit 89ddbddada
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 16:53:18 2022 +0000

    Fixed comment

    Signed-off-by: snipe <snipe@snipe.net>

commit 7498fe36e9
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 16:45:03 2022 +0000

    Removed extra space because pedantry

    Signed-off-by: snipe <snipe@snipe.net>

commit babf7c064b
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 16:38:45 2022 +0000

    Added ability to filter status label index endpoint by status type

    Signed-off-by: snipe <snipe@snipe.net>

commit 40a9470770
Merge: f499dcf7c 781b42601
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 08:54:10 2022 -0700

    Merge pull request #10828 from snipe/fixes/10826_parse_error_on_print_all

    Fixed #10826 - parse error on translation string for print all assets

commit 781b426018
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 08:50:47 2022 -0700

    Fixed #10826 - parse error on translation string for print all assets

    Signed-off-by: snipe <snipe@snipe.net>

commit f499dcf7c3
Merge: 84aa26dd5 54d6a4d97
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 08:25:29 2022 -0700

    Merge pull request #10827 from snipe/fixes/modal_dropdowns_broken

    Fixed #10825 - selectlists in modals b0rked

commit 54d6a4d978
Author: snipe <snipe@snipe.net>
Date:   Wed Mar 16 08:23:50 2022 -0700

    Fixed #10825 - selectlists in modals b0rked

    Signed-off-by: snipe <snipe@snipe.net>

commit 84aa26dd50
Merge: 8984b3a59 14bd07e97
Author: snipe <snipe@snipe.net>
Date:   Thu Mar 10 13:02:45 2022 -0800

    Merge pull request #10728 from Godmartinz/gh10191-css-dropdownmenu

    Fixed #10191 - font color contrast on mobile menu

commit 8984b3a59d
Merge: d06ef4bde cdc0805fc
Author: snipe <snipe@snipe.net>
Date:   Thu Mar 10 10:40:33 2022 -0800

    Merge pull request #10812 from inietov/fixes/CarbonExceptions_InvalidFormatException_DateTime_develop

    Fixes Carbon\Exceptions\InvalidFormatException: DateTime::__construct(): Failed to parse time string develop

commit cdc0805fc4
Author: Ivan Nieto Vivanco <inietov@gmail.com>
Date:   Thu Mar 10 12:07:07 2022 -0600

    Sanitize dates input in the importer before saving

commit 14bd07e97d
Merge: 4435ea95f 16fd10954
Author: Godfrey M <godmartinz@gmail.com>
Date:   Thu Mar 3 10:14:30 2022 -0800

    gerge branchevelop' into gh10191-css-dropdownmenu

commit 4435ea95fd
Author: Godfrey M <godmartinz@gmail.com>
Date:   Thu Mar 3 09:47:11 2022 -0800

    removes changes to mix-manifest

commit f115385243
Author: Godfrey M <godmartinz@gmail.com>
Date:   Thu Mar 3 09:32:05 2022 -0800

    C&P mix-manifest to be tracked

commit 708dc08b4f
Author: Godfrey M <godmartinz@gmail.com>
Date:   Wed Feb 23 11:17:44 2022 -0800

    mixes color contrast on dropdown menu from navbar
    on file

Signed-off-by: snipe <snipe@snipe.net>
2022-03-16 20:31:14 +00:00
snipe
9ae2d17579 Minor changes
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 20:06:20 -08:00
snipe
3404b63922 Removed stray echo
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 18:12:23 -08:00
snipe
b1796e6ff2 Make the box header colors different if they are superadmin or admin permisions
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 18:10:54 -08:00
snipe
471527dfc6 Removed unneeded title
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 18:09:57 -08:00
snipe
12c24d7c0f Some jquery fixes
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 17:40:05 -08:00
snipe
5651d060d8 Initial rework of UI for groups
Signed-off-by: snipe <snipe@snipe.net>
2022-03-09 17:10:42 -08:00
8274 changed files with 254787 additions and 526324 deletions

View File

@@ -1,14 +1,11 @@
{
"projectName": "snipe-it",
"projectOwner": "snipe",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"CONTRIBUTORS.md"
"README.md"
],
"imageSize": 110,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "snipe",
@@ -2588,554 +2585,6 @@
"contributions": [
"code"
]
},
{
"login": "QveenSi",
"name": "Yevhenii Huzii",
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
"profile": "https://github.com/QveenSi",
"contributions": [
"code"
]
},
{
"login": "veenone",
"name": "Achmad Fienan Rahardianto",
"avatar_url": "https://avatars.githubusercontent.com/u/3839381?v=4",
"profile": "https://github.com/veenone",
"contributions": [
"code"
]
},
{
"login": "QveenSi",
"name": "Yevhenii Huzii",
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
"profile": "https://github.com/QveenSi",
"contributions": [
"code"
]
},
{
"login": "chrisweirich",
"name": "Christian Weirich",
"avatar_url": "https://avatars.githubusercontent.com/u/97299851?v=4",
"profile": "https://github.com/chrisweirich",
"contributions": [
"code"
]
},
{
"login": "denzfarid",
"name": "denzfarid",
"avatar_url": "https://avatars.githubusercontent.com/u/1294403?v=4",
"profile": "https://github.com/denzfarid",
"contributions": []
},
{
"login": "ntbutler-nbcs",
"name": "ntbutler-nbcs",
"avatar_url": "https://avatars.githubusercontent.com/u/94018771?v=4",
"profile": "https://github.com/ntbutler-nbcs",
"contributions": [
"code"
]
},
{
"login": "naveensrinivasan",
"name": "Naveen",
"avatar_url": "https://avatars.githubusercontent.com/u/172697?v=4",
"profile": "https://naveensrinivasan.dev",
"contributions": [
"code"
]
},
{
"login": "mikeroq",
"name": "Mike Roquemore",
"avatar_url": "https://avatars.githubusercontent.com/u/55674383?v=4",
"profile": "https://github.com/mikeroq",
"contributions": [
"code"
]
},
{
"login": "reederda",
"name": "Daniel Reeder",
"avatar_url": "https://avatars.githubusercontent.com/u/7991086?v=4",
"profile": "https://github.com/reederda",
"contributions": [
"translation",
"translation",
"code"
]
},
{
"login": "vickyjaura183",
"name": "vickyjaura183",
"avatar_url": "https://avatars.githubusercontent.com/u/109422491?v=4",
"profile": "https://github.com/vickyjaura183",
"contributions": [
"code"
]
},
{
"login": "julian-piehl",
"name": "Peace",
"avatar_url": "https://avatars.githubusercontent.com/u/32363424?v=4",
"profile": "https://github.com/julian-piehl",
"contributions": [
"code"
]
},
{
"login": "kylegordon",
"name": "Kyle Gordon",
"avatar_url": "https://avatars.githubusercontent.com/u/231528?v=4",
"profile": "https://github.com/kylegordon",
"contributions": [
"code"
]
},
{
"login": "sunflowerbofh",
"name": "Katharina Drexel",
"avatar_url": "https://avatars.githubusercontent.com/u/53009155?v=4",
"profile": "http://www.bfh.ch",
"contributions": [
"code"
]
},
{
"login": "dsferruzza",
"name": "David Sferruzza",
"avatar_url": "https://avatars.githubusercontent.com/u/1931963?v=4",
"profile": "https://david.sferruzza.fr/",
"contributions": [
"code"
]
},
{
"login": "rnelsonee",
"name": "Rick Nelson",
"avatar_url": "https://avatars.githubusercontent.com/u/19511639?v=4",
"profile": "https://github.com/rnelsonee",
"contributions": [
"code"
]
},
{
"login": "BasO12",
"name": "BasO12",
"avatar_url": "https://avatars.githubusercontent.com/u/94169344?v=4",
"profile": "https://github.com/BasO12",
"contributions": [
"code"
]
},
{
"login": "Vautia",
"name": "Vautia",
"avatar_url": "https://avatars.githubusercontent.com/u/111710123?v=4",
"profile": "https://github.com/Vautia",
"contributions": [
"code"
]
},
{
"login": "chartjes",
"name": "Chris Hartjes",
"avatar_url": "https://avatars.githubusercontent.com/u/28321?v=4",
"profile": "http://www.littlehart.net/atthekeyboard",
"contributions": [
"code"
]
},
{
"login": "geo-chen",
"name": "geo-chen",
"avatar_url": "https://avatars.githubusercontent.com/u/2404584?v=4",
"profile": "https://github.com/geo-chen",
"contributions": [
"code"
]
},
{
"login": "nh314",
"name": "Phan Nguyen",
"avatar_url": "https://avatars.githubusercontent.com/u/6006620?v=4",
"profile": "https://github.com/nh314",
"contributions": [
"code"
]
},
{
"login": "StarlessNights",
"name": "Iisakki Jaakkola",
"avatar_url": "https://avatars.githubusercontent.com/u/115993812?v=4",
"profile": "https://github.com/StarlessNights",
"contributions": [
"code"
]
},
{
"login": "eltociear",
"name": "Ikko Ashimine",
"avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4",
"profile": "https://bandism.net/",
"contributions": [
"code"
]
},
{
"login": "lukasfehling",
"name": "Lukas Fehling",
"avatar_url": "https://avatars.githubusercontent.com/u/56871540?v=4",
"profile": "https://github.com/lukasfehling",
"contributions": [
"code"
]
},
{
"login": "fernando-almeida",
"name": "Fernando Almeida",
"avatar_url": "https://avatars.githubusercontent.com/u/1975990?v=4",
"profile": "https://github.com/fernando-almeida",
"contributions": [
"code"
]
},
{
"login": "akemidx",
"name": "akemidx",
"avatar_url": "https://avatars.githubusercontent.com/u/116301219?v=4",
"profile": "https://github.com/akemidx",
"contributions": [
"code"
]
},
{
"login": "oguzbilgic",
"name": "Oguz Bilgic",
"avatar_url": "https://avatars.githubusercontent.com/u/144778?v=4",
"profile": "http://oguz.site",
"contributions": [
"code"
]
},
{
"login": "scoo73r",
"name": "Scooter Crawford",
"avatar_url": "https://avatars.githubusercontent.com/u/9262438?v=4",
"profile": "https://github.com/scoo73r",
"contributions": [
"code"
]
},
{
"login": "subdriven",
"name": "subdriven",
"avatar_url": "https://avatars.githubusercontent.com/u/5957345?v=4",
"profile": "https://github.com/subdriven",
"contributions": [
"code"
]
},
{
"login": "AndrewSav",
"name": "Andrew Savinykh",
"avatar_url": "https://avatars.githubusercontent.com/u/658865?v=4",
"profile": "https://github.com/AndrewSav",
"contributions": [
"code"
]
},
{
"login": "kenchan0130",
"name": "Tadayuki Onishi",
"avatar_url": "https://avatars.githubusercontent.com/u/1155067?v=4",
"profile": "https://kenchan0130.github.io",
"contributions": [
"code"
]
},
{
"login": "floschoepfer",
"name": "Florian",
"avatar_url": "https://avatars.githubusercontent.com/u/112496896?v=4",
"profile": "https://github.com/floschoepfer",
"contributions": [
"code"
]
},
{
"login": "spencerrlongg",
"name": "Spencer Long",
"avatar_url": "https://avatars.githubusercontent.com/u/7305753?v=4",
"profile": "http://spencerlong.com",
"contributions": [
"code"
]
},
{
"login": "marcusmoore",
"name": "Marcus Moore",
"avatar_url": "https://avatars.githubusercontent.com/u/1141514?v=4",
"profile": "https://github.com/marcusmoore",
"contributions": [
"code"
]
},
{
"login": "Mezzle",
"name": "Martin Meredith",
"avatar_url": "https://avatars.githubusercontent.com/u/570639?v=4",
"profile": "https://github.com/Mezzle",
"contributions": []
},
{
"login": "dboth",
"name": "dboth",
"avatar_url": "https://avatars.githubusercontent.com/u/5731963?v=4",
"profile": "http://dboth.de",
"contributions": [
"code"
]
},
{
"login": "zacharyfleck",
"name": "Zachary Fleck",
"avatar_url": "https://avatars.githubusercontent.com/u/87536651?v=4",
"profile": "https://github.com/zacharyfleck",
"contributions": [
"code"
]
},
{
"login": "vikaas-cyper",
"name": "VIKAAS-A",
"avatar_url": "https://avatars.githubusercontent.com/u/74609912?v=4",
"profile": "https://github.com/vikaas-cyper",
"contributions": [
"code"
]
},
{
"login": "ak-piracha",
"name": "Abdul Kareem",
"avatar_url": "https://avatars.githubusercontent.com/u/88882041?v=4",
"profile": "https://github.com/ak-piracha",
"contributions": [
"code"
]
},
{
"login": "NojoudAlshehri",
"name": "NojoudAlshehri",
"avatar_url": "https://avatars.githubusercontent.com/u/111287779?v=4",
"profile": "https://github.com/NojoudAlshehri",
"contributions": [
"code"
]
},
{
"login": "stefanstidlffg",
"name": "Stefan Stidl",
"avatar_url": "https://avatars.githubusercontent.com/u/54367449?v=4",
"profile": "https://github.com/stefanstidlffg",
"contributions": [
"code"
]
},
{
"login": "qay21",
"name": "Quentin Aymard",
"avatar_url": "https://avatars.githubusercontent.com/u/87803479?v=4",
"profile": "https://github.com/qay21",
"contributions": [
"code"
]
},
{
"login": "cram42",
"name": "Grant Le Roux",
"avatar_url": "https://avatars.githubusercontent.com/u/5396871?v=4",
"profile": "https://github.com/cram42",
"contributions": [
"code"
]
},
{
"login": "Singrity",
"name": "Bogdan",
"avatar_url": "https://avatars.githubusercontent.com/u/58479551?v=4",
"profile": "http://@singrity",
"contributions": [
"code"
]
},
{
"login": "mmanjos",
"name": "mmanjos",
"avatar_url": "https://avatars.githubusercontent.com/u/3483684?v=4",
"profile": "https://github.com/mmanjos",
"contributions": [
"code"
]
},
{
"login": "Azooz2014",
"name": "Abdelaziz Faki",
"avatar_url": "https://avatars.githubusercontent.com/u/7429229?v=4",
"profile": "https://azooz2014.github.io/",
"contributions": [
"code"
]
},
{
"login": "bilias",
"name": "bilias",
"avatar_url": "https://avatars.githubusercontent.com/u/47315739?v=4",
"profile": "https://github.com/bilias",
"contributions": [
"code"
]
},
{
"login": "coach1988",
"name": "coach1988",
"avatar_url": "https://avatars.githubusercontent.com/u/2565989?v=4",
"profile": "https://github.com/coach1988",
"contributions": [
"code"
]
},
{
"login": "mauro-miatello",
"name": "MrM",
"avatar_url": "https://avatars.githubusercontent.com/u/11910225?v=4",
"profile": "https://github.com/mauro-miatello",
"contributions": [
"code"
]
},
{
"login": "koiakoia",
"name": "koiakoia",
"avatar_url": "https://avatars.githubusercontent.com/u/60405354?v=4",
"profile": "https://github.com/koiakoia",
"contributions": [
"code"
]
},
{
"login": "mustafa-online",
"name": "Mustafa Online",
"avatar_url": "https://avatars.githubusercontent.com/u/5323832?v=4",
"profile": "https://github.com/mustafa-online",
"contributions": [
"code"
]
},
{
"login": "franceslui",
"name": "franceslui",
"avatar_url": "https://avatars.githubusercontent.com/u/104601439?v=4",
"profile": "https://github.com/franceslui",
"contributions": [
"code"
]
},
{
"login": "Q4kK",
"name": "Q4kK",
"avatar_url": "https://avatars.githubusercontent.com/u/125313163?v=4",
"profile": "https://github.com/Q4kK",
"contributions": [
"code"
]
},
{
"login": "squintfox",
"name": "squintfox",
"avatar_url": "https://avatars.githubusercontent.com/u/55590532?v=4",
"profile": "https://github.com/squintfox",
"contributions": [
"code"
]
},
{
"login": "jeffclay",
"name": "Jeff Clay",
"avatar_url": "https://avatars.githubusercontent.com/u/1380084?v=4",
"profile": "https://github.com/jeffclay",
"contributions": [
"code"
]
},
{
"login": "PP-JN-RL",
"name": "Phil J R",
"avatar_url": "https://avatars.githubusercontent.com/u/52716446?v=4",
"profile": "https://github.com/PP-JN-RL",
"contributions": [
"code"
]
},
{
"login": "chandanchowdhury",
"name": "i_virus",
"avatar_url": "https://avatars.githubusercontent.com/u/1496725?v=4",
"profile": "https://www.corelight.com/",
"contributions": [
"code"
]
},
{
"login": "gitgrimbo",
"name": "Paul Grime",
"avatar_url": "https://avatars.githubusercontent.com/u/1020541?v=4",
"profile": "https://github.com/gitgrimbo",
"contributions": [
"code"
]
},
{
"login": "LeePorte",
"name": "Lee Porte",
"avatar_url": "https://avatars.githubusercontent.com/u/922815?v=4",
"profile": "https://leeporte.co.uk",
"contributions": [
"code"
]
},
{
"login": "bryanlopezinc",
"name": "BRYAN ",
"avatar_url": "https://avatars.githubusercontent.com/u/23613427?v=4",
"profile": "https://github.com/bryanlopezinc",
"contributions": [
"code",
"test"
]
},
{
"login": "U-H-T",
"name": "U-H-T",
"avatar_url": "https://avatars.githubusercontent.com/u/64061710?v=4",
"profile": "https://github.com/U-H-T",
"contributions": [
"code"
]
},
{
"login": "Tyree",
"name": "Matt Tyree",
"avatar_url": "https://avatars.githubusercontent.com/u/5395363?v=4",
"profile": "https://github.com/Tyree",
"contributions": [
"doc"
]
}
]
}

View File

@@ -1,166 +0,0 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=develop
APP_DEBUG=false
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=mariadb
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_IS_PAAS=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: PUBLIC S3 Settings
# --------------------------------------------
PUBLIC_AWS_SECRET_ACCESS_KEY=null
PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: PRIVATE S3 Settings
# --------------------------------------------
PRIVATE_AWS_ACCESS_KEY_ID=null
PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600

View File

@@ -1,18 +1,18 @@
# --------------------------------------------
# REQUIRED: DOCKER SPECIFIC SETTINGS
# REQUIRED: DB SETUP
# --------------------------------------------
APP_VERSION=v6.4.1
APP_PORT=8000
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=production
APP_ENV=develop
APP_DEBUG=false
# Please regenerate the APP_KEY value by calling `docker compose run --rm snipeit php artisan key:generate --show`. Copy paste the value here
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
@@ -27,12 +27,10 @@ PUBLIC_FILESYSTEM_DISK=local_public
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT='3306'
DB_HOST=mariadb
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
@@ -47,35 +45,29 @@ DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=smtp
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_TLS_VERIFY_PEER=true
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: DATA PROTECTION
# --------------------------------------------
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
@@ -83,6 +75,7 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
@@ -97,7 +90,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.0.0.0/8
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
@@ -115,7 +108,7 @@ CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=null
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
@@ -145,13 +138,6 @@ PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
@@ -162,11 +148,10 @@ RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOG=stderr
APP_LOG_MAX_FILES=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600

View File

@@ -1,107 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=local
APP_DEBUG=false
APP_KEY=base64:hTUIUh9CP6dQx+6EjSlfWTgbaMaaRvlpEwk45vp+xmk=
APP_URL=http://127.0.0.1:8000
APP_TIMEZONE='US/Eastern'
APP_LOCALE=en
APP_LOCKED=false
MAX_RESULTS=200
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
DB_PREFIX=null
#DB_DUMP_PATH=
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER="log"
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=true
COOKIE_NAME=snipeit_v5_local
SECURE_COOKIES=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
REFERRER_POLICY=same-origin
ENABLE_CSP=true
CORS_ALLOWED_ORIGINS="*"
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=50000
LOGIN_LOCKOUT_DURATION=1000
RESET_PASSWORD_LINK_EXPIRES=15
# --------------------------------------------
# OPTIONAL: API
# --------------------------------------------
API_MAX_REQUESTS_PER_HOUR=200
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
DISABLE_NOSAML_LOCAL_LOGIN=true
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_LEVEL=debug
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=null
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=true
ENABLE_HSTS=false
WARN_DEBUG=false
APP_CIPHER=AES-256-CBC

105
.env.dusk.local Normal file
View File

@@ -0,0 +1,105 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=local
APP_DEBUG=false
APP_KEY=base64:hTUIUh9CP6dQx+6EjSlfWTgbaMaaRvlpEwk45vp+xmk=
APP_URL=http://127.0.0.1:8000
APP_TIMEZONE='US/Eastern'
APP_LOCALE=en
APP_LOCKED=false
MAX_RESULTS=200
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=snipeit-local
DB_USERNAME=snipeit-local
DB_PASSWORD=snipeit-local
DB_PREFIX=null
DB_DUMP_PATH='/Applications/MAMP/Library/bin'
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_DRIVER="log"
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=true
COOKIE_NAME=snipeit_v5_local
SECURE_COOKIES=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
REFERRER_POLICY=same-origin
ENABLE_CSP=true
CORS_ALLOWED_ORIGINS="*"
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=50000
LOGIN_LOCKOUT_DURATION=1000
RESET_PASSWORD_LINK_EXPIRES=15
# --------------------------------------------
# OPTIONAL: API
# --------------------------------------------
API_MAX_REQUESTS_PER_HOUR=200
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
DISABLE_NOSAML_LOCAL_LOGIN=true
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
APP_LOG=single
LOG_LEVEL=debug
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=null
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=true
ENABLE_HSTS=false
WARN_DEBUG=false
APP_CIPHER=AES-256-CBC

View File

@@ -6,7 +6,7 @@ APP_DEBUG=false
APP_KEY=ChangeMe
APP_URL=null
APP_TIMEZONE='UTC'
APP_LOCALE='en-US'
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
@@ -24,7 +24,6 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
@@ -42,26 +41,21 @@ DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=smtp
MAIL_DRIVER=smtp
MAIL_HOST=email-smtp.us-west-2.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=YOURUSERNAME
MAIL_PASSWORD=YOURPASSWORD
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
MAIL_TLS_VERIFY_PEER=true
# MAIL_ENCRYPTION is no longer supported. SymfonyMailer will use tls if it's
# advertised, and won't if it's not. If you want to use your mail server's IP but it's failing
# because of certificate errors, set MAIL_TLS_VERIFY_PEER-true
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
@@ -76,13 +70,11 @@ IMAGE_LIB=gd
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_DRIVER=file
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
@@ -90,8 +82,6 @@ COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=cookieStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
@@ -100,7 +90,6 @@ APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
ADDITIONAL_CSP_URLS=null
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
@@ -108,6 +97,7 @@ ENABLE_HSTS=false
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
@@ -144,32 +134,18 @@ PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
LOGIN_AUTOCOMPLETE=false
# --------------------------------------------
# OPTIONAL: FORGOTTEN PASSWORD SETTINGS
# --------------------------------------------
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_MAX_DAYS=10
APP_LOG=single
APP_LOG_MAX_FILES=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
@@ -182,19 +158,3 @@ IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000
REQUIRE_SAML=false
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
# --------------------------------------------
# OPTIONAL: HASHING
# --------------------------------------------
HASHING_DRIVER='bcrypt'
BCRYPT_ROUNDS=10
ARGON_MEMORY=1024
ARGON_THREADS=2
ARGON_TIME=2
# --------------------------------------------
# OPTIONAL: SCIM
# --------------------------------------------
SCIM_TRACE=false
SCIM_STANDARDS_COMPLIANCE=false

74
.env.testing Normal file
View File

@@ -0,0 +1,74 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='US/Pacific'
APP_LOCALE=en
FILESYSTEM_DISK=local
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=sqlite_testing
DB_HOST=localhost
DB_DATABASE=testing.sqlite
DB_USERNAME=null
DB_PASSWORD=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_DRIVER=log
MAIL_HOST=email-smtp.us-west-2.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=YOURUSERNAME
MAIL_PASSWORD=YOURPASSWORD
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: AWS S3 SETTINGS
# --------------------------------------------
AWS_SECRET_ACCESS_KEY=null
AWS_ACCESS_KEY_ID=null
AWS_DEFAULT_REGION=null
AWS_BUCKET=null
AWS_BUCKET_ROOT=null
AWS_URL=null
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeittest_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
# --------------------------------------------
# OPTIONAL: APP LOG FORMAT
# --------------------------------------------
APP_LOG=single
APP_LOG_LEVEL=debug

View File

@@ -6,7 +6,7 @@ APP_DEBUG=false
APP_KEY='base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU='
APP_URL='http://localhost:8000'
APP_TIMEZONE='US/Pacific'
APP_LOCALE='en-US'
APP_LOCALE=en
FILESYSTEM_DISK=local
# --------------------------------------------
@@ -14,7 +14,6 @@ FILESYSTEM_DISK=local
# --------------------------------------------
DB_CONNECTION=sqlite
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE='sqlite_testing'
DB_USERNAME=root
DB_PASSWORD=null
@@ -22,7 +21,7 @@ DB_PASSWORD=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=log
MAIL_DRIVER=log
# --------------------------------------------
@@ -35,4 +34,4 @@ IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: APP LOG FORMAT
# --------------------------------------------
LOG_CHANNEL=single
APP_LOG=single

View File

@@ -1,19 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE='en-US'
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null

View File

@@ -4,7 +4,6 @@ APP_URL=http://snipe-it.localapp
DB_CONNECTION=mysql
DB_DEFAULT=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=snipeittests
DB_USERNAME=snipeit
DB_PASSWORD=snipe
@@ -18,6 +17,6 @@ APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
LOGIN_MAX_ATTEMPTS=1000000
LOGIN_LOCKOUT_DURATION=100000000
MAIL_MAILER=log
MAIL_DRIVER=log
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT

View File

@@ -4,7 +4,6 @@ APP_URL=http://snipe-it.localapp
DB_CONNECTION=sqlite_testing
DB_DEFAULT=sqlite_testing
DB_HOST=localhost
DB_PORT=3306
APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
@@ -15,6 +14,6 @@ APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
LOGIN_MAX_ATTEMPTS=1000000
LOGIN_LOCKOUT_DURATION=100000000
MAIL_MAILER=log
MAIL_DRIVER=log
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT

2
.github/CODEOWNERS vendored
View File

@@ -15,8 +15,8 @@
# *.js @octocat @github/js
app/Importer/* @dmeltzer
app/Http/Controllers/CustomFields* @uberbrady
app/Http/Controllers/Api/CustomFields* @uberbrady
resources/views/custom_fields/* @uberbrady
docker/* @uberbrady
app/Providers/SamlServiceProvider.php @uberbrady

View File

@@ -1,25 +1,42 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature Request]: "
labels: ["feature request"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. The more information you can provide about your use-case, the more liklely we are to consider your feature.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
name: Feature Request
description: Suggest an idea for this project
body:
- type: input
attributes:
label: Snipe-IT Version
validations:
required: true
- type: input
id: server_operatingSystem
attributes:
label: Operating System
description: 'e.g. Ubuntu, Windows'
validations:
required: true
- type: input
attributes:
label: Web Server
description: 'e.g. Apache, IIS'
validations:
required: true
- type: input
attributes:
label: PHP Version
validations:
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Additional context Add any other context or screenshots about the feature request here.

View File

@@ -1,22 +1,4 @@
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*"]
skins: ["*.js", "*.css", "*.scss", "*.less"]
css: ["*.css","*.scss", "*.less"]
javascript: ["*.js", "package.json", "package.lock"]
backend: ["/app/*", "composer.json", "composer.lock"]
translations: ["/resources/lang"]
livewire: ["/app/Http/Livewire/*", "resources/views/livewire/*"]
backups: ["*backup*"]
restore: ["*restore*"]
saml: ["*saml*"]
scim: ["*scim*"]
custom fields: ["*fields*", "*fieldsets*"]
dependencies: ["composer.json", "composer.lock", "package.json", "package.lock"]
consumables: ["*consumables*"]
api: ["/app/Http/Controllers/Api/*"]
notifications: ["/app/Notifications/*"]
importer: ["/app/Importer/*","/app/Http/Livewire/Importer.php", "resources/views/livewire/importer.php"]
cli / artisan: ["/app/Console/*"]
LDAP: ["*Ldap*", "/app/Console/Commands/Ldap*","/app/Models/Ldap.php"]
docker: ["*docker/*", "Dockerfile", "Dockerfile.alpine", "Dockerfile.fpm-alpine", ".dockerignore", ".env.docker"]
tests: ["/tests/*", "/database/factories/*", "/stubs"]
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*"]
backend: ["/app", "*.php"]
legal: ["LICENSE*", "NOTICES*"]
config: .github

View File

@@ -1,7 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"

View File

@@ -1,39 +0,0 @@
# This workflow checks out code, performs a CodeQL analysis (for JavaScript) and integrates the results
# with the GitHub Advanced Security code scanning feature.
# More information: https://codeql.github.com/
name: CodeQL Security Scan
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# schedule:
# - cron: '15 17 * * 1'
jobs:
analyze:
name: CodeQL Security Scan
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -17,26 +17,18 @@ on:
schedule:
- cron: '36 23 * * 3'
permissions:
contents: read
jobs:
codacy-security-scan:
# Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v2
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.1
uses: codacy/codacy-analysis-cli-action@1.1.0
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
@@ -52,6 +44,6 @@ jobs:
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: results.sarif

View File

@@ -1,21 +0,0 @@
name: Crowdin Action
on:
push:
branches: [ develop ]
jobs:
upload-sources-to-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin push
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -15,9 +15,6 @@ on:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
@@ -32,7 +29,6 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=tag,suffix=-alpine
type=semver,pattern=v{{major}}-latest-alpine
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
@@ -42,17 +38,17 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
uses: actions/checkout@v2
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -64,7 +60,7 @@ jobs:
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
uses: docker/metadata-action@v3
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
@@ -73,11 +69,11 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -15,9 +15,6 @@ on:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
@@ -32,7 +29,6 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
@@ -42,17 +38,17 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
uses: actions/checkout@v2
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -64,7 +60,7 @@ jobs:
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
uses: docker/metadata-action@v3
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
@@ -73,11 +69,11 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -1,22 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- master
- develop
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Description
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
repository: snipe/snipe-it
readme-filepath: ./README.md

View File

@@ -1,79 +0,0 @@
name: Tests in MySQL
on:
push:
branches:
- master
- develop
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: snipeit
ports:
- 33306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
php-version:
- "8.1"
- "8.2"
- "8.3"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Laravel
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: php artisan test --parallel

View File

@@ -1,75 +0,0 @@
name: Tests in Postgres
on: workflow_dispatch
jobs:
tests:
runs-on: ubuntu-latest
services:
postgresql:
image: postgres
env:
POSTGRES_DB: snipeit
POSTGRES_USER: snipeit
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
php-version:
- "8.2"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Laravel
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: php artisan test --parallel

View File

@@ -1,61 +0,0 @@
name: Tests in SQLite
on:
push:
branches:
- master
- develop
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version:
- "8.1.1"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Setup Passport
run: php artisan passport:keys
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite_testing
run: php artisan test --parallel

2
.gitignore vendored
View File

@@ -1,8 +1,6 @@
.couscous
.DS_Store
.env
.env.testing
phpstan.neon
.idea
/bin/
/bootstrap/compiled.php

2
.nvmrc
View File

@@ -1 +1 @@
v18.16.0
v12.22.1

View File

@@ -1,10 +0,0 @@
{
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
"DOC4": "You should really just ignore it and run upgrade.php. Really",
"php_min_version": "8.1.0",
"php_max_major_minor": "8.3",
"php_max_wontwork": "8.4.0",
"current_snipeit_version": "7.0"
}

View File

@@ -1,461 +0,0 @@
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.snipe.net"><img src="https://avatars3.githubusercontent.com/u/197404?v=3?s=110" width="110px;" alt="snipe"/><br /><sub><b>snipe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Code">💻</a> <a href="#infra-snipe" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Asnipe" title="Bug reports">🐛</a> <a href="#design-snipe" title="Design">🎨</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Asnipe" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.uberbrady.com"><img src="https://avatars0.githubusercontent.com/u/36335?v=3?s=110" width="110px;" alt="Brady Wetherington"/><br /><sub><b>Brady Wetherington</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Documentation">📖</a> <a href="#infra-uberbrady" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Auberbrady" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmeltzer"><img src="https://avatars0.githubusercontent.com/u/3803132?v=3?s=110" width="110px;" alt="Daniel Meltzer"/><br /><sub><b>Daniel Meltzer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.tuckertechonline.com"><img src="https://avatars0.githubusercontent.com/u/1609106?v=3?s=110" width="110px;" alt="Michael T"/><br /><sub><b>Michael T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mtucker6784" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madd15"><img src="https://avatars2.githubusercontent.com/u/3274937?v=3?s=110" width="110px;" alt="madd15"/><br /><sub><b>madd15</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=madd15" title="Documentation">📖</a> <a href="#question-madd15" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vsposato"><img src="https://avatars2.githubusercontent.com/u/894126?v=3?s=110" width="110px;" alt="Vincent Sposato"/><br /><sub><b>Vincent Sposato</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vsposato" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vjandrea"><img src="https://avatars0.githubusercontent.com/u/1639757?v=3?s=110" width="110px;" alt="Andrea Bergamasco"/><br /><sub><b>Andrea Bergamasco</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vjandrea" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kpawelski"><img src="https://avatars0.githubusercontent.com/u/10640152?v=3?s=110" width="110px;" alt="Karol"/><br /><sub><b>Karol</b></sub></a><br /><a href="#translation-kpawelski" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=kpawelski" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.morph027.de/"><img src="https://avatars3.githubusercontent.com/u/600106?v=3?s=110" width="110px;" alt="morph027"/><br /><sub><b>morph027</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=morph027" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fvleminckx"><img src="https://avatars3.githubusercontent.com/u/22935755?v=3?s=110" width="110px;" alt="fvleminckx"/><br /><sub><b>fvleminckx</b></sub></a><br /><a href="#infra-fvleminckx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itsupportcmsukorg"><img src="https://avatars2.githubusercontent.com/u/15633547?v=3?s=110" width="110px;" alt="itsupportcmsukorg"/><br /><sub><b>itsupportcmsukorg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://override.io"><img src="https://avatars3.githubusercontent.com/u/12373799?v=3?s=110" width="110px;" alt="Frank"/><br /><sub><b>Frank</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=base-zero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghost"><img src="https://avatars0.githubusercontent.com/u/10137?v=3?s=110" width="110px;" alt="Deleted user"/><br /><sub><b>Deleted user</b></sub></a><br /><a href="#translation-ghost" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=ghost" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tiagom62"><img src="https://avatars1.githubusercontent.com/u/10802313?v=3?s=110" width="110px;" alt="tiagom62"/><br /><sub><b>tiagom62</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tiagom62" title="Code">💻</a> <a href="#infra-tiagom62" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rystaf"><img src="https://avatars3.githubusercontent.com/u/2389047?v=3?s=110" width="110px;" alt="Ryan Stafford"/><br /><sub><b>Ryan Stafford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rystaf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehanlon"><img src="https://avatars2.githubusercontent.com/u/10345935?v=3?s=110" width="110px;" alt="Eammon Hanlon"/><br /><sub><b>Eammon Hanlon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ehanlon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zjean"><img src="https://avatars0.githubusercontent.com/u/441924?v=3?s=110" width="110px;" alt="zjean"/><br /><sub><b>zjean</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zjean" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.frei.media"><img src="https://avatars0.githubusercontent.com/u/12660103?v=3?s=110" width="110px;" alt="Matthias Frei"/><br /><sub><b>Matthias Frei</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FREImedia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/opsydev"><img src="https://avatars0.githubusercontent.com/u/3767518?v=3?s=110" width="110px;" alt="opsydev"/><br /><sub><b>opsydev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=opsydev" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.ddreier.com"><img src="https://avatars1.githubusercontent.com/u/82290?v=3?s=110" width="110px;" alt="Daniel Dreier"/><br /><sub><b>Daniel Dreier</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ddreier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://rassie.org"><img src="https://avatars0.githubusercontent.com/u/23448?v=3?s=110" width="110px;" alt="Nikolai Prokoschenko"/><br /><sub><b>Nikolai Prokoschenko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rassie" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YetAnotherCodeMonkey"><img src="https://avatars0.githubusercontent.com/u/13452757?v=3?s=110" width="110px;" alt="Drew"/><br /><sub><b>Drew</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/merid14"><img src="https://avatars0.githubusercontent.com/u/1342320?v=3?s=110" width="110px;" alt="Walter"/><br /><sub><b>Walter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=merid14" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/balous"><img src="https://avatars3.githubusercontent.com/u/11254614?v=3?s=110" width="110px;" alt="Petr Baloun"/><br /><sub><b>Petr Baloun</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balous" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reidblomquist"><img src="https://avatars0.githubusercontent.com/u/6117660?v=3?s=110" width="110px;" alt="reidblomquist"/><br /><sub><b>reidblomquist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reidblomquist" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mathieuk"><img src="https://avatars0.githubusercontent.com/u/539914?v=3?s=110" width="110px;" alt="Mathieu Kooiman"/><br /><sub><b>Mathieu Kooiman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mathieuk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csayre"><img src="https://avatars3.githubusercontent.com/u/6606421?v=3?s=110" width="110px;" alt="csayre"/><br /><sub><b>csayre</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=csayre" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adamdunson"><img src="https://avatars1.githubusercontent.com/u/768488?v=3?s=110" width="110px;" alt="Adam Dunson"/><br /><sub><b>Adam Dunson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamdunson" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thehereward"><img src="https://avatars0.githubusercontent.com/u/5547470?v=3?s=110" width="110px;" alt="Hereward"/><br /><sub><b>Hereward</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thehereward" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swoopdk"><img src="https://avatars0.githubusercontent.com/u/5802977?v=3?s=110" width="110px;" alt="swoopdk"/><br /><sub><b>swoopdk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=swoopdk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linkedin.com/in/ahimta"><img src="https://avatars1.githubusercontent.com/u/3470403?v=3?s=110" width="110px;" alt="Abdullah Alansari"/><br /><sub><b>Abdullah Alansari</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Ahimta" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars0.githubusercontent.com/u/796443?v=3?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://macadmincorner.com"><img src="https://avatars0.githubusercontent.com/u/614564?v=3?s=110" width="110px;" alt="Patrick Gallagher"/><br /><sub><b>Patrick Gallagher</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patgmac" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Miliamber"><img src="https://avatars3.githubusercontent.com/u/7165922?v=3?s=110" width="110px;" alt="Miliamber"/><br /><sub><b>Miliamber</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Miliamber" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hawk554"><img src="https://avatars3.githubusercontent.com/u/861766?v=3?s=110" width="110px;" alt="hawk554"/><br /><sub><b>hawk554</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hawk554" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jbirdkerr.net"><img src="https://avatars1.githubusercontent.com/u/1695622?v=3?s=110" width="110px;" alt="Justin Kerr"/><br /><sub><b>Justin Kerr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbirdkerr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.irasnyder.com/devel/"><img src="https://avatars3.githubusercontent.com/u/11426176?v=3?s=110" width="110px;" alt="Ira W. Snyder"/><br /><sub><b>Ira W. Snyder</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=irasnyd" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aalaily"><img src="https://avatars2.githubusercontent.com/u/2475759?v=3?s=110" width="110px;" alt="Aladin Alaily"/><br /><sub><b>Aladin Alaily</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aalaily" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kobie-chasehansen"><img src="https://avatars0.githubusercontent.com/u/10247644?v=3?s=110" width="110px;" alt="Chase Hansen"/><br /><sub><b>Chase Hansen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen" title="Code">💻</a> <a href="#question-kobie-chasehansen" title="Answering Questions">💬</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IDM-Helpdesk"><img src="https://avatars2.githubusercontent.com/u/13545400?v=3?s=110" width="110px;" alt="IDM Helpdesk"/><br /><sub><b>IDM Helpdesk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://balticer.de"><img src="https://avatars2.githubusercontent.com/u/614439?v=3?s=110" width="110px;" alt="Kai"/><br /><sub><b>Kai</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balticer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.michaeldaniels.me"><img src="https://avatars1.githubusercontent.com/u/8762511?v=3?s=110" width="110px;" alt="Michael Daniels"/><br /><sub><b>Michael Daniels</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mdaniels5757" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://tomcastleman.me"><img src="https://avatars3.githubusercontent.com/u/1532660?v=3?s=110" width="110px;" alt="Tom Castleman"/><br /><sub><b>Tom Castleman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tomcastleman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DanielNemanic"><img src="https://avatars3.githubusercontent.com/u/10723243?v=3?s=110" width="110px;" alt="Daniel Nemanic"/><br /><sub><b>Daniel Nemanic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DanielNemanic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/southwolf"><img src="https://avatars0.githubusercontent.com/u/150648?v=3?s=110" width="110px;" alt="SouthWolf"/><br /><sub><b>SouthWolf</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=southwolf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ivarne"><img src="https://avatars2.githubusercontent.com/u/131616?v=3?s=110" width="110px;" alt="Ivar Nesje"/><br /><sub><b>Ivar Nesje</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ivarne" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.j0k3r.net"><img src="https://avatars1.githubusercontent.com/u/62333?v=3?s=110" width="110px;" alt="Jérémy Benoist"/><br /><sub><b>Jérémy Benoist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=j0k3r" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cleathley"><img src="https://avatars2.githubusercontent.com/u/724344?v=3?s=110" width="110px;" alt="Chris Leathley"/><br /><sub><b>Chris Leathley</b></sub></a><br /><a href="#infra-cleathley" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/splaer"><img src="https://avatars0.githubusercontent.com/u/972498?v=3?s=110" width="110px;" alt="splaer"/><br /><sub><b>splaer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Asplaer" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=splaer" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.joeferguson.me"><img src="https://avatars1.githubusercontent.com/u/967362?v=3?s=110" width="110px;" alt="Joe Ferguson"/><br /><sub><b>Joe Ferguson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=svpernova09" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/diwanicki"><img src="https://avatars3.githubusercontent.com/u/6108682?v=3?s=110" width="110px;" alt="diwanicki"/><br /><sub><b>diwanicki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pakkua80"><img src="https://avatars3.githubusercontent.com/u/2527115?v=3?s=110" width="110px;" alt="Lee Thoong Ching"/><br /><sub><b>Lee Thoong Ching</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://shu.io"><img src="https://avatars1.githubusercontent.com/u/461491?v=3?s=110" width="110px;" alt="Marek Šuppa"/><br /><sub><b>Marek Šuppa</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mrshu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mizar1616"><img src="https://avatars1.githubusercontent.com/u/8693762?v=3?s=110" width="110px;" alt="Juan J. Martinez"/><br /><sub><b>Juan J. Martinez</b></sub></a><br /><a href="#translation-mizar1616" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rrdial"><img src="https://avatars1.githubusercontent.com/u/1458388?v=3?s=110" width="110px;" alt="R Ryan Dial"/><br /><sub><b>R Ryan Dial</b></sub></a><br /><a href="#translation-rrdial" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/burlito"><img src="https://avatars2.githubusercontent.com/u/2871745?v=3?s=110" width="110px;" alt="Andrej Manduch"/><br /><sub><b>Andrej Manduch</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=burlito" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.cordeos.com"><img src="https://avatars0.githubusercontent.com/u/8341172?v=3?s=110" width="110px;" alt="Jay Richards"/><br /><sub><b>Jay Richards</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=technogenus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://necurity.co.uk"><img src="https://avatars2.githubusercontent.com/u/7295127?v=3?s=110" width="110px;" alt="Alexander Innes"/><br /><sub><b>Alexander Innes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leostat" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://buzzedword.codes"><img src="https://avatars2.githubusercontent.com/u/334485?v=3?s=110" width="110px;" alt="Danny Garcia"/><br /><sub><b>Danny Garcia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=buzzedword" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/archpoint"><img src="https://avatars2.githubusercontent.com/u/366855?v=3?s=110" width="110px;" alt="archpoint"/><br /><sub><b>archpoint</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=archpoint" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jakemcgraw.com"><img src="https://avatars1.githubusercontent.com/u/67991?v=3?s=110" width="110px;" alt="Jake McGraw"/><br /><sub><b>Jake McGraw</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jakemcgraw" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FleischKarussel"><img src="https://avatars1.githubusercontent.com/u/1714374?v=3?s=110" width="110px;" alt="FleischKarussel"/><br /><sub><b>FleischKarussel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FleischKarussel" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feeva"><img src="https://avatars3.githubusercontent.com/u/319644?v=3?s=110" width="110px;" alt="Dylan Yi"/><br /><sub><b>Dylan Yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=feeva" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://FlashingCursor.com"><img src="https://avatars2.githubusercontent.com/u/857740?v=3?s=110" width="110px;" alt="Gil Rutkowski"/><br /><sub><b>Gil Rutkowski</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=flashingcursor" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.desmondmorris.com"><img src="https://avatars3.githubusercontent.com/u/129360?v=3?s=110" width="110px;" alt="Desmond Morris"/><br /><sub><b>Desmond Morris</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=desmondmorris" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://peelman.us"><img src="https://avatars2.githubusercontent.com/u/52936?v=3?s=110" width="110px;" alt="Nick Peelman"/><br /><sub><b>Nick Peelman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=peelman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://abrahamvegh.com"><img src="https://avatars0.githubusercontent.com/u/53161?v=3?s=110" width="110px;" alt="Abraham Vegh"/><br /><sub><b>Abraham Vegh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=abrahamvegh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rashivkp"><img src="https://avatars0.githubusercontent.com/u/2818680?v=3?s=110" width="110px;" alt="Mohamed Rashid"/><br /><sub><b>Mohamed Rashid</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rashivkp" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hinchk.github.io"><img src="https://avatars3.githubusercontent.com/u/1509456?v=3?s=110" width="110px;" alt="Kasey"/><br /><sub><b>Kasey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=HinchK" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BrettFagerlund"><img src="https://avatars2.githubusercontent.com/u/10522541?v=3?s=110" width="110px;" alt="Brett"/><br /><sub><b>Brett</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BrettFagerlund" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jasonspriggs.com"><img src="https://avatars2.githubusercontent.com/u/16108587?v=3?s=110" width="110px;" alt="Jason Spriggs"/><br /><sub><b>Jason Spriggs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonspriggs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://n8felton.wordpress.com"><img src="https://avatars2.githubusercontent.com/u/1134568?v=3?s=110" width="110px;" alt="Nate Felton"/><br /><sub><b>Nate Felton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=n8felton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://homepages.dcc.ufmg.br/~manassesferreira"><img src="https://avatars2.githubusercontent.com/u/14036694?v=3?s=110" width="110px;" alt="Manasses Ferreira"/><br /><sub><b>Manasses Ferreira</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=manassesferreira" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/steveelwood"><img src="https://avatars0.githubusercontent.com/u/15913949?v=3?s=110" width="110px;" alt="Steve"/><br /><sub><b>Steve</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=steveelwood" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/matc"><img src="https://avatars1.githubusercontent.com/u/3361683?v=3?s=110" width="110px;" alt="matc"/><br /><sub><b>matc</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=matc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.davisracingteam.com"><img src="https://avatars3.githubusercontent.com/u/7405702?v=3?s=110" width="110px;" alt="Cole R. Davis"/><br /><sub><b>Cole R. Davis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gibsonjoshua55"><img src="https://avatars2.githubusercontent.com/u/10167681?v=3?s=110" width="110px;" alt="gibsonjoshua55"/><br /><sub><b>gibsonjoshua55</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zwerch"><img src="https://avatars2.githubusercontent.com/u/2809241?v=4?s=110" width="110px;" alt="Robin Temme"/><br /><sub><b>Robin Temme</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zwerch" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imanghafoori1"><img src="https://avatars0.githubusercontent.com/u/6961695?v=4?s=110" width="110px;" alt="Iman"/><br /><sub><b>Iman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imanghafoori1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richardhofman6"><img src="https://avatars1.githubusercontent.com/u/6551003?v=4?s=110" width="110px;" alt="Richard Hofman"/><br /><sub><b>Richard Hofman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=richardhofman6" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gizzmojr"><img src="https://avatars0.githubusercontent.com/u/3697569?v=4?s=110" width="110px;" alt="gizzmojr"/><br /><sub><b>gizzmojr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gizzmojr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imjennyli"><img src="https://avatars3.githubusercontent.com/u/404729?v=4?s=110" width="110px;" alt="Jenny Li"/><br /><sub><b>Jenny Li</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imjennyli" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GeoffYoung"><img src="https://avatars0.githubusercontent.com/u/869227?v=4?s=110" width="110px;" alt="Geoff Young"/><br /><sub><b>Geoff Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=GeoffYoung" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.elliotblackburn.com"><img src="https://avatars3.githubusercontent.com/u/1068477?v=4?s=110" width="110px;" alt="Elliot Blackburn"/><br /><sub><b>Elliot Blackburn</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BlueHatbRit" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://andmemasin.eu"><img src="https://avatars1.githubusercontent.com/u/6357451?v=4?s=110" width="110px;" alt="Tõnis Ormisson"/><br /><sub><b>Tõnis Ormisson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TonisOrmisson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.nicolai-essig.de"><img src="https://avatars0.githubusercontent.com/u/449411?v=4?s=110" width="110px;" alt="Nicolai Essig"/><br /><sub><b>Nicolai Essig</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thakilla" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/techincolor"><img src="https://avatars1.githubusercontent.com/u/14809698?v=4?s=110" width="110px;" alt="Danielle"/><br /><sub><b>Danielle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=techincolor" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TheVakman"><img src="https://avatars1.githubusercontent.com/u/18545156?v=4?s=110" width="110px;" alt="Lawrence"/><br /><sub><b>Lawrence</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TheVakman" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uknzaeinozpas"><img src="https://avatars1.githubusercontent.com/u/22473767?v=4?s=110" width="110px;" alt="uknzaeinozpas"/><br /><sub><b>uknzaeinozpas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gelob"><img src="https://avatars3.githubusercontent.com/u/422752?v=4?s=110" width="110px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Gelob" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vcordes79"><img src="https://avatars1.githubusercontent.com/u/10672546?v=4?s=110" width="110px;" alt="vcordes79"/><br /><sub><b>vcordes79</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vcordes79" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fordster78"><img src="https://avatars3.githubusercontent.com/u/27958330?v=4?s=110" width="110px;" alt="fordster78"/><br /><sub><b>fordster78</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fordster78" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CronKz"><img src="https://avatars0.githubusercontent.com/u/34064225?v=4?s=110" width="110px;" alt="CronKz"/><br /><sub><b>CronKz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CronKz" title="Code">💻</a> <a href="#translation-CronKz" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tdb"><img src="https://avatars1.githubusercontent.com/u/585486?v=4?s=110" width="110px;" alt="Tim Bishop"/><br /><sub><b>Tim Bishop</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tdb" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.seanmcilvenna.com"><img src="https://avatars2.githubusercontent.com/u/5384694?v=4?s=110" width="110px;" alt="Sean McIlvenna"/><br /><sub><b>Sean McIlvenna</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=seanmcilvenna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cepacs"><img src="https://avatars3.githubusercontent.com/u/36515590?v=4?s=110" width="110px;" alt="cepacs"/><br /><sub><b>cepacs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Acepacs" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=cepacs" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lea-mink"><img src="https://avatars2.githubusercontent.com/u/37537300?v=4?s=110" width="110px;" alt="lea-mink"/><br /><sub><b>lea-mink</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lea-mink" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hannahtinkler"><img src="https://avatars0.githubusercontent.com/u/7140719?v=4?s=110" width="110px;" alt="Hannah Tinkler"/><br /><sub><b>Hannah Tinkler</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hannahtinkler" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doekman"><img src="https://avatars1.githubusercontent.com/u/1086388?v=4?s=110" width="110px;" alt="Doeke Zanstra"/><br /><sub><b>Doeke Zanstra</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=doekman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sdhd.nl/"><img src="https://avatars1.githubusercontent.com/u/4325936?v=4?s=110" width="110px;" alt="Djamon Staal"/><br /><sub><b>Djamon Staal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=SjamonDaal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EarlRamirez"><img src="https://avatars3.githubusercontent.com/u/12306859?v=4?s=110" width="110px;" alt="Earl Ramirez"/><br /><sub><b>Earl Ramirez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EarlRamirez" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RichardRay"><img src="https://avatars2.githubusercontent.com/u/8671456?v=4?s=110" width="110px;" alt="Richard Ray Thomas"/><br /><sub><b>Richard Ray Thomas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=RichardRay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=110" width="110px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thelamer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ParadoxGuitarist"><img src="https://avatars1.githubusercontent.com/u/6751928?v=4?s=110" width="110px;" alt="Brian Monroe"/><br /><sub><b>Brian Monroe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/plexorama"><img src="https://avatars1.githubusercontent.com/u/605167?v=4?s=110" width="110px;" alt="plexorama"/><br /><sub><b>plexorama</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=plexorama" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tilldeeke.de"><img src="https://avatars2.githubusercontent.com/u/1795149?v=4?s=110" width="110px;" alt="Till Deeke"/><br /><sub><b>Till Deeke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tilldeeke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/5quirrel"><img src="https://avatars0.githubusercontent.com/u/12634129?v=4?s=110" width="110px;" alt="5quirrel"/><br /><sub><b>5quirrel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=5quirrel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jasonlshelton"><img src="https://avatars1.githubusercontent.com/u/13071957?v=4?s=110" width="110px;" alt="Jason"/><br /><sub><b>Jason</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonlshelton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chemfy"><img src="https://avatars3.githubusercontent.com/u/7128321?v=4?s=110" width="110px;" alt="Antti"/><br /><sub><b>Antti</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chemfy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeusMaximus"><img src="https://avatars3.githubusercontent.com/u/10080364?v=4?s=110" width="110px;" alt="DeusMaximus"/><br /><sub><b>DeusMaximus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DeusMaximus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/A-ROYAL"><img src="https://avatars2.githubusercontent.com/u/16384611?v=4?s=110" width="110px;" alt="a-royal"/><br /><sub><b>a-royal</b></sub></a><br /><a href="#translation-A-ROYAL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/albertoaldrigo"><img src="https://avatars0.githubusercontent.com/u/5358208?v=4?s=110" width="110px;" alt="Alberto Aldrigo"/><br /><sub><b>Alberto Aldrigo</b></sub></a><br /><a href="#translation-albertoaldrigo" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://alex.stanev.org/blog"><img src="https://avatars0.githubusercontent.com/u/1412342?v=4?s=110" width="110px;" alt="Alex Stanev"/><br /><sub><b>Alex Stanev</b></sub></a><br /><a href="#translation-RealEnder" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://devel.itsolution2.de"><img src="https://avatars0.githubusercontent.com/u/177295?v=4?s=110" width="110px;" alt="Andreas Rehm"/><br /><sub><b>Andreas Rehm</b></sub></a><br /><a href="#translation-sirrus" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xelan"><img src="https://avatars0.githubusercontent.com/u/5080535?v=4?s=110" width="110px;" alt="Andreas Erhard"/><br /><sub><b>Andreas Erhard</b></sub></a><br /><a href="#translation-xelan" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/angeldeejay"><img src="https://avatars2.githubusercontent.com/u/142350?v=4?s=110" width="110px;" alt="Andrés Vanegas Jiménez"/><br /><sub><b>Andrés Vanegas Jiménez</b></sub></a><br /><a href="#translation-angeldeejay" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aschiavon91"><img src="https://avatars0.githubusercontent.com/u/3910403?v=4?s=110" width="110px;" alt="Antonio Schiavon"/><br /><sub><b>Antonio Schiavon</b></sub></a><br /><a href="#translation-aschiavon91" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benunter"><img src="https://avatars0.githubusercontent.com/u/10464547?v=4?s=110" width="110px;" alt="benunter"/><br /><sub><b>benunter</b></sub></a><br /><a href="#translation-benunter" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://catweb24.pl"><img src="https://avatars1.githubusercontent.com/u/5038647?v=4?s=110" width="110px;" alt="Borys Żmuda"/><br /><sub><b>Borys Żmuda</b></sub></a><br /><a href="#translation-rudashi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chibacityblues"><img src="https://avatars0.githubusercontent.com/u/5539359?v=4?s=110" width="110px;" alt="chibacityblues"/><br /><sub><b>chibacityblues</b></sub></a><br /><a href="#translation-chibacityblues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cwlin0416"><img src="https://avatars1.githubusercontent.com/u/1954830?v=4?s=110" width="110px;" alt="Chien Wei Lin"/><br /><sub><b>Chien Wei Lin</b></sub></a><br /><a href="#translation-cwlin0416" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Againstreality"><img src="https://avatars3.githubusercontent.com/u/11700533?v=4?s=110" width="110px;" alt="Christian Schuster"/><br /><sub><b>Christian Schuster</b></sub></a><br /><a href="#translation-Againstreality" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://chriss.webhostid.com"><img src="https://avatars1.githubusercontent.com/u/4308704?v=4?s=110" width="110px;" alt="Christian Stefanus"/><br /><sub><b>Christian Stefanus</b></sub></a><br /><a href="#translation-kopi-item" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://wxcafe.net"><img src="https://avatars3.githubusercontent.com/u/3009327?v=4?s=110" width="110px;" alt="wxcafé"/><br /><sub><b>wxcafé</b></sub></a><br /><a href="#translation-wxcafe" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpyroc"><img src="https://avatars3.githubusercontent.com/u/35761525?v=4?s=110" width="110px;" alt="dpyroc"/><br /><sub><b>dpyroc</b></sub></a><br /><a href="#translation-dpyroc" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.friedlmaier.net"><img src="https://avatars1.githubusercontent.com/u/2153639?v=4?s=110" width="110px;" alt="Daniel Friedlmaier"/><br /><sub><b>Daniel Friedlmaier</b></sub></a><br /><a href="#translation-da-friedl" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielheene"><img src="https://avatars1.githubusercontent.com/u/2947640?v=4?s=110" width="110px;" alt="Daniel Heene"/><br /><sub><b>Daniel Heene</b></sub></a><br /><a href="#translation-danielheene" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielcb"><img src="https://avatars3.githubusercontent.com/u/319022?v=4?s=110" width="110px;" alt="danielcb"/><br /><sub><b>danielcb</b></sub></a><br /><a href="#translation-danielcb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dominiksenti"><img src="https://avatars3.githubusercontent.com/u/15846537?v=4?s=110" width="110px;" alt="Dominik Senti"/><br /><sub><b>Dominik Senti</b></sub></a><br /><a href="#translation-dominiksenti" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.konectik.com"><img src="https://avatars0.githubusercontent.com/u/25570954?v=4?s=110" width="110px;" alt="Eric Gautheron"/><br /><sub><b>Eric Gautheron</b></sub></a><br /><a href="#translation-EpixFr" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://erlpil.com"><img src="https://avatars1.githubusercontent.com/u/5732623?v=4?s=110" width="110px;" alt="Erlend Pilø"/><br /><sub><b>Erlend Pilø</b></sub></a><br /><a href="#translation-Erlpil" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://fabio.technology"><img src="https://avatars0.githubusercontent.com/u/541832?v=4?s=110" width="110px;" alt="Fabio Rapposelli"/><br /><sub><b>Fabio Rapposelli</b></sub></a><br /><a href="#translation-frapposelli" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fgbs"><img src="https://avatars2.githubusercontent.com/u/3605240?v=4?s=110" width="110px;" alt="Felipe Barros"/><br /><sub><b>Felipe Barros</b></sub></a><br /><a href="#translation-fgbs" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/possebon"><img src="https://avatars0.githubusercontent.com/u/257745?v=4?s=110" width="110px;" alt="Fernando Possebon"/><br /><sub><b>Fernando Possebon</b></sub></a><br /><a href="#translation-possebon" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gdraque"><img src="https://avatars3.githubusercontent.com/u/2540832?v=4?s=110" width="110px;" alt="gdraque"/><br /><sub><b>gdraque</b></sub></a><br /><a href="#translation-gdraque" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/georgwallisch"><img src="https://avatars0.githubusercontent.com/u/23440381?v=4?s=110" width="110px;" alt="Georg Wallisch"/><br /><sub><b>Georg Wallisch</b></sub></a><br /><a href="#translation-georgwallisch" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgroblesr85"><img src="https://avatars1.githubusercontent.com/u/9852832?v=4?s=110" width="110px;" alt="Gerardo Robles"/><br /><sub><b>Gerardo Robles</b></sub></a><br /><a href="#translation-jgroblesr85" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://t.me/Gluek"><img src="https://avatars2.githubusercontent.com/u/11082640?v=4?s=110" width="110px;" alt="Gluek"/><br /><sub><b>Gluek</b></sub></a><br /><a href="#translation-mrgluek" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdnanAbuShahad"><img src="https://avatars0.githubusercontent.com/u/6847946?v=4?s=110" width="110px;" alt="AdnanAbuShahad"/><br /><sub><b>AdnanAbuShahad</b></sub></a><br /><a href="#translation-AdnanAbuShahad" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hafidzi.my"><img src="https://avatars1.githubusercontent.com/u/3580608?v=4?s=110" width="110px;" alt="Hafidzi My"/><br /><sub><b>Hafidzi My</b></sub></a><br /><a href="#translation-hafidzi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fofwisdom"><img src="https://avatars2.githubusercontent.com/u/205521?v=4?s=110" width="110px;" alt="Harim Park"/><br /><sub><b>Harim Park</b></sub></a><br /><a href="#translation-fofwisdom" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.kentsson.se"><img src="https://avatars2.githubusercontent.com/u/3333841?v=4?s=110" width="110px;" alt="Henrik Kentsson"/><br /><sub><b>Henrik Kentsson</b></sub></a><br /><a href="#translation-Kentsson" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/husnulyaqien"><img src="https://avatars0.githubusercontent.com/u/36551034?v=4?s=110" width="110px;" alt="Husnul Yaqien"/><br /><sub><b>Husnul Yaqien</b></sub></a><br /><a href="#translation-husnulyaqien" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://abaalkhail.org"><img src="https://avatars1.githubusercontent.com/u/2372747?v=4?s=110" width="110px;" alt="Ibrahim"/><br /><sub><b>Ibrahim</b></sub></a><br /><a href="#translation-abaalkh" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/igolman"><img src="https://avatars0.githubusercontent.com/u/1389334?v=4?s=110" width="110px;" alt="igolman"/><br /><sub><b>igolman</b></sub></a><br /><a href="#translation-igolman" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itangiang"><img src="https://avatars1.githubusercontent.com/u/3257070?v=4?s=110" width="110px;" alt="itangiang"/><br /><sub><b>itangiang</b></sub></a><br /><a href="#translation-itangiang" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jarby1211"><img src="https://avatars2.githubusercontent.com/u/14814254?v=4?s=110" width="110px;" alt="jarby1211"/><br /><sub><b>jarby1211</b></sub></a><br /><a href="#translation-jarby1211" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jwillker.com"><img src="https://avatars3.githubusercontent.com/u/6719357?v=4?s=110" width="110px;" alt="Jhonn Willker"/><br /><sub><b>Jhonn Willker</b></sub></a><br /><a href="#translation-JohnWillker" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joxelito94"><img src="https://avatars2.githubusercontent.com/u/10983635?v=4?s=110" width="110px;" alt="Jose"/><br /><sub><b>Jose</b></sub></a><br /><a href="#translation-joxelito94" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/laopangzi"><img src="https://avatars0.githubusercontent.com/u/5206122?v=4?s=110" width="110px;" alt="laopangzi"/><br /><sub><b>laopangzi</b></sub></a><br /><a href="#translation-laopangzi" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://usrportage.de"><img src="https://avatars2.githubusercontent.com/u/79707?v=4?s=110" width="110px;" alt="Lars Strojny"/><br /><sub><b>Lars Strojny</b></sub></a><br /><a href="#translation-lstrojny" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/marcosbl"><img src="https://avatars0.githubusercontent.com/u/389801?v=4?s=110" width="110px;" alt="MarcosBL"/><br /><sub><b>MarcosBL</b></sub></a><br /><a href="#translation-MarcosBL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariejoyacajes"><img src="https://avatars3.githubusercontent.com/u/35664606?v=4?s=110" width="110px;" alt="marie joy cajes"/><br /><sub><b>marie joy cajes</b></sub></a><br /><a href="#translation-mariejoyacajes" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.markjohansen.dk"><img src="https://avatars2.githubusercontent.com/u/3052816?v=4?s=110" width="110px;" alt="Mark S. Johansen"/><br /><sub><b>Mark S. Johansen</b></sub></a><br /><a href="#translation-msjohansen" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://martinstub.dk"><img src="https://avatars2.githubusercontent.com/u/982885?v=4?s=110" width="110px;" alt="Martin Stub"/><br /><sub><b>Martin Stub</b></sub></a><br /><a href="#translation-stubben" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/meyerf99"><img src="https://avatars2.githubusercontent.com/u/28959963?v=4?s=110" width="110px;" alt="Meyer Flavio"/><br /><sub><b>Meyer Flavio</b></sub></a><br /><a href="#translation-meyerf99" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars3.githubusercontent.com/u/796443?v=4?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="#translation-MicaelRodrigues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://rubixy.com/"><img src="https://avatars0.githubusercontent.com/u/10481331?v=4?s=110" width="110px;" alt="Mikael Rasmussen"/><br /><sub><b>Mikael Rasmussen</b></sub></a><br /><a href="#translation-mikaelssen" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IxFail"><img src="https://avatars1.githubusercontent.com/u/1544552?v=4?s=110" width="110px;" alt="IxFail"/><br /><sub><b>IxFail</b></sub></a><br /><a href="#translation-IxFail" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.mohammedfota.com"><img src="https://avatars3.githubusercontent.com/u/18483118?v=4?s=110" width="110px;" alt="Mohammed Fota"/><br /><sub><b>Mohammed Fota</b></sub></a><br /><a href="#translation-MohammedFota" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omego"><img src="https://avatars0.githubusercontent.com/u/227080?v=4?s=110" width="110px;" alt="Moayad Alserihi"/><br /><sub><b>Moayad Alserihi</b></sub></a><br /><a href="#translation-omego" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saymd"><img src="https://avatars0.githubusercontent.com/u/1680266?v=4?s=110" width="110px;" alt="saymd"/><br /><sub><b>saymd</b></sub></a><br /><a href="#translation-saymd" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nordsken.se"><img src="https://avatars0.githubusercontent.com/u/1826808?v=4?s=110" width="110px;" alt="Patrik Larsson"/><br /><sub><b>Patrik Larsson</b></sub></a><br /><a href="#translation-pooot" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drcryo"><img src="https://avatars1.githubusercontent.com/u/20584746?v=4?s=110" width="110px;" alt="drcryo"/><br /><sub><b>drcryo</b></sub></a><br /><a href="#translation-drcryo" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pawel1615"><img src="https://avatars1.githubusercontent.com/u/19408004?v=4?s=110" width="110px;" alt="pawel1615"/><br /><sub><b>pawel1615</b></sub></a><br /><a href="#translation-pawel1615" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bodrovics"><img src="https://avatars2.githubusercontent.com/u/23340468?v=4?s=110" width="110px;" alt="bodrovics"/><br /><sub><b>bodrovics</b></sub></a><br /><a href="#translation-bodrovics" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/priatna"><img src="https://avatars0.githubusercontent.com/u/3257654?v=4?s=110" width="110px;" alt="priatna"/><br /><sub><b>priatna</b></sub></a><br /><a href="#translation-priatna" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://amayume.net"><img src="https://avatars1.githubusercontent.com/u/5358374?v=4?s=110" width="110px;" alt="Fan Jiang"/><br /><sub><b>Fan Jiang</b></sub></a><br /><a href="#translation-ProfFan" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ragnarcx"><img src="https://avatars1.githubusercontent.com/u/22555451?v=4?s=110" width="110px;" alt="ragnarcx"/><br /><sub><b>ragnarcx</b></sub></a><br /><a href="#translation-ragnarcx" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.reinvanhaaren.nl/"><img src="https://avatars2.githubusercontent.com/u/18654582?v=4?s=110" width="110px;" alt="Rein van Haaren"/><br /><sub><b>Rein van Haaren</b></sub></a><br /><a href="#translation-reinvanhaaren" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://dheche.songolimo.net"><img src="https://avatars1.githubusercontent.com/u/386672?v=4?s=110" width="110px;" alt="Teguh Dwicaksana"/><br /><sub><b>Teguh Dwicaksana</b></sub></a><br /><a href="#translation-dheche" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FRaccie"><img src="https://avatars2.githubusercontent.com/u/2572552?v=4?s=110" width="110px;" alt="fraccie"/><br /><sub><b>fraccie</b></sub></a><br /><a href="#translation-FRaccie" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinzruzell"><img src="https://avatars0.githubusercontent.com/u/35182720?v=4?s=110" width="110px;" alt="vinzruzell"/><br /><sub><b>vinzruzell</b></sub></a><br /><a href="#translation-vinzruzell" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://kevinaustin.com"><img src="https://avatars1.githubusercontent.com/u/7883603?v=4?s=110" width="110px;" alt="Kevin Austin"/><br /><sub><b>Kevin Austin</b></sub></a><br /><a href="#translation-vipsystem" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://azuraweb.xyz"><img src="https://avatars3.githubusercontent.com/u/3861828?v=4?s=110" width="110px;" alt="Wira Sandy"/><br /><sub><b>Wira Sandy</b></sub></a><br /><a href="#translation-wira-sandy" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrayHoax"><img src="https://avatars2.githubusercontent.com/u/8663789?v=4?s=110" width="110px;" alt="Илья"/><br /><sub><b>Илья</b></sub></a><br /><a href="#translation-GrayHoax" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/godusevpn"><img src="https://avatars3.githubusercontent.com/u/30119111?v=4?s=110" width="110px;" alt="GodUseVPN"/><br /><sub><b>GodUseVPN</b></sub></a><br /><a href="#translation-godusevpn" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EngrZhou"><img src="https://avatars1.githubusercontent.com/u/745576?v=4?s=110" width="110px;" alt="周周"/><br /><sub><b>周周</b></sub></a><br /><a href="#translation-EngrZhou" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/takuy"><img src="https://avatars3.githubusercontent.com/u/1631095?v=4?s=110" width="110px;" alt="Sam"/><br /><sub><b>Sam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=takuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.illisian.com.au"><img src="https://avatars1.githubusercontent.com/u/264022?v=4?s=110" width="110px;" alt="Azerothian"/><br /><sub><b>Azerothian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azerothian" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://macfoo.wordpress.com/"><img src="https://avatars1.githubusercontent.com/u/4930051?v=4?s=110" width="110px;" alt="Wes Hulette"/><br /><sub><b>Wes Hulette</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jwhulette" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patrict"><img src="https://avatars0.githubusercontent.com/u/8134591?v=4?s=110" width="110px;" alt="patrict"/><br /><sub><b>patrict</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patrict" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VELIKII-DIVAN"><img src="https://avatars3.githubusercontent.com/u/2611616?v=4?s=110" width="110px;" alt="Dmitriy Minaev"/><br /><sub><b>Dmitriy Minaev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liquidhorse"><img src="https://avatars0.githubusercontent.com/u/5132245?v=4?s=110" width="110px;" alt="liquidhorse"/><br /><sub><b>liquidhorse</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=liquidhorse" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://seld.be/"><img src="https://avatars1.githubusercontent.com/u/183678?v=4?s=110" width="110px;" alt="Jordi Boggiano"/><br /><sub><b>Jordi Boggiano</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Seldaek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inietov"><img src="https://avatars0.githubusercontent.com/u/653557?v=4?s=110" width="110px;" alt="Ivan Nieto"/><br /><sub><b>Ivan Nieto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=inietov" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benrubson"><img src="https://avatars2.githubusercontent.com/u/6764151?v=4?s=110" width="110px;" alt="Ben RUBSON"/><br /><sub><b>Ben RUBSON</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benrubson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NMathar"><img src="https://avatars2.githubusercontent.com/u/8554558?v=4?s=110" width="110px;" alt="NMathar"/><br /><sub><b>NMathar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NMathar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smb"><img src="https://avatars1.githubusercontent.com/u/139566?v=4?s=110" width="110px;" alt="Steffen"/><br /><sub><b>Steffen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=smb" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sxderp"><img src="https://avatars0.githubusercontent.com/u/6609453?v=4?s=110" width="110px;" alt="Sxderp"/><br /><sub><b>Sxderp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Sxderp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fanta8897"><img src="https://avatars1.githubusercontent.com/u/4807843?v=4?s=110" width="110px;" alt="fanta8897"/><br /><sub><b>fanta8897</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fanta8897" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://andreybolonin.com/phpconsulting/"><img src="https://avatars2.githubusercontent.com/u/2576509?v=4?s=110" width="110px;" alt="Andrey Bolonin"/><br /><sub><b>Andrey Bolonin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreybolonin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.shinayoshi.net/"><img src="https://avatars3.githubusercontent.com/u/2173307?v=4?s=110" width="110px;" alt="shinayoshi"/><br /><sub><b>shinayoshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=shinayoshi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reuser"><img src="https://avatars3.githubusercontent.com/u/2130159?v=4?s=110" width="110px;" alt="Hubert"/><br /><sub><b>Hubert</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reuser" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://brashear.me"><img src="https://avatars0.githubusercontent.com/u/6865789?v=4?s=110" width="110px;" alt="KeenRivals"/><br /><sub><b>KeenRivals</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=KeenRivals" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omyno"><img src="https://avatars3.githubusercontent.com/u/2902513?v=4?s=110" width="110px;" alt="omyno"/><br /><sub><b>omyno</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=omyno" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackka"><img src="https://avatars1.githubusercontent.com/u/6271335?v=4?s=110" width="110px;" alt="Evgeny"/><br /><sub><b>Evgeny</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jackka" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://digitalist.se"><img src="https://avatars2.githubusercontent.com/u/1169963?v=4?s=110" width="110px;" alt="Colin Campbell"/><br /><sub><b>Colin Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=colin-campbell" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lubo"><img src="https://avatars3.githubusercontent.com/u/2872098?v=4?s=110" width="110px;" alt="Ľubomír Kučera"/><br /><sub><b>Ľubomír Kučera</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lubo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sourceguru.net"><img src="https://avatars3.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Mezzle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timothyfarmer"><img src="https://avatars1.githubusercontent.com/u/7632599?v=4?s=110" width="110px;" alt="Tim Farmer"/><br /><sub><b>Tim Farmer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=timothyfarmer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mskrip"><img src="https://avatars0.githubusercontent.com/u/17459600?v=4?s=110" width="110px;" alt="Marián Skrip"/><br /><sub><b>Marián Skrip</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mskrip" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Godmartinz"><img src="https://avatars2.githubusercontent.com/u/47435081?v=4?s=110" width="110px;" alt="Godfrey Martinez"/><br /><sub><b>Godfrey Martinez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Godmartinz" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bigtreeEdo"><img src="https://avatars1.githubusercontent.com/u/2075128?v=4?s=110" width="110px;" alt="bigtreeEdo"/><br /><sub><b>bigtreeEdo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bigtreeEdo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://colinmcneil.me/"><img src="https://avatars0.githubusercontent.com/u/5000430?v=4?s=110" width="110px;" alt="Colin McNeil"/><br /><sub><b>Colin McNeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ColinMcNeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JoKneeMo"><img src="https://avatars0.githubusercontent.com/u/421625?v=4?s=110" width="110px;" alt="JoKneeMo"/><br /><sub><b>JoKneeMo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JoKneeMo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.redbridge.se"><img src="https://avatars0.githubusercontent.com/u/54849013?v=4?s=110" width="110px;" alt="Joshi"/><br /><sub><b>Joshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joshi-redbridge" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anthonypburns"><img src="https://avatars2.githubusercontent.com/u/15731458?v=4?s=110" width="110px;" alt="Anthony Burns"/><br /><sub><b>Anthony Burns</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=anthonypburns" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnson-yi"><img src="https://avatars1.githubusercontent.com/u/63399474?v=4?s=110" width="110px;" alt="johnson-yi"/><br /><sub><b>johnson-yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=johnson-yi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tangentmc.net"><img src="https://avatars1.githubusercontent.com/u/1862720?v=4?s=110" width="110px;" alt="Sanjay Govind"/><br /><sub><b>Sanjay Govind</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sanjay900" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://peter.upfold.org.uk/"><img src="https://avatars0.githubusercontent.com/u/1255375?v=4?s=110" width="110px;" alt="Peter Upfold"/><br /><sub><b>Peter Upfold</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterUpfold" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jbiel"><img src="https://avatars2.githubusercontent.com/u/961717?v=4?s=110" width="110px;" alt="Jared Biel"/><br /><sub><b>Jared Biel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dampfklon"><img src="https://avatars1.githubusercontent.com/u/1733625?v=4?s=110" width="110px;" alt="Dampfklon"/><br /><sub><b>Dampfklon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dampfklon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://communityclosing.com"><img src="https://avatars2.githubusercontent.com/u/52973156?v=4?s=110" width="110px;" alt="Charles Hamilton"/><br /><sub><b>Charles Hamilton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chamilton-ccn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giannello"><img src="https://avatars.githubusercontent.com/u/551789?v=4?s=110" width="110px;" alt="Giuseppe Iannello"/><br /><sub><b>Giuseppe Iannello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=giannello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.peterdavehello.org/"><img src="https://avatars.githubusercontent.com/u/3691490?v=4?s=110" width="110px;" alt="Peter Dave Hello"/><br /><sub><b>Peter Dave Hello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterDaveHello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sigmoidal"><img src="https://avatars.githubusercontent.com/u/6106332?v=4?s=110" width="110px;" alt="sigmoidal"/><br /><sub><b>sigmoidal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sigmoidal" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phenixdotnet"><img src="https://avatars.githubusercontent.com/u/2082554?v=4?s=110" width="110px;" alt="Vincent Lainé"/><br /><sub><b>Vincent Lainé</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=phenixdotnet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.lucas-pless.com"><img src="https://avatars.githubusercontent.com/u/1943040?v=4?s=110" width="110px;" alt="Lucas Pleß"/><br /><sub><b>Lucas Pleß</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derlucas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/iansltx"><img src="https://avatars.githubusercontent.com/u/472804?v=4?s=110" width="110px;" alt="Ian Littman"/><br /><sub><b>Ian Littman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=iansltx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PauloLuna"><img src="https://avatars.githubusercontent.com/u/3519029?v=4?s=110" width="110px;" alt="João Paulo"/><br /><sub><b>João Paulo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PauloLuna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThoBur"><img src="https://avatars.githubusercontent.com/u/70443365?v=4?s=110" width="110px;" alt="ThoBur"/><br /><sub><b>ThoBur</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ThoBur" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://phpprofi.ru/"><img src="https://avatars.githubusercontent.com/u/1972329?v=4?s=110" width="110px;" alt="Alexander Chibrikin"/><br /><sub><b>Alexander Chibrikin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alek13" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winstan"><img src="https://avatars.githubusercontent.com/u/438332?v=4?s=110" width="110px;" alt="Anthony Winstanley"/><br /><sub><b>Anthony Winstanley</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=winstan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fashberg"><img src="https://avatars.githubusercontent.com/u/3075214?v=4?s=110" width="110px;" alt="Folke"/><br /><sub><b>Folke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fashberg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benwa"><img src="https://avatars.githubusercontent.com/u/1351571?v=4?s=110" width="110px;" alt="Bennett Blodinger"/><br /><sub><b>Bennett Blodinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benwa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nmc.dev"><img src="https://avatars.githubusercontent.com/u/2974631?v=4?s=110" width="110px;" alt="NMC"/><br /><sub><b>NMC</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ncareau" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andres-baller"><img src="https://avatars.githubusercontent.com/u/52182449?v=4?s=110" width="110px;" alt="andres-baller"/><br /><sub><b>andres-baller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andres-baller" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sean-borg"><img src="https://avatars.githubusercontent.com/u/67109348?v=4?s=110" width="110px;" alt="sean-borg"/><br /><sub><b>sean-borg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sean-borg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EDVLeer"><img src="https://avatars.githubusercontent.com/u/32170051?v=4?s=110" width="110px;" alt="EDVLeer"/><br /><sub><b>EDVLeer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EDVLeer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kurokat"><img src="https://avatars.githubusercontent.com/u/23075196?v=4?s=110" width="110px;" alt="Kurokat"/><br /><sub><b>Kurokat</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Kurokat" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.kevinkoellmann.de"><img src="https://avatars.githubusercontent.com/u/915514?v=4?s=110" width="110px;" alt="Kevin Köllmann"/><br /><sub><b>Kevin Köllmann</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koelle25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sw-mreyes"><img src="https://avatars.githubusercontent.com/u/49025941?v=4?s=110" width="110px;" alt="sw-mreyes"/><br /><sub><b>sw-mreyes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sw-mreyes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pittet.ca"><img src="https://avatars.githubusercontent.com/u/70129?v=4?s=110" width="110px;" alt="Joel Pittet"/><br /><sub><b>Joel Pittet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joelpittet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://elyscape.com"><img src="https://avatars.githubusercontent.com/u/792695?v=4?s=110" width="110px;" alt="Eli Young"/><br /><sub><b>Eli Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=elyscape" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raelldottin"><img src="https://avatars.githubusercontent.com/u/317015?v=4?s=110" width="110px;" alt="Raell Dottin"/><br /><sub><b>Raell Dottin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=raelldottin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/misilot"><img src="https://avatars.githubusercontent.com/u/1446856?v=4?s=110" width="110px;" alt="Tom Misilo"/><br /><sub><b>Tom Misilo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=misilot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://david.davenne.be"><img src="https://avatars.githubusercontent.com/u/4496300?v=4?s=110" width="110px;" alt="David Davenne"/><br /><sub><b>David Davenne</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JuustoMestari" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://markstenglein.com"><img src="https://avatars.githubusercontent.com/u/9255772?v=4?s=110" width="110px;" alt="Mark Stenglein"/><br /><sub><b>Mark Stenglein</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ocelotsloth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ajsy"><img src="https://avatars.githubusercontent.com/u/35658596?v=4?s=110" width="110px;" alt="ajsy"/><br /><sub><b>ajsy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ajsy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/t3easy"><img src="https://avatars.githubusercontent.com/u/3628035?v=4?s=110" width="110px;" alt="Jan Kiesewetter"/><br /><sub><b>Jan Kiesewetter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=t3easy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tetrachloromethane250"><img src="https://avatars.githubusercontent.com/u/79449630?v=4?s=110" width="110px;" alt="Tetrachloromethane250"/><br /><sub><b>Tetrachloromethane250</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.kajes.se/"><img src="https://avatars.githubusercontent.com/u/22004482?v=4?s=110" width="110px;" alt="Lars Kajes"/><br /><sub><b>Lars Kajes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kajes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joly0"><img src="https://avatars.githubusercontent.com/u/13993216?v=4?s=110" width="110px;" alt="Joly0"/><br /><sub><b>Joly0</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Joly0" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/limeless"><img src="https://avatars.githubusercontent.com/u/1501022?v=4?s=110" width="110px;" alt="theburger"/><br /><sub><b>theburger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=limeless" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deivishome"><img src="https://avatars.githubusercontent.com/u/36065681?v=4?s=110" width="110px;" alt="David Valin Alonso"/><br /><sub><b>David Valin Alonso</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=deivishome" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andreaci"><img src="https://avatars.githubusercontent.com/u/8290389?v=4?s=110" width="110px;" alt="andreaci"/><br /><sub><b>andreaci</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreaci" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jellesebreghts.be"><img src="https://avatars.githubusercontent.com/u/1828542?v=4?s=110" width="110px;" alt="Jelle Sebreghts"/><br /><sub><b>Jelle Sebreghts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Jelle-S" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Skywalker-11"><img src="https://avatars.githubusercontent.com/u/11180862?v=4?s=110" width="110px;" alt="Michael Pietsch"/><br /><sub><b>Michael Pietsch</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sh1hab"><img src="https://avatars.githubusercontent.com/u/22068886?v=4?s=110" width="110px;" alt="Masudul Haque Shihab"/><br /><sub><b>Masudul Haque Shihab</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sh1hab" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.freedomdive.com/"><img src="https://avatars.githubusercontent.com/u/16099942?v=4?s=110" width="110px;" alt="Supapong Areeprasertkul"/><br /><sub><b>Supapong Areeprasertkul</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zybersup" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/psarossy"><img src="https://avatars.githubusercontent.com/u/207358?v=4?s=110" width="110px;" alt="Peter Sarossy"/><br /><sub><b>Peter Sarossy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=psarossy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nepella"><img src="https://avatars.githubusercontent.com/u/11823649?v=4?s=110" width="110px;" alt="Renee Margaret McConahy"/><br /><sub><b>Renee Margaret McConahy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nepella" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohnnyPicnic"><img src="https://avatars.githubusercontent.com/u/5553884?v=4?s=110" width="110px;" alt="JohnnyPicnic"/><br /><sub><b>JohnnyPicnic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markbrule"><img src="https://avatars.githubusercontent.com/u/8799594?v=4?s=110" width="110px;" alt="markbrule"/><br /><sub><b>markbrule</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=markbrule" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikecmpbll"><img src="https://avatars.githubusercontent.com/u/1962801?v=4?s=110" width="110px;" alt="Mike Campbell"/><br /><sub><b>Mike Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikecmpbll" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tbrconnect"><img src="https://avatars.githubusercontent.com/u/11973217?v=4?s=110" width="110px;" alt="tbrconnect"/><br /><sub><b>tbrconnect</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tbrconnect" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kcoyo"><img src="https://avatars.githubusercontent.com/u/12447225?v=4?s=110" width="110px;" alt="kcoyo"/><br /><sub><b>kcoyo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kcoyo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://travismiller.com/"><img src="https://avatars.githubusercontent.com/u/494017?v=4?s=110" width="110px;" alt="Travis Miller"/><br /><sub><b>Travis Miller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=travismiller" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Delta5"><img src="https://avatars.githubusercontent.com/u/1975640?v=4?s=110" width="110px;" alt="Evan Taylor"/><br /><sub><b>Evan Taylor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Delta5" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PetriAsi"><img src="https://avatars.githubusercontent.com/u/8735148?v=4?s=110" width="110px;" alt="Petri Asikainen"/><br /><sub><b>Petri Asikainen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PetriAsi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/derdeagle"><img src="https://avatars.githubusercontent.com/u/11424540?v=4?s=110" width="110px;" alt="derdeagle"/><br /><sub><b>derdeagle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derdeagle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wh0rd.org/"><img src="https://avatars.githubusercontent.com/u/176950?v=4?s=110" width="110px;" alt="Mike Frysinger"/><br /><sub><b>Mike Frysinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vapier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AL4AL"><img src="https://avatars.githubusercontent.com/u/22044358?v=4?s=110" width="110px;" alt="ALPHA"/><br /><sub><b>ALPHA</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AL4AL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.ifern.de"><img src="https://avatars.githubusercontent.com/u/1042587?v=4?s=110" width="110px;" alt="FliegenKLATSCH"/><br /><sub><b>FliegenKLATSCH</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerm"><img src="https://avatars.githubusercontent.com/u/442138?v=4?s=110" width="110px;" alt="Jeremy Price"/><br /><sub><b>Jeremy Price</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jerm" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Toreg87"><img src="https://avatars.githubusercontent.com/u/84392209?v=4?s=110" width="110px;" alt="Toreg87"/><br /><sub><b>Toreg87</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Toreg87" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Computroniks"><img src="https://avatars.githubusercontent.com/u/67638596?v=4?s=110" width="110px;" alt="Matthew Nickson"/><br /><sub><b>Matthew Nickson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Computroniks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jethron.id.au"><img src="https://avatars.githubusercontent.com/u/1646397?v=4?s=110" width="110px;" alt="Jethro Nederhof"/><br /><sub><b>Jethro Nederhof</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jethron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/01ste02"><img src="https://avatars.githubusercontent.com/u/23289826?v=4?s=110" width="110px;" alt="Oskar Stenberg"/><br /><sub><b>Oskar Stenberg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=01ste02" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Robert-Azelis"><img src="https://avatars.githubusercontent.com/u/82208283?v=4?s=110" width="110px;" alt="Robert-Azelis"/><br /><sub><b>Robert-Azelis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Robert-Azelis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alwism"><img src="https://avatars.githubusercontent.com/u/60648387?v=4?s=110" width="110px;" alt="Alexander William Smith"/><br /><sub><b>Alexander William Smith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alwism" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.leitwerk.de/"><img src="https://avatars.githubusercontent.com/u/24418301?v=4?s=110" width="110px;" alt="LEITWERK AG"/><br /><sub><b>LEITWERK AG</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leitwerk-ag" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.aboutcher.co.uk"><img src="https://avatars.githubusercontent.com/u/1911435?v=4?s=110" width="110px;" alt="Adam"/><br /><sub><b>Adam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamboutcher" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://snksrv.com"><img src="https://avatars.githubusercontent.com/u/16104273?v=4?s=110" width="110px;" alt="Ian"/><br /><sub><b>Ian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sneak-it" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.bestlong.idv.tw/"><img src="https://avatars.githubusercontent.com/u/4023909?v=4?s=110" width="110px;" alt="Shao Yu-Lung (Allen)"/><br /><sub><b>Shao Yu-Lung (Allen)</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bestlong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Haxatron"><img src="https://avatars.githubusercontent.com/u/76475453?v=4?s=110" width="110px;" alt="Haxatron"/><br /><sub><b>Haxatron</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Haxatron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PlaneNuts"><img src="https://avatars.githubusercontent.com/u/88776392?v=4?s=110" width="110px;" alt="PlaneNuts"/><br /><sub><b>PlaneNuts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PlaneNuts" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://bjcpgd.cias.rit.edu"><img src="https://avatars.githubusercontent.com/u/3842948?v=4?s=110" width="110px;" alt="Bradley Coudriet"/><br /><sub><b>Bradley Coudriet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=exula" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://daltondur.st"><img src="https://avatars.githubusercontent.com/u/21966173?v=4?s=110" width="110px;" alt="Dalton Durst"/><br /><sub><b>Dalton Durst</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://adagiohealth.org"><img src="https://avatars.githubusercontent.com/u/38761237?v=4?s=110" width="110px;" alt="Alex Janes"/><br /><sub><b>Alex Janes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adagioajanes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nuraeil"><img src="https://avatars.githubusercontent.com/u/32387849?v=4?s=110" width="110px;" alt="Nuraeil"/><br /><sub><b>Nuraeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nuraeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TenOfTens"><img src="https://avatars.githubusercontent.com/u/48162670?v=4?s=110" width="110px;" alt="TenOfTens"/><br /><sub><b>TenOfTens</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TenOfTens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ditisjens.be/"><img src="https://avatars.githubusercontent.com/u/9415391?v=4?s=110" width="110px;" alt="waffle"/><br /><sub><b>waffle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=insert-waffle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/veenone"><img src="https://avatars.githubusercontent.com/u/3839381?v=4?s=110" width="110px;" alt="Achmad Fienan Rahardianto"/><br /><sub><b>Achmad Fienan Rahardianto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=veenone" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisweirich"><img src="https://avatars.githubusercontent.com/u/97299851?v=4?s=110" width="110px;" alt="Christian Weirich"/><br /><sub><b>Christian Weirich</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chrisweirich" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/denzfarid"><img src="https://avatars.githubusercontent.com/u/1294403?v=4?s=110" width="110px;" alt="denzfarid"/><br /><sub><b>denzfarid</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ntbutler-nbcs"><img src="https://avatars.githubusercontent.com/u/94018771?v=4?s=110" width="110px;" alt="ntbutler-nbcs"/><br /><sub><b>ntbutler-nbcs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://naveensrinivasan.dev"><img src="https://avatars.githubusercontent.com/u/172697?v=4?s=110" width="110px;" alt="Naveen"/><br /><sub><b>Naveen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=naveensrinivasan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikeroq"><img src="https://avatars.githubusercontent.com/u/55674383?v=4?s=110" width="110px;" alt="Mike Roquemore"/><br /><sub><b>Mike Roquemore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikeroq" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reederda"><img src="https://avatars.githubusercontent.com/u/7991086?v=4?s=110" width="110px;" alt="Daniel Reeder"/><br /><sub><b>Daniel Reeder</b></sub></a><br /><a href="#translation-reederda" title="Translation">🌍</a> <a href="#translation-reederda" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=reederda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vickyjaura183"><img src="https://avatars.githubusercontent.com/u/109422491?v=4?s=110" width="110px;" alt="vickyjaura183"/><br /><sub><b>vickyjaura183</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vickyjaura183" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/julian-piehl"><img src="https://avatars.githubusercontent.com/u/32363424?v=4?s=110" width="110px;" alt="Peace"/><br /><sub><b>Peace</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=julian-piehl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kylegordon"><img src="https://avatars.githubusercontent.com/u/231528?v=4?s=110" width="110px;" alt="Kyle Gordon"/><br /><sub><b>Kyle Gordon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kylegordon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.bfh.ch"><img src="https://avatars.githubusercontent.com/u/53009155?v=4?s=110" width="110px;" alt="Katharina Drexel"/><br /><sub><b>Katharina Drexel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sunflowerbofh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://david.sferruzza.fr/"><img src="https://avatars.githubusercontent.com/u/1931963?v=4?s=110" width="110px;" alt="David Sferruzza"/><br /><sub><b>David Sferruzza</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dsferruzza" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rnelsonee"><img src="https://avatars.githubusercontent.com/u/19511639?v=4?s=110" width="110px;" alt="Rick Nelson"/><br /><sub><b>Rick Nelson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rnelsonee" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BasO12"><img src="https://avatars.githubusercontent.com/u/94169344?v=4?s=110" width="110px;" alt="BasO12"/><br /><sub><b>BasO12</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BasO12" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vautia"><img src="https://avatars.githubusercontent.com/u/111710123?v=4?s=110" width="110px;" alt="Vautia"/><br /><sub><b>Vautia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Vautia" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.littlehart.net/atthekeyboard"><img src="https://avatars.githubusercontent.com/u/28321?v=4?s=110" width="110px;" alt="Chris Hartjes"/><br /><sub><b>Chris Hartjes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chartjes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geo-chen"><img src="https://avatars.githubusercontent.com/u/2404584?v=4?s=110" width="110px;" alt="geo-chen"/><br /><sub><b>geo-chen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=geo-chen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nh314"><img src="https://avatars.githubusercontent.com/u/6006620?v=4?s=110" width="110px;" alt="Phan Nguyen"/><br /><sub><b>Phan Nguyen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nh314" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/StarlessNights"><img src="https://avatars.githubusercontent.com/u/115993812?v=4?s=110" width="110px;" alt="Iisakki Jaakkola"/><br /><sub><b>Iisakki Jaakkola</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=StarlessNights" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=110" width="110px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=eltociear" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukasfehling"><img src="https://avatars.githubusercontent.com/u/56871540?v=4?s=110" width="110px;" alt="Lukas Fehling"/><br /><sub><b>Lukas Fehling</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lukasfehling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fernando-almeida"><img src="https://avatars.githubusercontent.com/u/1975990?v=4?s=110" width="110px;" alt="Fernando Almeida"/><br /><sub><b>Fernando Almeida</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fernando-almeida" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/akemidx"><img src="https://avatars.githubusercontent.com/u/116301219?v=4?s=110" width="110px;" alt="akemidx"/><br /><sub><b>akemidx</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=akemidx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://oguz.site"><img src="https://avatars.githubusercontent.com/u/144778?v=4?s=110" width="110px;" alt="Oguz Bilgic"/><br /><sub><b>Oguz Bilgic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=oguzbilgic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/scoo73r"><img src="https://avatars.githubusercontent.com/u/9262438?v=4?s=110" width="110px;" alt="Scooter Crawford"/><br /><sub><b>Scooter Crawford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=scoo73r" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/subdriven"><img src="https://avatars.githubusercontent.com/u/5957345?v=4?s=110" width="110px;" alt="subdriven"/><br /><sub><b>subdriven</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=subdriven" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AndrewSav"><img src="https://avatars.githubusercontent.com/u/658865?v=4?s=110" width="110px;" alt="Andrew Savinykh"/><br /><sub><b>Andrew Savinykh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AndrewSav" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kenchan0130.github.io"><img src="https://avatars.githubusercontent.com/u/1155067?v=4?s=110" width="110px;" alt="Tadayuki Onishi"/><br /><sub><b>Tadayuki Onishi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kenchan0130" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/floschoepfer"><img src="https://avatars.githubusercontent.com/u/112496896?v=4?s=110" width="110px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=floschoepfer" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://spencerlong.com"><img src="https://avatars.githubusercontent.com/u/7305753?v=4?s=110" width="110px;" alt="Spencer Long"/><br /><sub><b>Spencer Long</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=spencerrlongg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcusmoore"><img src="https://avatars.githubusercontent.com/u/1141514?v=4?s=110" width="110px;" alt="Marcus Moore"/><br /><sub><b>Marcus Moore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=marcusmoore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mezzle"><img src="https://avatars.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="http://dboth.de"><img src="https://avatars.githubusercontent.com/u/5731963?v=4?s=110" width="110px;" alt="dboth"/><br /><sub><b>dboth</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dboth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zacharyfleck"><img src="https://avatars.githubusercontent.com/u/87536651?v=4?s=110" width="110px;" alt="Zachary Fleck"/><br /><sub><b>Zachary Fleck</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zacharyfleck" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vikaas-cyper"><img src="https://avatars.githubusercontent.com/u/74609912?v=4?s=110" width="110px;" alt="VIKAAS-A"/><br /><sub><b>VIKAAS-A</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vikaas-cyper" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ak-piracha"><img src="https://avatars.githubusercontent.com/u/88882041?v=4?s=110" width="110px;" alt="Abdul Kareem"/><br /><sub><b>Abdul Kareem</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ak-piracha" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NojoudAlshehri"><img src="https://avatars.githubusercontent.com/u/111287779?v=4?s=110" width="110px;" alt="NojoudAlshehri"/><br /><sub><b>NojoudAlshehri</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stefanstidlffg"><img src="https://avatars.githubusercontent.com/u/54367449?v=4?s=110" width="110px;" alt="Stefan Stidl"/><br /><sub><b>Stefan Stidl</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=stefanstidlffg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qay21"><img src="https://avatars.githubusercontent.com/u/87803479?v=4?s=110" width="110px;" alt="Quentin Aymard"/><br /><sub><b>Quentin Aymard</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=qay21" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cram42"><img src="https://avatars.githubusercontent.com/u/5396871?v=4?s=110" width="110px;" alt="Grant Le Roux"/><br /><sub><b>Grant Le Roux</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=cram42" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://@singrity"><img src="https://avatars.githubusercontent.com/u/58479551?v=4?s=110" width="110px;" alt="Bogdan"/><br /><sub><b>Bogdan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Singrity" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mmanjos"><img src="https://avatars.githubusercontent.com/u/3483684?v=4?s=110" width="110px;" alt="mmanjos"/><br /><sub><b>mmanjos</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mmanjos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://azooz2014.github.io/"><img src="https://avatars.githubusercontent.com/u/7429229?v=4?s=110" width="110px;" alt="Abdelaziz Faki"/><br /><sub><b>Abdelaziz Faki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azooz2014" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bilias"><img src="https://avatars.githubusercontent.com/u/47315739?v=4?s=110" width="110px;" alt="bilias"/><br /><sub><b>bilias</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bilias" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coach1988"><img src="https://avatars.githubusercontent.com/u/2565989?v=4?s=110" width="110px;" alt="coach1988"/><br /><sub><b>coach1988</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=coach1988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mauro-miatello"><img src="https://avatars.githubusercontent.com/u/11910225?v=4?s=110" width="110px;" alt="MrM"/><br /><sub><b>MrM</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mauro-miatello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/koiakoia"><img src="https://avatars.githubusercontent.com/u/60405354?v=4?s=110" width="110px;" alt="koiakoia"/><br /><sub><b>koiakoia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koiakoia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mustafa-online"><img src="https://avatars.githubusercontent.com/u/5323832?v=4?s=110" width="110px;" alt="Mustafa Online"/><br /><sub><b>Mustafa Online</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mustafa-online" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franceslui"><img src="https://avatars.githubusercontent.com/u/104601439?v=4?s=110" width="110px;" alt="franceslui"/><br /><sub><b>franceslui</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=franceslui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Q4kK"><img src="https://avatars.githubusercontent.com/u/125313163?v=4?s=110" width="110px;" alt="Q4kK"/><br /><sub><b>Q4kK</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Q4kK" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/squintfox"><img src="https://avatars.githubusercontent.com/u/55590532?v=4?s=110" width="110px;" alt="squintfox"/><br /><sub><b>squintfox</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=squintfox" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeffclay"><img src="https://avatars.githubusercontent.com/u/1380084?v=4?s=110" width="110px;" alt="Jeff Clay"/><br /><sub><b>Jeff Clay</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jeffclay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PP-JN-RL"><img src="https://avatars.githubusercontent.com/u/52716446?v=4?s=110" width="110px;" alt="Phil J R"/><br /><sub><b>Phil J R</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PP-JN-RL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.corelight.com/"><img src="https://avatars.githubusercontent.com/u/1496725?v=4?s=110" width="110px;" alt="i_virus"/><br /><sub><b>i_virus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chandanchowdhury" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gitgrimbo"><img src="https://avatars.githubusercontent.com/u/1020541?v=4?s=110" width="110px;" alt="Paul Grime"/><br /><sub><b>Paul Grime</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gitgrimbo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://leeporte.co.uk"><img src="https://avatars.githubusercontent.com/u/922815?v=4?s=110" width="110px;" alt="Lee Porte"/><br /><sub><b>Lee Porte</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=LeePorte" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bryanlopezinc"><img src="https://avatars.githubusercontent.com/u/23613427?v=4?s=110" width="110px;" alt="BRYAN "/><br /><sub><b>BRYAN </b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/U-H-T"><img src="https://avatars.githubusercontent.com/u/64061710?v=4?s=110" width="110px;" alt="U-H-T"/><br /><sub><b>U-H-T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=U-H-T" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyree"><img src="https://avatars.githubusercontent.com/u/5395363?v=4?s=110" width="110px;" alt="Matt Tyree"/><br /><sub><b>Matt Tyree</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tyree" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,4 +1,4 @@
FROM ubuntu:22.04
FROM ubuntu:20.04
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
# No need to add `apt-get clean` here, reference:
@@ -14,17 +14,15 @@ RUN export DEBIAN_FRONTEND=noninteractive; \
apt-utils \
apache2 \
apache2-bin \
libapache2-mod-php8.1 \
php8.1-curl \
php8.1-ldap \
php8.1-mysql \
php8.1-gd \
php8.1-xml \
php8.1-mbstring \
php8.1-zip \
php8.1-bcmath \
php8.1-redis \
php-memcached \
libapache2-mod-php7.4 \
php7.4-curl \
php7.4-ldap \
php7.4-mysql \
php7.4-gd \
php7.4-xml \
php7.4-mbstring \
php7.4-zip \
php7.4-bcmath \
patch \
curl \
wget \
@@ -38,10 +36,9 @@ gcc \
make \
autoconf \
libc-dev \
libldap-common \
pkg-config \
libmcrypt-dev \
php8.1-dev \
php7.4-dev \
ca-certificates \
unzip \
dnsutils \
@@ -51,16 +48,16 @@ dnsutils \
RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar
RUN php go-pear.phar
RUN pecl install mcrypt
RUN pecl install mcrypt-1.0.3
RUN bash -c "echo extension=/usr/lib/php/20210902/mcrypt.so > /etc/php/8.1/mods-available/mcrypt.ini"
RUN bash -c "echo extension=/usr/lib/php/20190902/mcrypt.so > /etc/php/7.4/mods-available/mcrypt.ini"
RUN phpenmod mcrypt
RUN phpenmod gd
RUN phpenmod bcmath
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/apache2/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/cli/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/7.4/apache2/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/7.4/cli/php.ini
RUN useradd -m --uid 1000 --gid 50 docker
@@ -105,7 +102,7 @@ RUN \
&& ln -fs "/var/lib/snipeit/keys/ldap_client_tls.cert" "/var/www/html/storage/ldap_client_tls.cert" \
&& ln -fs "/var/lib/snipeit/keys/ldap_client_tls.key" "/var/www/html/storage/ldap_client_tls.key" \
&& chown docker "/var/lib/snipeit/keys/" \
&& chown -Rh docker "/var/www/html/storage/" \
&& chown -h docker "/var/www/html/storage/" \
&& chmod +x /var/www/html/artisan \
&& echo "Finished setting up application in /var/www/html"
@@ -141,4 +138,4 @@ RUN chmod +x /startup.sh /usr/bin/supervisor-exit-event-listener
CMD ["/startup.sh"]
EXPOSE 80
EXPOSE 443
EXPOSE 443

View File

@@ -1,35 +1,32 @@
FROM alpine:3.18.6
FROM alpine:3.14.2
# Apache + PHP
RUN apk add --no-cache \
apache2 \
php81 \
php81-common \
php81-apache2 \
php81-curl \
php81-ldap \
php81-mysqli \
php81-gd \
php81-xml \
php81-mbstring \
php81-zip \
php81-ctype \
php81-tokenizer \
php81-pdo_mysql \
php81-openssl \
php81-bcmath \
php81-phar \
php81-json \
php81-iconv \
php81-fileinfo \
php81-simplexml \
php81-session \
php81-dom \
php81-xmlwriter \
php81-xmlreader \
php81-sodium \
php81-redis \
php81-pecl-memcached \
php81-exif \
php7 \
php7-common \
php7-apache2 \
php7-curl \
php7-ldap \
php7-mysqli \
php7-gd \
php7-xml \
php7-mbstring \
php7-zip \
php7-ctype \
php7-tokenizer \
php7-pdo_mysql \
php7-openssl \
php7-bcmath \
php7-phar \
php7-json \
php7-iconv \
php7-fileinfo \
php7-simplexml \
php7-session \
php7-dom \
php7-xmlwriter \
php7-xmlreader \
php7-sodium \
curl \
wget \
vim \
@@ -42,7 +39,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php81/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php7/php.ini
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite
@@ -87,4 +84,4 @@ ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/entrypoint.sh"]
EXPOSE 80
EXPOSE 80

View File

@@ -1,8 +1,8 @@
ARG ENVIRONMENT=production
ARG SNIPEIT_RELEASE=6.1.0
ARG PHP_VERSION=8.2
ARG PHP_ALPINE_VERSION=3.17
ARG COMPOSER_VERSION=2
ARG SNIPEIT_RELEASE=5.1.3
ARG PHP_VERSION=7.4.16
ARG PHP_ALPINE_VERSION=3.13
ARG COMPOSER_VERSION=2.0.11
# Cannot use arguments with 'COPY --from' workaround
# https://github.com/moby/moby/issues/34482#issuecomment-454716952
@@ -52,7 +52,7 @@ RUN { \
# Install php extensions inside docker containers easily
# https://github.com/mlocati/docker-php-extension-installer
COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensions /usr/local/bin/
COPY --from=mlocati/php-extension-installer:1.2.19 /usr/bin/install-php-extensions /usr/local/bin/
RUN set -eux; \
install-php-extensions \
bcmath \
@@ -75,14 +75,14 @@ RUN set -eux; \
rm snipeit.tar.gz; \
# Install composer php dependencies
if [ "$ENVIRONMENT" = "production" ]; then \
echo "production environment detected!"; \
echo "production enviroment detected!"; \
composer update \
--no-cache \
--no-dev \
--optimize-autoloader \
--working-dir=/var/www/html; \
else \
echo "development environment detected!"; \
echo "development enviroment detected!"; \
apk add --no-cache \
${DEV_PACKAGES}; \
composer update \
@@ -100,4 +100,4 @@ COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]

View File

@@ -1,18 +1,15 @@
![snipe-it-by-grok](https://github.com/snipe/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/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![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/snipe/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/snipe/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)
![Build Status](https://app.chipperci.com/projects/0e5f8979-31eb-4ee6-9abf-050b76ab0383/status/master) [![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/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=snipe/snipe-it&amp;utm_campaign=Badge_Grade)
[![All Contributors](https://img.shields.io/badge/all_contributors-284-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
## Snipe-IT - Open Source Asset Management System
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
It is built on [Laravel 10](http://laravel.com).
It is built on [Laravel 6](http://laravel.com).
Snipe-IT is actively developed and we [release quite frequently](https://github.com/snipe/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
> [!TIP]
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
__This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
-----
@@ -22,7 +19,7 @@ For instructions on installing and configuring Snipe-IT on your server, check ou
If you're having trouble with the installation, please check the [Common Issues](https://snipe-it.readme.io/docs/common-issues) and [Getting Help](https://snipe-it.readme.io/docs/getting-help) documentation, and search this repository's open *and* closed issues for help.
<!-- [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) -->
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
-----
### User's Manual
@@ -33,9 +30,8 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
> [!IMPORTANT]
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
>
**PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
-----
### Upgrading
@@ -59,25 +55,18 @@ Please see the [translations documentation](https://snipe-it.readme.io/docs/tran
Since the release of the JSON REST API, several third-party developers have been developing modules and libraries to work with Snipe-IT.
> [!NOTE]
> As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
- [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
- [InQRy -unmaintained-](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [SnipeitPS](https://github.com/snazy2000/SnipeitPS) by [@snazy2000](https://github.com/snazy2000) - Powershell API Wrapper for Snipe-it
- [jamf2snipe](https://github.com/grokability/jamf2snipe) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT
- [jamf2snipe](https://github.com/ParadoxGuitarist/jamf2snipe) by [@ParadoxGuitarist](https://github.com/ParadoxGuitarist) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
- [Marksman](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-it.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT
As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
-----
@@ -85,15 +74,65 @@ Since the release of the JSON REST API, several third-party developers have been
Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
The ERD is available [online here](https://drawsql.app/templates/snipe-it).
[Here is a list](CONTRIBUTORS.md) of the wonderful people that have contributed to the Snipe-IT.
-----
### Security
> [!IMPORTANT]
> **To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.**
To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.
-----
### Contributors
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
| [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
| [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/5547470?v=3" width="110px;"/><br /><sub>Hereward</sub>](https://github.com/thehereward)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thehereward "Code") | [<img src="https://avatars0.githubusercontent.com/u/5802977?v=3" width="110px;"/><br /><sub>swoopdk</sub>](https://github.com/swoopdk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=swoopdk "Code") | [<img src="https://avatars1.githubusercontent.com/u/3470403?v=3" width="110px;"/><br /><sub>Abdullah Alansari</sub>](https://linkedin.com/in/ahimta)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Ahimta "Code") | [<img src="https://avatars0.githubusercontent.com/u/796443?v=3" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues "Code") | [<img src="https://avatars0.githubusercontent.com/u/614564?v=3" width="110px;"/><br /><sub>Patrick Gallagher</sub>](http://macadmincorner.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=patgmac "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/7165922?v=3" width="110px;"/><br /><sub>Miliamber</sub>](https://github.com/Miliamber)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Miliamber "Code") | [<img src="https://avatars3.githubusercontent.com/u/861766?v=3" width="110px;"/><br /><sub>hawk554</sub>](https://github.com/hawk554)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hawk554 "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1695622?v=3" width="110px;"/><br /><sub>Justin Kerr</sub>](http://jbirdkerr.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbirdkerr "Code") | [<img src="https://avatars3.githubusercontent.com/u/11426176?v=3" width="110px;"/><br /><sub>Ira W. Snyder</sub>](http://www.irasnyder.com/devel/)<br />[📖](https://github.com/snipe/snipe-it/commits?author=irasnyd "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/2475759?v=3" width="110px;"/><br /><sub>Aladin Alaily</sub>](https://github.com/aalaily)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aalaily "Code") | [<img src="https://avatars0.githubusercontent.com/u/10247644?v=3" width="110px;"/><br /><sub>Chase Hansen</sub>](https://github.com/kobie-chasehansen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [<img src="https://avatars2.githubusercontent.com/u/13545400?v=3" width="110px;"/><br /><sub>IDM Helpdesk</sub>](https://github.com/IDM-Helpdesk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk "Code") | [<img src="https://avatars2.githubusercontent.com/u/614439?v=3" width="110px;"/><br /><sub>Kai</sub>](http://balticer.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balticer "Code") | [<img src="https://avatars1.githubusercontent.com/u/8762511?v=3" width="110px;"/><br /><sub>Michael Daniels</sub>](http://www.michaeldaniels.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mdaniels5757 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/1532660?v=3" width="110px;"/><br /><sub>Tom Castleman</sub>](http://tomcastleman.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tomcastleman "Code") | [<img src="https://avatars3.githubusercontent.com/u/10723243?v=3" width="110px;"/><br /><sub>Daniel Nemanic</sub>](https://github.com/DanielNemanic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielNemanic "Code") | [<img src="https://avatars0.githubusercontent.com/u/150648?v=3" width="110px;"/><br /><sub>SouthWolf</sub>](https://github.com/southwolf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=southwolf "Code") | [<img src="https://avatars2.githubusercontent.com/u/131616?v=3" width="110px;"/><br /><sub>Ivar Nesje</sub>](https://github.com/ivarne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ivarne "Code") | [<img src="https://avatars1.githubusercontent.com/u/62333?v=3" width="110px;"/><br /><sub>Jérémy Benoist</sub>](http://www.j0k3r.net)<br />[📖](https://github.com/snipe/snipe-it/commits?author=j0k3r "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/724344?v=3" width="110px;"/><br /><sub>Chris Leathley</sub>](https://github.com/cleathley)<br />[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars0.githubusercontent.com/u/972498?v=3" width="110px;"/><br /><sub>splaer</sub>](https://github.com/splaer)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=splaer "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/967362?v=3" width="110px;"/><br /><sub>Joe Ferguson</sub>](http://www.joeferguson.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=svpernova09 "Code") | [<img src="https://avatars3.githubusercontent.com/u/6108682?v=3" width="110px;"/><br /><sub>diwanicki</sub>](https://github.com/diwanicki)<br />[💻](https://github.com/snipe/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/snipe/snipe-it/commits?author=diwanicki "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/2527115?v=3" width="110px;"/><br /><sub>Lee Thoong Ching</sub>](https://github.com/pakkua80)<br />[📖](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Code") | [<img src="https://avatars1.githubusercontent.com/u/461491?v=3" width="110px;"/><br /><sub>Marek Šuppa</sub>](http://shu.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrshu "Code") | [<img src="https://avatars1.githubusercontent.com/u/8693762?v=3" width="110px;"/><br /><sub>Juan J. Martinez</sub>](https://github.com/mizar1616)<br />[🌍](#translation-mizar1616 "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1458388?v=3" width="110px;"/><br /><sub>R Ryan Dial</sub>](https://github.com/rrdial)<br />[🌍](#translation-rrdial "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2871745?v=3" width="110px;"/><br /><sub>Andrej Manduch</sub>](https://github.com/burlito)<br />[📖](https://github.com/snipe/snipe-it/commits?author=burlito "Documentation") |
| [<img src="https://avatars0.githubusercontent.com/u/8341172?v=3" width="110px;"/><br /><sub>Jay Richards</sub>](http://www.cordeos.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=technogenus "Code") | [<img src="https://avatars2.githubusercontent.com/u/7295127?v=3" width="110px;"/><br /><sub>Alexander Innes</sub>](https://necurity.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leostat "Code") | [<img src="https://avatars2.githubusercontent.com/u/334485?v=3" width="110px;"/><br /><sub>Danny Garcia</sub>](https://buzzedword.codes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=buzzedword "Code") | [<img src="https://avatars2.githubusercontent.com/u/366855?v=3" width="110px;"/><br /><sub>archpoint</sub>](https://github.com/archpoint)<br />[💻](https://github.com/snipe/snipe-it/commits?author=archpoint "Code") | [<img src="https://avatars1.githubusercontent.com/u/67991?v=3" width="110px;"/><br /><sub>Jake McGraw</sub>](http://www.jakemcgraw.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jakemcgraw "Code") | [<img src="https://avatars1.githubusercontent.com/u/1714374?v=3" width="110px;"/><br /><sub>FleischKarussel</sub>](https://github.com/FleischKarussel)<br />[📖](https://github.com/snipe/snipe-it/commits?author=FleischKarussel "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/319644?v=3" width="110px;"/><br /><sub>Dylan Yi</sub>](https://github.com/feeva)<br />[💻](https://github.com/snipe/snipe-it/commits?author=feeva "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/857740?v=3" width="110px;"/><br /><sub>Gil Rutkowski</sub>](http://FlashingCursor.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=flashingcursor "Code") | [<img src="https://avatars3.githubusercontent.com/u/129360?v=3" width="110px;"/><br /><sub>Desmond Morris</sub>](http://www.desmondmorris.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=desmondmorris "Code") | [<img src="https://avatars2.githubusercontent.com/u/52936?v=3" width="110px;"/><br /><sub>Nick Peelman</sub>](http://peelman.us)<br />[💻](https://github.com/snipe/snipe-it/commits?author=peelman "Code") | [<img src="https://avatars0.githubusercontent.com/u/53161?v=3" width="110px;"/><br /><sub>Abraham Vegh</sub>](https://abrahamvegh.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=abrahamvegh "Code") | [<img src="https://avatars0.githubusercontent.com/u/2818680?v=3" width="110px;"/><br /><sub>Mohamed Rashid</sub>](https://github.com/rashivkp)<br />[📖](https://github.com/snipe/snipe-it/commits?author=rashivkp "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/1509456?v=3" width="110px;"/><br /><sub>Kasey</sub>](http://hinchk.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=HinchK "Code") | [<img src="https://avatars2.githubusercontent.com/u/10522541?v=3" width="110px;"/><br /><sub>Brett</sub>](https://github.com/BrettFagerlund)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=BrettFagerlund "Tests") |
| [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
| [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
| [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/4930051?v=4" width="110px;"/><br /><sub>Wes Hulette</sub>](http://macfoo.wordpress.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [<img src="https://avatars0.githubusercontent.com/u/8134591?v=4" width="110px;"/><br /><sub>patrict</sub>](https://github.com/patrict)<br />[💻](https://github.com/snipe/snipe-it/commits?author=patrict "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/2611616?v=4" width="110px;"/><br /><sub>Dmitriy Minaev</sub>](https://github.com/VELIKII-DIVAN)<br />[💻](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [<img src="https://avatars0.githubusercontent.com/u/5132245?v=4" width="110px;"/><br /><sub>liquidhorse</sub>](https://github.com/liquidhorse)<br />[💻](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [<img src="https://avatars1.githubusercontent.com/u/183678?v=4" width="110px;"/><br /><sub>Jordi Boggiano</sub>](https://seld.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [<img src="https://avatars0.githubusercontent.com/u/653557?v=4" width="110px;"/><br /><sub>Ivan Nieto</sub>](https://github.com/inietov)<br />[💻](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [<img src="https://avatars2.githubusercontent.com/u/6764151?v=4" width="110px;"/><br /><sub>Ben RUBSON</sub>](https://github.com/benrubson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [<img src="https://avatars2.githubusercontent.com/u/8554558?v=4" width="110px;"/><br /><sub>NMathar</sub>](https://github.com/NMathar)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [<img src="https://avatars1.githubusercontent.com/u/139566?v=4" width="110px;"/><br /><sub>Steffen</sub>](https://github.com/smb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smb "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/6609453?v=4" width="110px;"/><br /><sub>Sxderp</sub>](https://github.com/Sxderp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [<img src="https://avatars1.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>fanta8897</sub>](https://github.com/fanta8897)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [<img src="https://avatars2.githubusercontent.com/u/2576509?v=4" width="110px;"/><br /><sub>Andrey Bolonin</sub>](https://andreybolonin.com/phpconsulting/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [<img src="https://avatars3.githubusercontent.com/u/2173307?v=4" width="110px;"/><br /><sub>shinayoshi</sub>](http://www.shinayoshi.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [<img src="https://avatars3.githubusercontent.com/u/2130159?v=4" width="110px;"/><br /><sub>Hubert</sub>](https://github.com/reuser)<br />[💻](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [<img src="https://avatars0.githubusercontent.com/u/6865789?v=4" width="110px;"/><br /><sub>KeenRivals</sub>](https://brashear.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [<img src="https://avatars3.githubusercontent.com/u/2902513?v=4" width="110px;"/><br /><sub>omyno</sub>](https://github.com/omyno)<br />[💻](https://github.com/snipe/snipe-it/commits?author=omyno "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/6271335?v=4" width="110px;"/><br /><sub>Evgeny</sub>](https://github.com/jackka)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jackka "Code") | [<img src="https://avatars2.githubusercontent.com/u/1169963?v=4" width="110px;"/><br /><sub>Colin Campbell</sub>](https://digitalist.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=colin-campbell "Code") | [<img src="https://avatars3.githubusercontent.com/u/2872098?v=4" width="110px;"/><br /><sub>Ľubomír Kučera</sub>](https://github.com/lubo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lubo "Code") | [<img src="https://avatars3.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://www.sourceguru.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mezzle "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [<img src="https://avatars0.githubusercontent.com/u/17459600?v=4" width="110px;"/><br /><sub>Marián Skrip</sub>](https://github.com/mskrip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | [<img src="https://avatars2.githubusercontent.com/u/47435081?v=4" width="110px;"/><br /><sub>Godfrey Martinez</sub>](https://github.com/Godmartinz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/2075128?v=4" width="110px;"/><br /><sub>bigtreeEdo</sub>](https://github.com/bigtreeEdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [<img src="https://avatars0.githubusercontent.com/u/5000430?v=4" width="110px;"/><br /><sub>Colin McNeil</sub>](https://colinmcneil.me/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [<img src="https://avatars0.githubusercontent.com/u/421625?v=4" width="110px;"/><br /><sub>JoKneeMo</sub>](https://github.com/JoKneeMo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [<img src="https://avatars0.githubusercontent.com/u/54849013?v=4" width="110px;"/><br /><sub>Joshi</sub>](http://www.redbridge.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [<img src="https://avatars2.githubusercontent.com/u/15731458?v=4" width="110px;"/><br /><sub>Anthony Burns</sub>](https://github.com/anthonypburns)<br />[💻](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [<img src="https://avatars1.githubusercontent.com/u/63399474?v=4" width="110px;"/><br /><sub>johnson-yi</sub>](https://github.com/johnson-yi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=johnson-yi "Code") | [<img src="https://avatars1.githubusercontent.com/u/1862720?v=4" width="110px;"/><br /><sub>Sanjay Govind</sub>](https://tangentmc.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sanjay900 "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/1255375?v=4" width="110px;"/><br /><sub>Peter Upfold</sub>](https://peter.upfold.org.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterUpfold "Code") | [<img src="https://avatars2.githubusercontent.com/u/961717?v=4" width="110px;"/><br /><sub>Jared Biel</sub>](https://github.com/jbiel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbiel "Code") | [<img src="https://avatars1.githubusercontent.com/u/1733625?v=4" width="110px;"/><br /><sub>Dampfklon</sub>](https://github.com/dampfklon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dampfklon "Code") | [<img src="https://avatars2.githubusercontent.com/u/52973156?v=4" width="110px;"/><br /><sub>Charles Hamilton</sub>](https://communityclosing.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chamilton-ccn "Code") | [<img src="https://avatars.githubusercontent.com/u/551789?v=4" width="110px;"/><br /><sub>Giuseppe Iannello</sub>](https://github.com/giannello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=giannello "Code") | [<img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="110px;"/><br /><sub>Peter Dave Hello</sub>](https://www.peterdavehello.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterDaveHello "Code") | [<img src="https://avatars.githubusercontent.com/u/6106332?v=4" width="110px;"/><br /><sub>sigmoidal</sub>](https://github.com/sigmoidal)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sigmoidal "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2082554?v=4" width="110px;"/><br /><sub>Vincent Lainé</sub>](https://github.com/phenixdotnet)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phenixdotnet "Code") | [<img src="https://avatars.githubusercontent.com/u/1943040?v=4" width="110px;"/><br /><sub>Lucas Pleß</sub>](http://www.lucas-pless.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derlucas "Code") | [<img src="https://avatars.githubusercontent.com/u/472804?v=4" width="110px;"/><br /><sub>Ian Littman</sub>](http://twitter.com/iansltx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=iansltx "Code") | [<img src="https://avatars.githubusercontent.com/u/3519029?v=4" width="110px;"/><br /><sub>João Paulo</sub>](https://github.com/PauloLuna)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PauloLuna "Code") | [<img src="https://avatars.githubusercontent.com/u/70443365?v=4" width="110px;"/><br /><sub>ThoBur</sub>](https://github.com/ThoBur)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ThoBur "Code") | [<img src="https://avatars.githubusercontent.com/u/1972329?v=4" width="110px;"/><br /><sub>Alexander Chibrikin</sub>](http://phpprofi.ru/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | [<img src="https://avatars.githubusercontent.com/u/438332?v=4" width="110px;"/><br /><sub>Anthony Winstanley</sub>](https://github.com/winstan)<br />[💻](https://github.com/snipe/snipe-it/commits?author=winstan "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3075214?v=4" width="110px;"/><br /><sub>Folke</sub>](https://github.com/fashberg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fashberg "Code") | [<img src="https://avatars.githubusercontent.com/u/1351571?v=4" width="110px;"/><br /><sub>Bennett Blodinger</sub>](https://github.com/benwa)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benwa "Code") | [<img src="https://avatars.githubusercontent.com/u/2974631?v=4" width="110px;"/><br /><sub>NMC</sub>](https://nmc.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ncareau "Code") | [<img src="https://avatars.githubusercontent.com/u/52182449?v=4" width="110px;"/><br /><sub>andres-baller</sub>](https://github.com/andres-baller)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andres-baller "Code") | [<img src="https://avatars.githubusercontent.com/u/67109348?v=4" width="110px;"/><br /><sub>sean-borg</sub>](https://github.com/sean-borg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sean-borg "Code") | [<img src="https://avatars.githubusercontent.com/u/32170051?v=4" width="110px;"/><br /><sub>EDVLeer</sub>](https://github.com/EDVLeer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EDVLeer "Code") | [<img src="https://avatars.githubusercontent.com/u/23075196?v=4" width="110px;"/><br /><sub>Kurokat</sub>](https://github.com/Kurokat)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Kurokat "Code") |
| [<img src="https://avatars.githubusercontent.com/u/915514?v=4" width="110px;"/><br /><sub>Kevin Köllmann</sub>](https://www.kevinkoellmann.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koelle25 "Code") | [<img src="https://avatars.githubusercontent.com/u/49025941?v=4" width="110px;"/><br /><sub>sw-mreyes</sub>](https://github.com/sw-mreyes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sw-mreyes "Code") | [<img src="https://avatars.githubusercontent.com/u/70129?v=4" width="110px;"/><br /><sub>Joel Pittet</sub>](https://pittet.ca)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joelpittet "Code") | [<img src="https://avatars.githubusercontent.com/u/792695?v=4" width="110px;"/><br /><sub>Eli Young</sub>](https://elyscape.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=elyscape "Code") | [<img src="https://avatars.githubusercontent.com/u/317015?v=4" width="110px;"/><br /><sub>Raell Dottin</sub>](https://github.com/raelldottin)<br />[💻](https://github.com/snipe/snipe-it/commits?author=raelldottin "Code") | [<img src="https://avatars.githubusercontent.com/u/1446856?v=4" width="110px;"/><br /><sub>Tom Misilo</sub>](https://github.com/misilot)<br />[💻](https://github.com/snipe/snipe-it/commits?author=misilot "Code") | [<img src="https://avatars.githubusercontent.com/u/4496300?v=4" width="110px;"/><br /><sub>David Davenne</sub>](http://david.davenne.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JuustoMestari "Code") |
| [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
| [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
| [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,65 +0,0 @@
# Running the Test Suite
This document is targeted at developers looking to make modifications to this application's code base and want to run the existing test suite.
Before starting, follow the [instructions](README.md#installation) for installing the application locally and ensure you can load it in a browser properly.
## Unit and Feature Tests
Before attempting to run the test suite copy the example environment file for tests and update the values to match your environment:
`cp .env.testing.example .env.testing`
The following should work for running tests in memory with sqlite:
```
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=sqlite_testing
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=null
#DB_USERNAME=null
#DB_PASSWORD=null
```
To use MySQL you should update the `DB_` variables to match your local test database:
```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE={}
DB_USERNAME={}
DB_PASSWORD={}
```
Now you are ready to run the entire test suite from your terminal:
```shell
php artisan test
````
To run individual test files, you can pass the path to the test that you want to run:
```shell
php artisan test tests/Unit/AccessoryTest.php
```
Some tests, like ones concerning LDAP, are marked with the `@group` annotation. Those groups can be run, or excluded, using the `--group` or `--exclude-group` flags:
```shell
php artisan test --group=ldap
php artisan test --exclude-group=ldap
```
This can be helpful if a set of tests are failing because you don't have an extension, like LDAP, installed.

View File

@@ -38,7 +38,7 @@
"description": "The maximum number of search results that can be returned at one time.",
"value": "500"
},
"MAIL_MAILER": {
"MAIL_DRIVER": {
"description": "Mail driver - Generally SMTP on Heroku - https://snipe-it.readme.io/docs/configuration#required-outgoing-mail-settings",
"value": "smtp"
},
@@ -58,9 +58,9 @@
"description": "SMTP Server Password",
"value": "YOURPASSWORD"
},
"MAIL_TLS_VERIFY_PEER": {
"description": "Ensure validity of TLS certificate on remote mail server",
"value": true
"MAIL_ENCRYPTION": {
"description": "Encryption protocol for email sending.",
"value": "null"
},
"MAIL_FROM_ADDR": {
"description": "Email from address",
@@ -118,7 +118,7 @@
"description": "The duration (in seconds) that the user should be blocked from attempting to authenticate again.",
"value": "60"
},
"LOG_CHANNEL": {
"APP_LOG": {
"description": "Driver to send logs to. (errorlog for stderr)",
"value": "errorlog"
},
@@ -148,7 +148,7 @@
"image": "heroku/php",
"addons": [
"cleardb:ignite",
"heroku-redis:mini",
"heroku-redis:hobby-dev",
"papertrail:choklad"
]
}
}

View File

@@ -22,7 +22,7 @@ class CheckoutLicenseToAllUsers extends Command
*
* @var string
*/
protected $description = 'Checks out licenses to all users';
protected $description = 'Command description';
/**
* Create a new command instance.
@@ -56,7 +56,7 @@ class CheckoutLicenseToAllUsers extends Command
return false;
}
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
$users = User::whereNull('deleted_at')->with('licenses')->get();
if ($users->count() > $license->getAvailSeatsCountAttribute()) {
$this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. ');

View File

@@ -3,31 +3,15 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use \App\Models\User;
class CreateAdmin extends Command
{
/** @mixin User **/
/**
* App\Console\CreateAdmin
* @property mixed $first_name
* @property string $last_name
* @property string $username
* @property string $email
* @property string $permissions
* @property string $password
* @property boolean $activated
* @property boolean $show_in_list
* @property boolean $autoassign_licenses
* @property \Illuminate\Support\Carbon|null $created_at
* @property mixed $created_by
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?}';
/**
* The console command description.
@@ -46,7 +30,11 @@ class CreateAdmin extends Command
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$first_name = $this->option('first_name');
@@ -55,14 +43,11 @@ class CreateAdmin extends Command
$email = $this->option('email');
$password = $this->option('password');
$show_in_list = $this->argument('show_in_list');
$autoassign_licenses = $this->argument('autoassign_licenses');
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
$this->info('ERROR: All fields are required.');
} else {
$user = new User;
$user = new \App\Models\User;
$user->first_name = $first_name;
$user->last_name = $last_name;
$user->username = $username;
@@ -74,11 +59,6 @@ class CreateAdmin extends Command
if ($show_in_list == 'false') {
$user->show_in_list = 0;
}
if ($autoassign_licenses == 'false') {
$user->autoassign_licenses = 0;
}
if ($user->save()) {
$this->info('New user created');
$user->groups()->attach(1);

View File

@@ -1,97 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\DB;
class GeneratePersonalAccessToken extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:make-api-key
{--user_id= : The ID of the user to create the token for.}
{--name= : The name of the new API token}
{--key-only : Only return the value of the API key}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This console command allows you to generate Personal API tokens to be used with the Snipe-IT JSON REST API on behalf of a user.';
/**
* The token repository implementation.
*
* @var \Laravel\Passport\TokenRepository
*/
protected $tokenRepository;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
{
$this->validation = $validation;
$this->tokenRepository = $tokenRepository;
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$accessTokenName = $this->option('name');
if ($accessTokenName=='') {
$accessTokenName = 'CLI Auth Token';
}
if ($this->option('user_id')=='') {
return $this->error('ERROR: user_id cannot be blank.');
}
if ($user = User::find($this->option('user_id'))) {
$createAccessToken = $user->createToken($accessTokenName)->accessToken;
if ($this->option('key-only')) {
$this->info($createAccessToken);
} else {
$this->warn('Your API Token has been created. Be sure to copy this token now, as it will not be accessible again.');
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
$this->info('API Token ID: '.$token->id);
}
$this->info('API Token User: '.$user->present()->fullName.' ('.$user->username.')');
$this->info('API Token Name: '.$accessTokenName);
$this->info('API Token: '.$createAccessToken);
}
} else {
return $this->error('ERROR: Invalid user. API key was not created.');
}
}
}

View File

@@ -3,13 +3,12 @@
namespace App\Console\Commands;
use App\Models\Department;
use App\Models\Group;
use Illuminate\Console\Command;
use App\Models\Setting;
use App\Models\Ldap;
use App\Models\User;
use App\Models\Location;
use Illuminate\Support\Facades\Log;
use Log;
class LdapSync extends Command
{
@@ -18,7 +17,7 @@ class LdapSync extends Command
*
* @var string
*/
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--summary} {--json_summary}';
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=} {--base_dn=} {--summary} {--json_summary}';
/**
* The console command description.
@@ -44,29 +43,19 @@ class LdapSync extends Command
*/
public function handle()
{
// If LDAP enabled isn't set to 1 (ldap_enabled!=1) then we should cut this short immediately without going any further
if (Setting::getSettings()->ldap_enabled!='1') {
$this->error('LDAP is not enabled. Aborting. See Settings > LDAP to enable it.');
exit();
}
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
$ldap_result_username = Setting::getSettings()->ldap_username_field;
$ldap_result_last_name = Setting::getSettings()->ldap_lname_field;
$ldap_result_first_name = Setting::getSettings()->ldap_fname_field;
$ldap_result_active_flag = Setting::getSettings()->ldap_active_flag;
$ldap_result_active_flag = Setting::getSettings()->ldap_active_flag_field;
$ldap_result_emp_num = Setting::getSettings()->ldap_emp_num;
$ldap_result_email = Setting::getSettings()->ldap_email;
$ldap_result_phone = Setting::getSettings()->ldap_phone_field;
$ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
$ldap_result_country = Setting::getSettings()->ldap_country;
$ldap_result_location = Setting::getSettings()->ldap_location;
$ldap_result_dept = Setting::getSettings()->ldap_dept;
$ldap_result_manager = Setting::getSettings()->ldap_manager;
$ldap_default_group = Setting::getSettings()->ldap_default_group;
$search_base = Setting::getSettings()->ldap_base_dn;
try {
$ldapconn = Ldap::connectToLdap();
@@ -76,7 +65,7 @@ class LdapSync extends Command
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
LOG::info($e);
return [];
}
@@ -84,70 +73,42 @@ class LdapSync extends Command
$summary = [];
try {
/**
* if a location ID has been specified, use that OU
*/
if ( $this->option('location_id') ) {
foreach($this->option('location_id') as $location_id){
$location_ou = Location::where('id', '=', $location_id)->value('ldap_ou');
$search_base = $location_ou;
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
}
}
/**
* if a manual base DN has been specified, use that. Allow the Base DN to override
* even if there's a location-based DN - if you picked it, you must have picked it for a reason.
*/
if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn');
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".');
}
/**
* If a filter has been specified, use that
*/
if ($this->option('filter') != '') {
$results = Ldap::findLdapUsers($search_base, -1, $this->option('filter'));
LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".');
} else {
$results = Ldap::findLdapUsers($search_base);
$search_base = null;
}
$results = Ldap::findLdapUsers($search_base);
} catch (\Exception $e) {
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
LOG::info($e);
return [];
}
/* Determine which location to assign users to by default. */
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
if ($this->option('location') != '') {
if ($location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
} elseif ($this->option('location_id')) {
foreach($this->option('location_id') as $location_id) {
if ($location = Location::where('id', '=', $location_id)->first()) {
Log::debug('Location ID ' . $location_id . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
}
$location = Location::where('name', '=', $this->option('location'))->first();
LOG::debug('Location name '.$this->option('location').' passed');
LOG::debug('Importing to '.$location->name.' ('.$location->id.')');
} elseif ($this->option('location_id') != '') {
$location = Location::where('id', '=', $this->option('location_id'))->first();
LOG::debug('Location ID '.$this->option('location_id').' passed');
LOG::debug('Importing to '.$location->name.' ('.$location->id.')');
}
if (! isset($location)) {
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
LOG::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
}
/* Process locations with explicitly defined OUs, if doing a full import. */
if ($this->option('base_dn') == '' && $this->option('filter') == '') {
if ($this->option('base_dn') == '') {
// Retrieve locations with a mapped OU, and sort them from the shallowest to deepest OU (see #3993)
$ldap_ou_locations = Location::where('ldap_ou', '!=', '')->get()->toArray();
$ldap_ou_lengths = [];
@@ -159,7 +120,7 @@ class LdapSync extends Command
array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations);
if (count($ldap_ou_locations) > 0) {
Log::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.');
LOG::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.');
}
// Inject location information fields
@@ -177,7 +138,7 @@ class LdapSync extends Command
$json_summary = ['error' => true, 'error_message' => trans('admin/users/message.error.ldap_could_not_search').' Location: '.$ldap_loc['name'].' (ID: '.$ldap_loc['id'].') cannot connect to "'.$ldap_loc['ldap_ou'].'" - '.$e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
LOG::info($e);
return [];
}
@@ -205,44 +166,30 @@ class LdapSync extends Command
}
}
$manager_cache = [];
if($ldap_default_group != null) {
$default = Group::find($ldap_default_group);
if (!$default) {
$ldap_default_group = null; // un-set the default group if that group doesn't exist
}
}
/* Create user account entries in Snipe-IT */
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20);
$pass = bcrypt($tmp_pass);
for ($i = 0; $i < $results['count']; $i++) {
if (empty($ldap_result_active_flag) || $results[$i][$ldap_result_active_flag][0] == 'TRUE') {
$item = [];
$item['username'] = $results[$i][$ldap_result_username][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_result_emp_num][0] ?? '';
$item['lastname'] = $results[$i][$ldap_result_last_name][0] ?? '';
$item['firstname'] = $results[$i][$ldap_result_first_name][0] ?? '';
$item['email'] = $results[$i][$ldap_result_email][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_result_phone][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_result_jobtitle][0] ?? '';
$item['country'] = $results[$i][$ldap_result_country][0] ?? '';
$item['department'] = $results[$i][$ldap_result_dept][0] ?? '';
$item['manager'] = $results[$i][$ldap_result_manager][0] ?? '';
$item['location'] = $results[$i][$ldap_result_location][0] ?? '';
$item['username'] = isset($results[$i][$ldap_result_username][0]) ? $results[$i][$ldap_result_username][0] : '';
$item['employee_number'] = isset($results[$i][$ldap_result_emp_num][0]) ? $results[$i][$ldap_result_emp_num][0] : '';
$item['lastname'] = isset($results[$i][$ldap_result_last_name][0]) ? $results[$i][$ldap_result_last_name][0] : '';
$item['firstname'] = isset($results[$i][$ldap_result_first_name][0]) ? $results[$i][$ldap_result_first_name][0] : '';
$item['email'] = isset($results[$i][$ldap_result_email][0]) ? $results[$i][$ldap_result_email][0] : '';
$item['ldap_location_override'] = isset($results[$i]['ldap_location_override']) ? $results[$i]['ldap_location_override'] : '';
$item['location_id'] = isset($results[$i]['location_id']) ? $results[$i]['location_id'] : '';
$item['telephone'] = isset($results[$i][$ldap_result_phone][0]) ? $results[$i][$ldap_result_phone][0] : '';
$item['jobtitle'] = isset($results[$i][$ldap_result_jobtitle][0]) ? $results[$i][$ldap_result_jobtitle][0] : '';
$item['country'] = isset($results[$i][$ldap_result_country][0]) ? $results[$i][$ldap_result_country][0] : '';
$item['department'] = isset($results[$i][$ldap_result_dept][0]) ? $results[$i][$ldap_result_dept][0] : '';
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_result_location && $item['location']) {
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
}
$department = Department::firstOrCreate([
'name' => $item['department'],
]);
$user = User::where('username', $item['username'])->first();
if ($user) {
// Updating an existing user.
@@ -250,101 +197,24 @@ class LdapSync extends Command
} else {
// Creating a new user.
$user = new User;
$user->password = $user->noPassword();
$user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below)
$user->password = $pass;
$user->activated = 0;
$item['createorupdate'] = 'created';
}
//If a sync option is not filled in on the LDAP settings don't populate the user field
if($ldap_result_username != null){
$user->username = $item['username'];
}
if($ldap_result_last_name != null){
$user->last_name = $item['lastname'];
}
if($ldap_result_first_name != null){
$user->first_name = $item['firstname'];
}
if($ldap_result_emp_num != null){
$user->employee_num = e($item['employee_number']);
}
if($ldap_result_email != null){
$user->last_name = $item['lastname'];
$user->username = $item['username'];
$user->email = $item['email'];
}
if($ldap_result_phone != null){
$user->employee_num = e($item['employee_number']);
$user->phone = $item['telephone'];
}
if($ldap_result_jobtitle != null){
$user->jobtitle = $item['jobtitle'];
}
if($ldap_result_country != null){
$user->country = $item['country'];
}
if($ldap_result_dept != null){
$user->department_id = $department->id;
}
if($ldap_result_location != null){
$user->location_id = $location ? $location->id : null;
}
if($ldap_result_manager != null){
if($item['manager'] != null) {
// Check Cache first
if (isset($manager_cache[$item['manager']])) {
// found in cache; use that and avoid extra lookups
$user->manager_id = $manager_cache[$item['manager']];
} else {
// Get the LDAP Manager
try {
$ldap_manager = Ldap::findLdapUsers($item['manager'], -1, $this->option('filter'));
} catch (\Exception $e) {
Log::warning("Manager lookup caused an exception: " . $e->getMessage() . ". Falling back to direct username lookup");
// Hail-mary for Okta manager 'shortnames' - will only work if
// Okta configuration is using full email-address-style usernames
$ldap_manager = [
"count" => 1,
0 => [
$ldap_result_username => [$item['manager']]
]
];
}
if ($ldap_manager["count"] > 0) {
// Get the Manager's username
// PHP LDAP returns every LDAP attribute as an array, and 90% of the time it's an array of just one item. But, hey, it's an array.
$ldapManagerUsername = $ldap_manager[0][$ldap_result_username][0];
// Get User from Manager username.
$ldap_manager = User::where('username', $ldapManagerUsername)->first();
if ($ldap_manager && isset($ldap_manager->id)) {
// Link user to manager id.
$user->manager_id = $ldap_manager->id;
}
}
$manager_cache[$item['manager']] = $ldap_manager && isset($ldap_manager->id) ? $ldap_manager->id : null; // Store results in cache, even if 'failed'
}
}
}
// Sync activated state for Active Directory.
if ( !empty($ldap_result_active_flag)) { // IF we have an 'active' flag set....
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_result_active_flag][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool)$raw_value;
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
} elseif (array_key_exists('useraccountcontrol', $results[$i]) ) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
/* The following is _probably_ the correct logic, but we can't use it because
if (array_key_exists('useraccountcontrol', $results[$i])) {
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
to allow to log in.
@@ -361,25 +231,25 @@ class LdapSync extends Command
$user->activated = 0;
} */
$enabled_accounts = [
'512', // 0x200 NORMAL_ACCOUNT
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
'512', // 0x200 NORMAL_ACCOUNT
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
'1114624', // 0x110200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, NOT_DELEGATED,
];
$user->activated = (in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts)) ? 1 : 0;
}
// If we're not using AD, and there isn't an activated flag set, activate all users
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
already-existing accounts will be however the administrator has set them */
elseif (empty($ldap_result_active_flag)) {
$user->activated = 1;
}
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
@@ -390,7 +260,7 @@ class LdapSync extends Command
$user->location_id = $location->id;
}
}
$location = null;
$user->ldap_import = 1;
$errors = '';
@@ -398,10 +268,6 @@ class LdapSync extends Command
if ($user->save()) {
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ( $item['createorupdate'] === 'created' && $ldap_default_group) {
$user->groups()->attach($ldap_default_group);
}
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
@@ -411,6 +277,7 @@ class LdapSync extends Command
}
array_push($summary, $item);
}
}
if ($this->option('summary')) {

View File

@@ -5,7 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Setting;
use Exception;
use Illuminate\Support\Facades\Crypt;
use Crypt;
/**
* Check if a given ip is in a network
@@ -27,19 +27,6 @@ function ip_in_range( $ip, $range ) {
}
// NOTE - this function was shamelessly stolen from this gist: https://gist.github.com/tott/7684443
/**
* Ensure LDAP filters are parentheses-wrapped
*/
function parenthesized_filter($filter)
{
if(substr($filter,0,1) == "(" ) {
return $filter;
} else {
return "(".$filter.")";
}
}
class LdapTroubleshooter extends Command
{
/**
@@ -83,47 +70,6 @@ class LdapTroubleshooter extends Command
}
}
/**
* Clean the results from ldap_get_entries into something useful
* @param array $array
* @return array
*/
public function ldap_results_cleaner ($array) {
$cleaned = [];
for($i = 0; $i < $array['count']; $i++) {
$row = $array[$i];
$clean_row = [];
foreach($row AS $key => $val ) {
$this->debugout("Key is: ".$key);
if($key == "count" || is_int($key) || $key == "dn") {
$this->debugout(" and we're gonna skip it\n");
continue;
}
$this->debugout(" And that seems fine.\n");
if(array_key_exists('count',$val)) {
if($val['count'] == 1) {
$clean_row[$key] = $val[0];
} else {
unset($val['count']); //these counts are annoying
$elements = [];
foreach($val as $entry) {
if(isset($ldap_constants[$entry])) {
$elements[] = $ldap_constants[$entry];
} else {
$elements[] = $entry;
}
}
$clean_row[$key] = $elements;
}
} else {
$clean_row[$key] = $val;
}
}
$cleaned[$i] = $clean_row;
}
return $cleaned;
}
/**
* Execute the console command.
*
@@ -156,12 +102,16 @@ class LdapTroubleshooter extends Command
$output[] = "LDAPTLS_KEY=storage/ldap_client_tls.key";
}
$output[] = "ldapsearch";
$output[] = "-H ".$settings->ldap_server;
$output[] = $settings->ldap_server;
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
$output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
$output[] = "-w ".escapeshellarg(\Crypt::Decrypt($settings->ldap_pword));
if(substr($settings->ldap_filter,0,1) == "(" ) {
$output[] = escapeshellarg($settings->ldap_filter);
} else {
$output[] = escapeshellarg("(".$settings->ldap_filter.")");
}
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
$output[] = "-Z";
@@ -340,8 +290,45 @@ class LdapTroubleshooter extends Command
}
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
// recursive function that 'cleans' the returned array from ldap_get_entries which are formatted awfully
$cleaner = function ($array) {
$cleaned = [];
for($i = 0; $i < $array['count']; $i++) {
$row = $array[$i];
$clean_row = [];
foreach($row AS $key => $val ) {
$this->debugout("Key is: ".$key);
if($key == "count" || is_int($key) || $key == "dn") {
$this->debugout(" and we're gonna skip it\n");
continue;
}
$this->debugout(" And that seems fine.\n");
if(array_key_exists('count',$val)) {
if($val['count'] == 1) {
$clean_row[$key] = $val[0];
} else {
unset($val['count']); //these counts are annoying
$elements = [];
foreach($val as $entry) {
if(isset($ldap_constants[$entry])) {
$elements[] = $ldap_constants[$entry];
} else {
$elements[] = $entry;
}
}
$clean_row[$key] = $elements;
}
} else {
$clean_row[$key] = $val;
}
}
$cleaned[$i] = $clean_row;
}
return $cleaned;
};
foreach($ldap_urls AS $ldap_url) {
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword))) {
$this->info("Success getting informational bind!");
} else {
$this->error("Unable to get information from bind.");
@@ -435,9 +422,9 @@ class LdapTroubleshooter extends Command
});
}
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password,$settings)
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password, $settings) {
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password) {
try { // TODO - copypasta'ed from test_authed_bind
$conn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($conn, $username, $password);
@@ -448,13 +435,12 @@ class LdapTroubleshooter extends Command
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
$cleaned_results = $cleaner($results);
$this->line(print_r($cleaned_results,true));
//okay, great - now how do we display those results? I have no idea.
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->base_dn, $settings->filter);
$this->info("Printing first 10 results: ");
for($i=0;$i<10;$i++) {
$this->info($search_results[$i]);

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands;
use App\Events\UserMerged;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
@@ -42,20 +41,10 @@ class MergeUsersByUsername extends Command
{
// Get the list of users who have an email address as their username
$users = User::where('username', 'LIKE', '%@%')->whereNull('deleted_at')->get();
$this->info($users->count().' total non-deleted users whose usernames contain a @ symbol.');
foreach ($users as $user) {
$parts = explode('@', trim($user->username));
$this->info('Checking against username '.trim($parts[0]).'.');
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->get();
$parts = explode('@', $user->username);
$bad_users = User::where('username', '=', $parts[0])->whereNull('deleted_at')->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')->get();
foreach ($bad_users as $bad_user) {
$this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') ');
@@ -106,26 +95,10 @@ class MergeUsersByUsername extends Command
$managedLocation->save();
}
foreach ($bad_user->uploads as $upload) {
$this->info('Updating upload log record '.$upload->id.' to user '.$user->id);
$upload->item_id = $user->id;
$upload->save();
}
foreach ($bad_user->acceptances as $acceptance) {
$this->info('Updating acceptance log record '.$acceptance->id.' to user '.$user->id);
$acceptance->item_id = $user->id;
$acceptance->save();
}
// Mark the user as deleted
$this->info('Marking the user as deleted');
$bad_user->deleted_at = Carbon::now()->timestamp;
$bad_user->save();
event(new UserMerged($bad_user, $user, null));
}
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class MoveUploadsToNewDisk extends Command
{
@@ -47,121 +46,116 @@ class MoveUploadsToNewDisk extends Command
}
$delete_local = $this->argument('delete_local');
$public_uploads['accessories'] = glob('public/uploads/accessories'."/*.*");
$public_uploads['assets'] = glob('public/uploads/assets'."/*.*");
$public_uploads['avatars'] = glob('public/uploads/avatars'."/*.*");
$public_uploads['categories'] = glob('public/uploads/categories'."/*.*");
$public_uploads['companies'] = glob('public/uploads/companies'."/*.*");
$public_uploads['components'] = glob('public/uploads/components'."/*.*");
$public_uploads['consumables'] = glob('public/uploads/consumables'."/*.*");
$public_uploads['departments'] = glob('public/uploads/departments'."/*.*");
$public_uploads['locations'] = glob('public/uploads/locations'."/*.*");
$public_uploads['manufacturers'] = glob('public/uploads/manufacturers'."/*.*");
$public_uploads['suppliers'] = glob('public/uploads/suppliers'."/*.*");
$public_uploads['assetmodels'] = glob('public/uploads/models'."/*.*");
$public_uploads['accessories'] = glob('public/accessories'.'/*.*');
$public_uploads['assets'] = glob('public/assets'.'/*.*');
$public_uploads['avatars'] = glob('public/avatars'.'/*.*');
$public_uploads['categories'] = glob('public/categories'.'/*.*');
$public_uploads['companies'] = glob('public/companies'.'/*.*');
$public_uploads['components'] = glob('public/components'.'/*.*');
$public_uploads['consumables'] = glob('public/consumables'.'/*.*');
$public_uploads['departments'] = glob('public/departments'.'/*.*');
$public_uploads['locations'] = glob('public/locations'.'/*.*');
$public_uploads['manufacturers'] = glob('public/manufacturers'.'/*.*');
$public_uploads['suppliers'] = glob('public/suppliers'.'/*.*');
$public_uploads['assetmodels'] = glob('public/models'.'/*.*');
// iterate files
foreach ($public_uploads as $public_type => $public_upload) {
$type_count = 0;
$this->info('- There are ' . count($public_upload) . ' PUBLIC ' . $public_type . ' files.');
$this->info('- There are '.count($public_upload).' PUBLIC '.$public_type.' files.');
for ($i = 0; $i < count($public_upload); $i++) {
$type_count++;
$filename = basename($public_upload[$i]);
try {
Storage::disk('public')->put('uploads/'.$public_type.'/'.$filename, file_get_contents($public_upload[$i]));
try {
Storage::disk('public')->put('uploads/'.public_type.'/'.$filename, file_get_contents($public_upload[$i]));
$new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename);
$this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) {
Log::debug($e);
\Log::debug($e);
$this->error($e);
}
}
}
$logos = glob("public/uploads/setting*.*");
$this->info("- There are ".count($logos).' files that might be logos.');
$logos = glob('public/uploads/setting*.*');
$this->info('- There are '.count($logos).' files that might be logos.');
$type_count = 0;
foreach ($logos as $logo) {
$this->info($logo);
$type_count++;
$filename = basename($logo);
Storage::disk('public')->put('uploads/' . $filename, file_get_contents($logo));
$this->info($type_count . '. LOGO: ' . $filename . ' was copied to ' . env('PUBLIC_AWS_URL') . '/uploads/' . $filename);
Storage::disk('public')->put('uploads/'.$filename, file_get_contents($logo));
$this->info($type_count.'. LOGO: '.$filename.' was copied to '.env('PUBLIC_AWS_URL').'/uploads/'.$filename);
}
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
$private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*");
$private_uploads['assets'] = glob('storage/private_uploads/assets'.'/*.*');
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'.'/*.*');
$private_uploads['audits'] = glob('storage/private_uploads/audits'.'/*.*');
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'.'/*.*');
$private_uploads['imports'] = glob('storage/private_uploads/imports'.'/*.*');
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'.'/*.*');
$private_uploads['users'] = glob('storage/private_uploads/users'.'/*.*');
$private_uploads['backups'] = glob('storage/private_uploads/users'.'/*.*');
foreach ($private_uploads as $private_type => $private_upload) {
{
$this->info('- There are ' . count($private_upload) . ' PRIVATE ' . $private_type . ' files.');
$this->info('- There are '.count($private_upload).' PRIVATE '.$private_type.' files.');
$type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) {
$type_count++;
$filename = basename($private_upload[$x]);
$type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) {
$type_count++;
$filename = basename($private_upload[$x]);
try {
Storage::put($private_type . '/' . $filename, file_get_contents($private_upload[$i]));
$new_url = Storage::url($private_type . '/' . $filename, $filename);
$this->info($type_count . '. PRIVATE: ' . $filename . ' was copied to ' . $new_url);
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
try {
Storage::put($private_type.'/'.$filename, file_get_contents($private_upload[$i]));
$new_url = Storage::url($private_type.'/'.$filename, $filename);
$this->info($type_count.'. PRIVATE: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) {
\Log::debug($e);
$this->error($e);
}
}
}
if ($delete_local == 'true') {
$public_delete_count = 0;
$private_delete_count = 0;
if ($delete_local == 'true') {
$public_delete_count = 0;
$private_delete_count = 0;
$this->info("\n\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
$this->warn("\nTHIS WILL DELETE ALL OF YOUR LOCAL UPLOADED FILES. \n\nThis cannot be undone, so you should take a backup of your system before you proceed.\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
$this->info("\n\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
$this->warn("\nTHIS WILL DELETE ALL OF YOUR LOCAL UPLOADED FILES. \n\nThis cannot be undone, so you should take a backup of your system before you proceed.\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
if ($this->confirm('Do you wish to continue?')) {
foreach ($public_uploads as $public_type => $public_upload) {
for ($i = 0; $i < count($public_upload); $i++) {
$filename = $public_upload[$i];
try {
unlink($filename);
$public_delete_count++;
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
if ($this->confirm('Do you wish to continue?')) {
foreach ($public_uploads as $public_type => $public_upload) {
for ($i = 0; $i < count($public_upload); $i++) {
$filename = $public_upload[$i];
try {
unlink($filename);
$public_delete_count++;
} catch (\Exception $e) {
\Log::debug($e);
$this->error($e);
}
}
foreach ($private_uploads as $private_type => $private_upload) {
for ($i = 0; $i < count($private_upload); $i++) {
$filename = $private_upload[$i];
try {
unlink($filename);
$private_delete_count++;
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
}
$this->info($public_delete_count . ' PUBLIC local files and ' . $private_delete_count . ' PRIVATE local files were deleted from your filesystem.');
}
foreach ($private_uploads as $private_type => $private_upload) {
for ($i = 0; $i < count($private_upload); $i++) {
$filename = $private_upload[$i];
try {
unlink($filename);
$private_delete_count++;
} catch (\Exception $e) {
\Log::debug($e);
$this->error($e);
}
}
}
$this->info($public_delete_count.' PUBLIC local files and '.$private_delete_count.' PRIVATE local files were deleted from your filesystem.');
}
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
class NormalizeUserNames extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:normalize-names';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Normalizes weirdly formatted names as first-letter upercased';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$users = User::get();
$this->info($users->count() . ' users');
foreach ($users as $user) {
$user->first_name = ucwords(strtolower($user->first_name));
$user->last_name = ucwords(strtolower($user->last_name));
$user->email = strtolower($user->email);
$user->save();
}
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Facades\Log;
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
@@ -60,7 +59,7 @@ class ObjectImportCommand extends Command
// This $logFile/useFiles() bit is currently broken, so commenting it out for now
// $logFile = $this->option('logfile');
// Log::useFiles($logFile);
// \Log::useFiles($logFile);
$this->comment('======= Importing Items from '.$filename.' =========');
$importer->import();
@@ -113,10 +112,10 @@ class ObjectImportCommand extends Command
public function log($string, $level = 'info')
{
if ($level === 'warning') {
Log::warning($string);
\Log::warning($string);
$this->comment($string);
} else {
Log::Info($string);
\Log::Info($string);
if ($this->option('verbose')) {
$this->comment($string);
}

View File

@@ -5,7 +5,7 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Schema;
use Illuminate\Support\Facades\DB;
use DB;
use Illuminate\Console\Command;
class PaveIt extends Command

View File

@@ -71,31 +71,22 @@ class ReEncodeCustomFieldNames extends Command
*/
$last_part = substr(strrchr($asset_column, '_snipeit_'), 1);
$custom_field_columns[$last_part] = $asset_column;
}
}
foreach ($fields as $field) {
$this->info($field->name.' ('.$field->id.') column should be '.$field->convertUnicodeDbSlug());
$this->info($field->name.' ('.$field->id.') column should be '.$field->convertUnicodeDbSlug().'');
/** The assets table has the column it should have, all is well */
if ($field->db_column == $field->convertUnicodeDbSlug() && \Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
$this->info('-- ✓ This field exists on the assets table and the value for db_column matches in the custom_fields table.');
if (\Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
$this->info('-- ✓ This field exists - all good');
/**
* There is a mismatch between the fieldname on the assets table and
* what $field->convertUnicodeDbSlug() is *now* expecting.
*/
} else {
if ($field->db_column != $field->convertUnicodeDbSlug()) {
$this->error('-- ✘ Field mismatch: '.$field->name.' value should be '.$field->convertUnicodeDbSlug().' but is '.$field->db_column.' in the custom_fields table');
} else {
$this->error('-- ✘ Field mismatch: '.$field->name.' column should be '.$field->convertUnicodeDbSlug().' but is '.$custom_field_columns[$field->id].' on the assets table.');
}
$this->warn('-- X Field mismatch: updating... ');
/** Make sure the custom_field_columns array has the ID */
if (array_key_exists($field->id, $custom_field_columns)) {
@@ -104,19 +95,13 @@ class ReEncodeCustomFieldNames extends Command
* Update the asset schema to the corrected fieldname that will be recognized by the
* system elsewhere that we use $field->convertUnicodeDbSlug()
*/
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the assets table');
\Schema::table('assets', function ($table) use ($custom_field_columns, $field) {
$table->renameColumn($custom_field_columns[$field->id], $field->convertUnicodeDbSlug());
});
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the custom fields table');
$field->db_column = $field->convertUnicodeDbSlug();
$field->save();
$this->warn('-- ✓ Field updated from '.$custom_field_columns[$field->id].' to '.$field->convertUnicodeDbSlug());
} else {
$this->warn('-- WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
$this->warn('-- X WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
}
}

View File

@@ -103,7 +103,7 @@ class RecryptFromMcrypt extends Command
$this->comment('INFO: No LDAP password found. Skipping... ');
} else {
$decrypted_ldap_pword = $mcrypter->decrypt($settings->ldap_pword);
$settings->ldap_pword = Crypt::encrypt($decrypted_ldap_pword);
$settings->ldap_pword = \Crypt::encrypt($decrypted_ldap_pword);
$settings->save();
}
/** @var CustomField[] $custom_fields */
@@ -132,7 +132,7 @@ class RecryptFromMcrypt extends Command
// Try to decrypt the payload using the legacy app key
try {
$decrypted_field = $mcrypter->decrypt($asset->{$columnName});
$asset->{$columnName} = Crypt::encrypt($decrypted_field);
$asset->{$columnName} = \Crypt::encrypt($decrypted_field);
$this->comment($decrypted_field);
} catch (\Exception $e) {
$errors[] = ' - ERROR: Could not decrypt field ['.$encrypted_field->name.']: '.$e->getMessage();

View File

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Artisan;
use Illuminate\Console\Command;
class RegenerateAssetTags extends Command
@@ -60,7 +60,7 @@ class RegenerateAssetTags extends Command
}
foreach ($total_assets as $asset) {
$start_tag++;
$output['info'][] = 'Asset tag:'.$asset->asset_tag;
$asset->asset_tag = $settings->auto_increment_prefix.$settings->auto_increment_prefix.$start_tag;
@@ -72,15 +72,8 @@ class RegenerateAssetTags extends Command
// Use forceSave here to override model level validation
$asset->forceSave();
$start_tag++;
if ($bar) {
$bar->advance();
}
}
$settings->next_auto_tag_base = Asset::zerofill($start_tag, $settings->zerofill_count);
$settings->save();
$bar->finish();
$this->info("\n");

View File

@@ -48,7 +48,6 @@ class ResetDemoSettings extends Command
$settings->auto_increment_assets = 1;
$settings->logo = 'snipe-logo.png';
$settings->alert_email = 'service@snipe-it.io';
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = null;
$settings->barcode_type = 'QRCODE';
$settings->default_currency = 'USD';
@@ -63,7 +62,7 @@ class ResetDemoSettings extends Command
$settings->date_display_format = 'D M d, Y';
$settings->time_display_format = 'g:iA';
$settings->thumbnail_max_h = '30';
$settings->locale = 'en-US';
$settings->locale = 'en';
$settings->version_footer = 'on';
$settings->support_footer = null;
$settings->saml_enabled = '0';
@@ -78,7 +77,7 @@ class ResetDemoSettings extends Command
$settings->save();
if ($user = User::where('username', '=', 'admin')->first()) {
$user->locale = 'en-US';
$user->locale = 'en';
$user->save();
}

View File

@@ -6,8 +6,8 @@ use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\License;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Artisan;
use DB;
use Illuminate\Console\Command;
class RestoreDeletedUsers extends Command

View File

@@ -4,152 +4,6 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use ZipArchive;
use Illuminate\Support\Facades\Log;
class SQLStreamer {
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
private bool $reading_beginning_of_line = true;
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
if($this->statement_is_permitted && $line[0] === ' ') {
return $line;
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
// ^^^^^^ that bit should *exit* the 'perimitted' black
];
foreach($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line;
}
}
// all that is not allowed is denied.
return "";
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command
{
@@ -158,13 +12,10 @@ class RestoreFromBackup extends Command
*
* @var string
*/
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar}
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
{--force : Skip the danger prompt; assuming you hit "y"}
{filename : The full path of the .zip file to be migrated}
{--no-progress : Don\'t show a progress bar}';
/**
* The console command description.
@@ -183,6 +34,8 @@ class RestoreFromBackup extends Command
parent::__construct();
}
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
/**
* Execute the console command.
*
@@ -192,7 +45,7 @@ class RestoreFromBackup extends Command
{
$dir = getcwd();
if( $dir != base_path() ) { // usually only the case when running via webserver, not via command-line
Log::debug("Current working directory is: $dir, changing directory to: ".base_path());
\Log::debug("Current working directory is: $dir, changing directory to: ".base_path());
chdir(base_path()); // TODO - is this *safe* to change on a running script?!
}
//
@@ -202,7 +55,7 @@ class RestoreFromBackup extends Command
return $this->error('Missing required filename');
}
if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
if (! $this->option('force') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
return $this->error('Data loss not confirmed');
}
@@ -231,36 +84,35 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/assetmodels',
'storage/private_uploads/users',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
];
$private_files = [
'storage/oauth-private.key',
'storage/oauth-public.key',
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
'public/uploads/categories',
'public/uploads/manufacturers',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/consumables',
'public/uploads/departments',
'public/uploads/locations',
'public/uploads/manufacturers',
'public/uploads/models',
'public/uploads/avatars',
'public/uploads/suppliers',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/locations',
'public/uploads/accessories',
'public/uploads/models',
'public/uploads/categories',
'public/uploads/avatars',
'public/uploads/manufacturers',
];
$public_files = [
@@ -297,19 +149,19 @@ class RestoreFromBackup extends Command
$boring_files[] = $raw_path;
continue;
}
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
Log::debug("Found a sql file!");
if (@pathinfo($raw_path)['extension'] == 'sql') {
\Log::debug("Found a sql file!");
$sqlfiles[] = $raw_path;
$sqlfile_indices[] = $i;
continue;
}
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
$last_pos = strrpos($raw_path, $dir . '/');
$last_pos = strrpos($raw_path, $dir.'/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
continue 2;
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
// we don't care about that; we just want files with the appropriate prefix
@@ -318,7 +170,7 @@ class RestoreFromBackup extends Command
}
}
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', ];
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
@@ -327,8 +179,8 @@ class RestoreFromBackup extends Command
$last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (!in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
if (! in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file '.$raw_path.' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
@@ -343,6 +195,7 @@ class RestoreFromBackup extends Command
}
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) {
@@ -354,17 +207,6 @@ class RestoreFromBackup extends Command
//older Snipe-IT installs don't have the db-dumps subdirectory component
}
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
//how to invoke the restore?
$pipes = [];
@@ -372,7 +214,7 @@ class RestoreFromBackup extends Command
$env_vars['MYSQL_PWD'] = config('database.connections.mysql.password');
// TODO notes: we are stealing the dump_binary_path (which *probably* also has your copy of the mysql binary in it. But it might not, so we might need to extend this)
// we unilaterally prepend a slash to the `mysql` command. This might mean your path could look like /blah/blah/blah//mysql - which should be fine. But maybe in some environments it isn't?
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? ".exe" : "");
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').'/mysql';
if( ! file_exists($mysql_binary) ) {
return $this->error("mysql tool at: '$mysql_binary' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary");
}
@@ -385,10 +227,6 @@ class RestoreFromBackup extends Command
return $this->error('Unable to invoke mysql via CLI');
}
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
// $this->info("Stdout says? ".fgets($pipes[1])); //FIXME: I think we might need to set non-blocking mode to use this properly?
// $this->info("Stderr says? ".fgets($pipes[2])); //FIXME: ditto, same.
// should we read stdout?
@@ -396,9 +234,9 @@ class RestoreFromBackup extends Command
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
// FIXME - this feels like it wants to go somewhere else?
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
// why do we care what's happening with pipes and stdout and stderr?!
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']);
if ($sql_contents === false) {
$stdout = fgets($pipes[1]);
$this->info($stdout);
@@ -407,35 +245,22 @@ class RestoreFromBackup extends Command
return false;
}
$bytes_read = 0;
try {
if ( $this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer);
while (($buffer = fgets($sql_contents, self::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
}
}
} else {
$sql_importer = new SQLStreamer($sql_contents, $pipes[0], $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
if ($bytes_written === false) {
$stdout = fgets($pipes[1]);
$this->info($stdout);
$stderr = fgets($pipes[2]);
$this->info($stderr);
return false;
}
} catch (\Exception $e) {
Log::error("Error during restore!!!! ".$e->getMessage());
// FIXME - put these back and/or put them in the right places?!
$err_out = fgets($pipes[1]);
$err_err = fgets($pipes[2]);
Log::error("Error OUTPUT: ".$err_out);
$this->info($err_out);
Log::error("Error ERROR : ".$err_err);
$this->error($err_err);
throw $e;
}
if (!feof($sql_contents) || $bytes_read == 0) {
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
}
@@ -467,7 +292,7 @@ class RestoreFromBackup extends Command
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
while (($buffer = fgets($fp, self::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);

View File

@@ -5,9 +5,8 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Artisan;
use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Encryption\Encrypter;
class RotateAppKey extends Command
@@ -17,17 +16,14 @@ class RotateAppKey extends Command
*
* @var string
*/
protected $signature = 'snipeit:rotate-key
{previous_key? : The previous key to rotate from}
{--emergency : Emergency mode - rotate from .env APP_KEY to newly-generated one, modifying .env}
{--force : Skip interactive confirmation}';
protected $signature = 'snipeit:rotate-key';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rotates APP_KEY to a new value, optionally taking the previous key as an argument';
protected $description = 'Command description';
/**
* Create a new command instance.
@@ -46,42 +42,26 @@ class RotateAppKey extends Command
*/
public function handle()
{
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
if ( (!$this->option('emergency') && !$this->argument('previous_key')) || ( $this->option('emergency') && $this->argument('previous_key'))) {
$this->error("Specify only one of --emergency, or an app key value, in order to rotate keys");
return 1;
}
if ( $this->option('emergency') ) {
$msg = "\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ";
} else {
$msg = "\n****************************************************\nTHIS WILL DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND RE-ENCRYPT THEM WITH YOUR\nAPP_KEY.\n\nThere is NO undo. \n\nMake SURE you have a database backup BEFORE running this command. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup? ";
}
if ($this->option('force') || $this->confirm($msg)) {
if ($this->confirm("\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ")) {
// Get the existing app_key and ciphers
// We put them in a variable since we clear the cache partway through here.
if ($this->option('emergency')) {
$old_app_key = config('app.key');
$cipher = config('app.cipher');
$old_app_key = config('app.key');
$cipher = config('app.cipher');
// Generate a new one
Artisan::call('key:generate', ['--show' => true]);
$new_app_key = trim(Artisan::output());
// Generate a new one
Artisan::call('key:generate', ['--show' => true]);
$new_app_key = Artisan::output();
// Clear the config cache
Artisan::call('config:clear');
// Clear the config cache
Artisan::call('config:clear');
// Write the new app key to the .env file
$this->writeNewEnvironmentFileWith($new_app_key);
} elseif ($this->argument('previous_key')) {
$old_app_key = $this->argument('previous_key');
$cipher = config('app.cipher'); // just a guess?
$new_app_key = config('app.key');
}
$this->warn('Your app cipher is: '.$cipher);
$this->warn('Your old APP_KEY is: '.$old_app_key);
$this->warn('Your new APP_KEY is: '.$new_app_key);
$this->warn('Your app cipher is: ' . $cipher);
$this->warn('Your old APP_KEY is: ' . $old_app_key);
$this->warn('Your new APP_KEY is: ' . $new_app_key);
// Write the new app key to the .env file
$this->writeNewEnvironmentFileWith($new_app_key);
// Manually create an old encrypter instance using the old app key
// and also create a new encrypter instance so we can re-crypt the field
@@ -95,16 +75,8 @@ class RotateAppKey extends Command
$assets = Asset::whereNotNull($field->db_column)->get();
foreach ($assets as $asset) {
try {
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
$this->line('DECRYPTED: ' . $field->db_column);
} catch (DecryptException $e) {
$this->line('Could not decrypt '. $field->db_column.' using "old key" - skipping...');
continue;
} catch (\Exception $e) {
$this->error("Error decrypting ".$field->db_column.", reason: ".$e->getMessage().". Aborting key rotation");
throw $e;
}
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
$this->line('DECRYPTED: '.$field->db_column);
$asset->{$field->db_column} = $newEncrypter->encrypt($asset->{$field->db_column});
$this->line('ENCRYPTED: '.$field->db_column);
$asset->save();
@@ -114,14 +86,10 @@ class RotateAppKey extends Command
// Handle the LDAP password if one is provided
$setting = Setting::first();
if ($setting->ldap_pword != '') {
try {
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
$setting->save();
$this->warn('LDAP password has been re-encrypted.');
} catch(DecryptException $e) {
$this->warn("Unable to decrypt old LDAP password; skipping");
}
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
$setting->save();
$this->warn('LDAP password has been re-encrypted.');
}
} else {
$this->info('This operation has been canceled. No changes have been made.');
@@ -138,7 +106,7 @@ class RotateAppKey extends Command
{
file_put_contents($this->laravel->environmentFilePath(), preg_replace(
$this->keyReplacementPattern(),
'APP_KEY="'.$key.'"',
'APP_KEY='.$key,
file_get_contents($this->laravel->environmentFilePath())
));
}
@@ -150,7 +118,7 @@ class RotateAppKey extends Command
*/
protected function keyReplacementPattern()
{
$escaped = '="?'.preg_quote($this->laravel['config']['app.key'], '/').'"?';
$escaped = preg_quote('='.$this->laravel['config']['app.key'], '/');
return "/^APP_KEY{$escaped}/m";
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\SamlNonce;
class SamlClearExpiredNonces extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'saml:clear_expired_nonces';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clears out expired SAML assertions from the saml_nonces table';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
SamlNonce::where('not_valid_after','<=',now())->delete();
return 0;
}
}

View File

@@ -43,7 +43,7 @@ class SendCurrentInventoryToUsers extends Command
$count = 0;
foreach ($users as $user) {
if (($user->assets->count() > 0) || ($user->accessories->count() > 0) || ($user->licenses->count() > 0) || ($user->consumables->count() > 0)) {
if (($user->assets->count() > 0) || ($user->accessories->count() > 0) || ($user->licenses->count() > 0)) {
$count++;
$user->notify((new CurrentInventory($user)));
}

View File

@@ -42,31 +42,24 @@ class SendExpectedCheckinAlerts extends Command
public function handle()
{
$settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForCheckin($settings)->orderBy('assets.expected_checkin', 'desc')->get();
$this->info($assets->count().' assets must be checked in on or before '.$interval_date.' is deadline');
$whenNotify = Carbon::now()->addDays(7);
$assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get();
$this->info($whenNotify.' is deadline');
$this->info($assets->count().' assets');
foreach ($assets as $asset) {
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) {
$this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email);
$asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
if ($asset->assigned && $asset->checkedOutToUser()) {
$asset->assigned->notify((new ExpectedCheckinNotification($asset)));
}
}
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new AlertRecipient($item);
});
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
}
}
}

View File

@@ -3,11 +3,13 @@
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\License;
use App\Models\Recipients;
use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use DB;
use Illuminate\Console\Command;
class SendUpcomingAuditReport extends Command
@@ -44,24 +46,39 @@ class SendUpcomingAuditReport extends Command
public function handle()
{
$settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
$this->info($assets->count().' assets must be audited in on or before '.$interval_date.' is deadline');
if (($settings->alert_email != '') && ($settings->audit_warning_days) && ($settings->alerts_enabled == 1)) {
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new \App\Models\Recipients\AlertRecipient($item);
});
$this->info('Sending Admin SendUpcomingAuditNotification to: '.$settings->alert_email);
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
// Assets due for auditing
$assets = Asset::whereNotNull('next_audit_date')
->DueOrOverdueForAudit($settings)
->orderBy('last_audit_date', 'asc')->get();
if ($assets->count() > 0) {
$this->info(trans_choice('mail.upcoming-audits', $assets->count(),
['count' => $assets->count(), 'threshold' => $settings->audit_warning_days]));
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
$this->info('Audit report sent to '.$settings->alert_email);
} else {
$this->info('No assets to be audited. No report sent.');
}
} elseif ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (! $settings->audit_warning_days) {
$this->error('No audit warning days set in Admin Notifications. No mail will be sent.');
} elseif ($settings->alerts_enabled != 1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
} else {
$this->error('Something went wrong. :( ');
$this->error('Admin Notifications Email Setting: '.$settings->alert_email);
$this->error('Admin Audit Warning Setting: '.$settings->audit_warning_days);
$this->error('Admin Alerts Emnabled: '.$settings->alerts_enabled);
}
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Console\Commands;
use App\Models\Asset;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SyncAssetCounters extends Command
{
@@ -40,39 +39,33 @@ class SyncAssetCounters extends Command
public function handle()
{
$start = microtime(true);
// We need the whole count of all assets in order to set up the progress bar
$assets_count = Asset::withTrashed()->count();
$bar = $this->output->createProgressBar($assets_count);
$assets = Asset::withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')
->withTrashed()->chunk(100, function ($assets) use ($bar) {
->withTrashed()->get();
if ($assets->count() > 0) {
if ($assets) {
if ($assets->count() > 0) {
$bar = $this->output->createProgressBar($assets->count());
foreach ($assets as $asset) {
foreach ($assets as $asset) {
$asset->checkin_counter = (int) $asset->checkins_count;
$asset->checkout_counter = (int) $asset->checkouts_count;
$asset->requests_counter = (int) $asset->user_requests_count;
$asset->unsetEventDispatcher();
$asset->save();
$output['info'][] = 'Asset: '.$asset->id.' has '.$asset->checkin_counter.' checkins, '.$asset->checkout_counter.' checkouts, and '.$asset->requests_counter.' requests';
$bar->advance();
}
$bar->finish();
$asset->checkin_counter = (int) $asset->checkins_count;
$asset->checkout_counter = (int) $asset->checkouts_count;
$asset->requests_counter = (int) $asset->user_requests_count;
$asset->unsetEventDispatcher();
$asset->save();
$bar->advance();
Log::debug('Asset: '.$asset->id.' has '.$asset->checkin_counter.' checkins, '.$asset->checkout_counter.' checkouts, and '.$asset->requests_counter.' requests');
}
foreach ($output['info'] as $key => $output_text) {
$this->info($output_text);
}
$time_elapsed_secs = microtime(true) - $start;
$this->info('Sync executed in '.$time_elapsed_secs.' seconds');
} else {
$this->info('No assets to sync');
}
});
$bar->finish();
$time_elapsed_secs = microtime(true) - $start;
$this->info("\nSync of ".$assets_count.' assets executed in '.$time_elapsed_secs.' seconds');
}
}
}

View File

@@ -11,7 +11,7 @@ class SystemBackup extends Command
*
* @var string
*/
protected $signature = 'snipeit:backup {--filename=}';
protected $name = 'snipeit:backup';
/**
* The console command description.
@@ -37,18 +37,7 @@ class SystemBackup extends Command
*/
public function handle()
{
if ($this->option('filename')) {
$filename = $this->option('filename');
// Make sure the filename ends in .zip
if (!ends_with($filename, '.zip')) {
$filename = $filename.'.zip';
}
$this->call('backup:run', ['--filename' => $filename]);
} else {
$this->call('backup:run');
}
//
$this->call('backup:run');
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ToggleCustomfieldEncryption extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:customfield-encryption
{fieldname : the db_column_name of the field}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command should be used to convert an unencrypted custom field into a custom field and encrypt the associated data in the assets table for that column.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$fieldname = $this->argument('fieldname');
if ($field = CustomField::where('db_column', $fieldname)->first()) {
// If the field is not encrypted, make it encrypted and encrypt the data in the assets table for the
// corresponding field.
DB::transaction(function () use ($field) {
if ($field->field_encrypted == 0) {
$assets = Asset::whereNotNull($field->db_column)->get();
foreach ($assets as $asset) {
$asset->{$field->db_column} = encrypt($asset->{$field->db_column});
$asset->save();
}
$field->field_encrypted = 1;
$field->save();
// This field is already encrypted. Do nothing.
} else {
$this->error('The custom field ' . $field->db_column.' is already encrypted. No action was taken.');
}
});
// No matching column name found
} else {
$this->error('No matching results for unencrypted custom fields with db_column name: ' . $fieldname.'. Please check the fieldname.');
}
}
}

View File

@@ -24,8 +24,6 @@ class Kernel extends ConsoleKernel
$schedule->command('snipeit:backup')->weekly();
$schedule->command('backup:clean')->daily();
$schedule->command('snipeit:upcoming-audits')->daily();
$schedule->command('auth:clear-resets')->everyFifteenMinutes();
$schedule->command('saml:clear_expired_nonces')->weekly();
}
/**

View File

@@ -15,20 +15,18 @@ class CheckoutableCheckedIn
public $checkedInBy;
public $note;
public $action_date; // Date setted in the hardware.checkin view at the checkin_at input, for the action log
public $originalValues;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null, $originalValues = [])
public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null)
{
$this->checkoutable = $checkoutable;
$this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy;
$this->note = $note;
$this->action_date = $action_date ?? date('Y-m-d');
$this->originalValues = $originalValues;
}
}

View File

@@ -14,19 +14,17 @@ class CheckoutableCheckedOut
public $checkedOutTo;
public $checkedOutBy;
public $note;
public $originalValues;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [])
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note)
{
$this->checkoutable = $checkoutable;
$this->checkedOutTo = $checkedOutTo;
$this->checkedOutBy = $checkedOutBy;
$this->note = $note;
$this->originalValues = $originalValues;
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
class UserMerged
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $from_user, User $to_user, ?User $admin)
{
$this->merged_from = $from_user;
$this->merged_to = $to_user;
$this->admin = $admin;
}
}

View File

@@ -6,11 +6,10 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Helpers\Helper;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use Illuminate\Support\Facades\Log;
use Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
class Handler extends ExceptionHandler
{
@@ -29,8 +28,6 @@ class Handler extends ExceptionHandler
\Intervention\Image\Exception\NotSupportedException::class,
\League\OAuth2\Server\Exception\OAuthServerException::class,
JsonException::class,
SCIMException::class, //these generally don't need to be reported
InvalidFormatException::class,
];
/**
@@ -44,9 +41,7 @@ class Handler extends ExceptionHandler
public function report(Throwable $exception)
{
if ($this->shouldReport($exception)) {
if (class_exists(Log::class)) {
Log::error($exception);
}
Log::error($exception);
return parent::report($exception);
}
}
@@ -56,7 +51,7 @@ class Handler extends ExceptionHandler
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $e)
{
@@ -70,39 +65,18 @@ class Handler extends ExceptionHandler
// 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);
return response()->json(Helper::formatStandardApiResponse('error', null, 'invalid JSON'), 422);
}
// Handle SCIM exceptions
if ($e instanceof SCIMException) {
try {
$e->report(); // logs as 'debug', so shouldn't get too noisy
} catch(\Exception $reportException) {
//do nothing
}
return $e->render($request); // ALL SCIMExceptions have the 'render()' method
}
// Handle standard 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) {
return redirect()->back()->withInput()->with('error', trans('validation.date', ['attribute' => 'date']));
}
// Handle API requests that fail
// Handle Ajax requests that fail because the model doesn't exist
if ($request->ajax() || $request->wantsJson()) {
// 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) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.date', ['attribute' => 'date'])), 200);
}
// Handle API requests that fail because the model doesn't exist
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$className = last(explode('\\', $e->getModel()));
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)) {
$statusCode = $e->getStatusCode();
@@ -122,8 +96,6 @@ class Handler extends ExceptionHandler
}
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
return response()->view('layouts/basic', [
'content' => view('errors/404')
@@ -139,8 +111,8 @@ class Handler extends ExceptionHandler
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
*/
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
@@ -150,11 +122,6 @@ class Handler extends ExceptionHandler
return redirect()->guest('login');
}
protected function invalidJson($request, ValidationException $exception)
{
return response()->json(Helper::formatStandardApiResponse('error', null, $exception->errors()), 200);
}
/**
* A list of the inputs that are never flashed for validation exceptions.

View File

@@ -1,9 +1,8 @@
<?php
namespace App\Helpers;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\CustomField;
@@ -11,73 +10,12 @@ use App\Models\CustomFieldset;
use App\Models\Depreciation;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\License;
use Illuminate\Support\Facades\Crypt;
use Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Intervention\Image\ImageManagerStatic as Image;
use Illuminate\Support\Facades\Session;
use Image;
class Helper
{
/**
* This is only used for reversing the migration that updates the locale to the 5-6 letter codes from two
* letter codes. The normal dropdowns use the autoglossonyms in the language files located
* in resources/en-US/localizations.php.
*/
public static $language_map = [
'af' => 'af-ZA', // Afrikaans
'am' => 'am-ET', // Amharic
'ar' => 'ar-SA', // Arabic
'bg' => 'bg-BG', // Bulgarian
'ca' => 'ca-ES', // Catalan
'cs' => 'cs-CZ', // Czech
'cy' => 'cy-GB', // Welsh
'da' => 'da-DK', // Danish
'de-i' => 'de-if', // German informal
'de' => 'de-DE', // German
'el' => 'el-GR', // Greek
'en' => 'en-US', // English
'et' => 'et-EE', // Estonian
'fa' => 'fa-IR', // Persian
'fi' => 'fi-FI', // Finnish
'fil' => 'fil-PH', // Filipino
'fr' => 'fr-FR', // French
'he' => 'he-IL', // Hebrew
'hr' => 'hr-HR', // Croatian
'hu' => 'hu-HU', // Hungarian
'id' => 'id-ID', // Indonesian
'is' => 'is-IS', // Icelandic
'it' => 'it-IT', // Italian
'iu' => 'iu-NU', // Inuktitut
'ja' => 'ja-JP', // Japanese
'ko' => 'ko-KR', // Korean
'lt' => 'lt-LT', // Lithuanian
'lv' => 'lv-LV', // Latvian
'mi' => 'mi-NZ', // Maori
'mk' => 'mk-MK', // Macedonian
'mn' => 'mn-MN', // Mongolian
'ms' => 'ms-MY', // Malay
'nl' => 'nl-NL', // Dutch
'no' => 'no-NO', // Norwegian
'pl' => 'pl-PL', // Polish
'ro' => 'ro-RO', // Romanian
'ru' => 'ru-RU', // Russian
'sk' => 'sk-SK', // Slovak
'sl' => 'sl-SI', // Slovenian
'so' => 'so-SO', // Somali
'ta' => 'ta-IN', // Tamil
'th' => 'th-TH', // Thai
'tl' => 'tl-PH', // Tagalog
'tr' => 'tr-TR', // Turkish
'uk' => 'uk-UA', // Ukrainian
'vi' => 'vi-VN', // Vietnamese
'zu' => 'zu-ZA', // Zulu
];
/**
* Simple helper to invoke the markdown parser
*
@@ -85,23 +23,12 @@ class Helper
* @since [v2.0]
* @return string
*/
public static function parseEscapedMarkedown($str = null)
public static function parseEscapedMarkedown($str)
{
$Parsedown = new \Parsedown();
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->text($str);
}
}
public static function parseEscapedMarkedownInline($str = null)
{
$Parsedown = new \Parsedown();
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->line($str);
return $Parsedown->text(e($str));
}
}
@@ -133,14 +60,10 @@ class Helper
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.3]
* @return string
* @return array
*/
public static function defaultChartColors(int $index = 0)
public static function defaultChartColors($index = 0)
{
if ($index < 0) {
$index = 0;
}
$colors = [
'#008941',
'#FF4A46',
@@ -410,23 +333,7 @@ class Helper
'#92896B',
];
$total_colors = count($colors);
if ($index >= $total_colors) {
Log::info('Status label count is '.$index.' and exceeds the allowed count of 266.');
//patch fix for array key overflow (color count starts at 1, array starts at 0)
$index = $index - $total_colors - 1;
//constraints to keep result in 0-265 range. This should never be needed, but if something happens
//to create this many status labels and it DOES happen, this will keep it from failing at least.
if($index < 0) {
$index = 0;
}
elseif($index >($total_colors - 1)) {
$index = $total_colors - 1;
}
}
return $colors[$index];
}
@@ -620,23 +527,20 @@ class Helper
* @since [v2.5]
* @return array
*/
public static function categoryTypeList($selection=null)
public static function categoryTypeList()
{
$category_types = [
'' => '',
'accessory' => trans('general.accessory'),
'asset' => trans('general.asset'),
'consumable' => trans('general.consumable'),
'component' => trans('general.component'),
'license' => trans('general.license'),
'accessory' => 'Accessory',
'asset' => 'Asset',
'consumable' => 'Consumable',
'component' => 'Component',
'license' => 'License',
];
if ($selection != null){
return $category_types[strtolower($selection)];
}
else
return $category_types;
}
/**
* Get the list of custom fields in an array to make a dropdown menu
*
@@ -718,19 +622,17 @@ class Helper
*/
public static function checkLowInventory()
{
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get();
$components = Component::whereNotNull('min_amt')->get();
$asset_models = AssetModel::where('min_amt', '>', 0)->get();
$licenses = License::where('min_amt', '>', 0)->get();
$components = Component::withCount('assets as assets_count')->whereNotNull('min_amt')->get();
$avail_consumables = 0;
$items_array = [];
$all_count = 0;
foreach ($consumables as $consumable) {
$avail = $consumable->numRemaining();
if ($avail < ($consumable->min_amt) + $alert_threshold) {
if ($avail < ($consumable->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($consumable->qty > 0) {
$percent = number_format((($avail / $consumable->qty) * 100), 0);
} else {
@@ -749,7 +651,7 @@ class Helper
foreach ($accessories as $accessory) {
$avail = $accessory->qty - $accessory->users_count;
if ($avail < ($accessory->min_amt) + $alert_threshold) {
if ($avail < ($accessory->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($accessory->qty > 0) {
$percent = number_format((($avail / $accessory->qty) * 100), 0);
} else {
@@ -767,8 +669,8 @@ class Helper
}
foreach ($components as $component) {
$avail = $component->numRemaining();
if ($avail < ($component->min_amt) + $alert_threshold) {
$avail = $component->qty - $component->assets_count;
if ($avail < ($component->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($component->qty > 0) {
$percent = number_format((($avail / $component->qty) * 100), 0);
} else {
@@ -785,48 +687,6 @@ class Helper
}
}
foreach ($asset_models as $asset_model){
$asset = new Asset();
$total_owned = $asset->where('model_id', '=', $asset_model->id)->count();
$avail = $asset->where('model_id', '=', $asset_model->id)->whereNull('assigned_to')->count();
if ($avail < ($asset_model->min_amt) + $alert_threshold) {
if ($avail > 0) {
$percent = number_format((($avail / $total_owned) * 100), 0);
} else {
$percent = 100;
}
$items_array[$all_count]['id'] = $asset_model->id;
$items_array[$all_count]['name'] = $asset_model->name;
$items_array[$all_count]['type'] = 'models';
$items_array[$all_count]['percent'] = $percent;
$items_array[$all_count]['remaining'] = $avail;
$items_array[$all_count]['min_amt'] = $asset_model->min_amt;
$all_count++;
}
}
foreach ($licenses as $license){
$avail = $license->remaincount();
if ($avail < ($license->min_amt) + $alert_threshold) {
if ($avail > 0) {
$percent = number_format((($avail / $license->min_amt) * 100), 0);
} else {
$percent = 100;
}
$items_array[$all_count]['id'] = $license->id;
$items_array[$all_count]['name'] = $license->name;
$items_array[$all_count]['type'] = 'licenses';
$items_array[$all_count]['percent'] = $percent;
$items_array[$all_count]['remaining'] = $avail;
$items_array[$all_count]['min_amt'] = $license->min_amt;
$all_count++;
}
}
return $items_array;
}
@@ -844,7 +704,7 @@ class Helper
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif')) {
return $filetype;
}
@@ -877,15 +737,12 @@ class Helper
$permission_name = $permission[$x]['permission'];
if ($permission[$x]['display'] === true) {
if (is_array($selected_arr)) {
if ($selected_arr) {
if (array_key_exists($permission_name, $selected_arr)) {
$permissions_arr[$permission_name] = $selected_arr[$permission_name];
} else {
$permissions_arr[$permission_name] = '0';
}
} else {
$permissions_arr[$permission_name] = '0';
}
@@ -985,16 +842,6 @@ class Helper
return preg_replace('/\s+/u', '_', trim($string));
}
/**
* Return an array (or null) of the the raw and formatted date object for easy use in
* the API and the bootstrap table listings.
*
* @param $date
* @param $type
* @param $array
* @return array|string|null
*/
public static function getFormattedDateObject($date, $type = 'datetime', $array = true)
{
if ($date == '') {
@@ -1002,42 +849,21 @@ class Helper
}
$settings = Setting::getSettings();
$tmp_date = new \Carbon($date);
/**
* Wrap this in a try/catch so that if Carbon crashes, for example if the $date value
* isn't actually valid, we don't crash out completely.
*
* While this *shouldn't* typically happen since we validate dates before entering them
* into the database (and we use date/datetime fields for native fields in the system),
* it is a possible scenario that a custom field could be created as an "ANY" field, data gets
* added, and then the custom field format gets edited later. If someone put bad data in the
* database before then - or if they manually edited the field's value - it will crash.
*
*/
try {
$tmp_date = new Carbon($date);
if ($type == 'datetime') {
$dt['datetime'] = $tmp_date->format('Y-m-d H:i:s');
$dt['formatted'] = $tmp_date->format($settings->date_display_format.' '.$settings->time_display_format);
} else {
$dt['date'] = $tmp_date->format('Y-m-d');
$dt['formatted'] = $tmp_date->format($settings->date_display_format);
}
if ($array == 'true') {
return $dt;
}
return $dt['formatted'];
} catch (\Exception $e) {
Log::warning($e);
return $date.' (Invalid '.$type.' value.)';
if ($type == 'datetime') {
$dt['datetime'] = $tmp_date->format('Y-m-d H:i:s');
$dt['formatted'] = $tmp_date->format($settings->date_display_format.' '.$settings->time_display_format);
} else {
$dt['date'] = $tmp_date->format('Y-m-d');
$dt['formatted'] = $tmp_date->format($settings->date_display_format);
}
if ($array == 'true') {
return $dt;
}
return $dt['formatted'];
}
// Nicked from Drupal :)
@@ -1111,8 +937,6 @@ class Helper
'jpeg' => 'far fa-image',
'gif' => 'far fa-image',
'png' => 'far fa-image',
'webp' => 'far fa-image',
'avif' => 'far fa-image',
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
@@ -1148,8 +972,6 @@ class Helper
case 'jpeg':
case 'gif':
case 'png':
case 'webp':
case 'avif':
return true;
break;
default:
@@ -1237,265 +1059,4 @@ class Helper
return $file_name;
}
/**
* Universal helper to show file size in human-readable formats
*
* @author A. Gianotto <snipe@snipe.net>
* @since 5.0
*
* @return string[]
*/
public static function formatFilesizeUnits($bytes)
{
if ($bytes >= 1073741824)
{
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
}
elseif ($bytes >= 1048576)
{
$bytes = number_format($bytes / 1048576, 2) . ' MB';
}
elseif ($bytes >= 1024)
{
$bytes = number_format($bytes / 1024, 2) . ' KB';
}
elseif ($bytes > 1)
{
$bytes = $bytes . ' bytes';
}
elseif ($bytes == 1)
{
$bytes = $bytes . ' byte';
}
else
{
$bytes = '0 bytes';
}
return $bytes;
}
/**
* This is weird but used by the side nav to determine which URL to point the user to
*
* @author A. Gianotto <snipe@snipe.net>
* @since 5.0
*
* @return string[]
*/
public static function SettingUrls(){
$settings=['#','fields.index', 'statuslabels.index', 'models.index', 'categories.index', 'manufacturers.index', 'suppliers.index', 'departments.index', 'locations.index', 'companies.index', 'depreciations.index'];
return $settings;
}
/**
* Generic helper (largely used by livewire right now) that returns the font-awesome icon
* for the object type.
*
* @author A. Gianotto <snipe@snipe.net>
* @since 6.1.0
*
* @return string
*/
public static function iconTypeByItem($item) {
switch ($item) {
case 'asset':
return 'fas fa-barcode';
break;
case 'accessory':
return 'fas fa-keyboard';
break;
case 'component':
return 'fas fa-hdd';
break;
case 'consumable':
return 'fas fa-tint';
break;
case 'license':
return 'far fa-save';
break;
case 'location':
return 'fas fa-map-marker-alt';
break;
case 'user':
return 'fas fa-user';
break;
}
}
/*
* This is a shorter way to see if the app is in demo mode.
*
* This makes it cleanly available in blades and in controllers, e.g.
*
* Blade:
* {{ Helper::isDemoMode() ? ' disabled' : ''}} for form blades where we need to disable a form
*
* Controller:
* if (Helper::isDemoMode()) {
* // don't allow the thing
* }
* @todo - use this everywhere else in the app where we have very long if/else config('app.lock_passwords') stuff
*/
public static function isDemoMode() {
if (config('app.lock_passwords') === true) {
return true;
Log::debug('app locked!');
}
return false;
}
/**
* Conversion between units of measurement
*
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
* @since 5.0
* @param float $value Measurement value to convert
* @param string $srcUnit Source unit of measurement
* @param string $dstUnit Destination unit of measurement
* @param int $round Round the result to decimals (Default false - No rounding)
* @return float
*/
public static function convertUnit($value, $srcUnit, $dstUnit, $round=false) {
$srcFactor = static::getUnitConversionFactor($srcUnit);
$dstFactor = static::getUnitConversionFactor($dstUnit);
$output = $value * $srcFactor / $dstFactor;
return ($round !== false) ? round($output, $round) : $output;
}
/**
* Get conversion factor from unit of measurement to mm
*
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
* @since 5.0
* @param string $unit Unit of measurement
* @return float
*/
public static function getUnitConversionFactor($unit) {
switch (strtolower($unit)) {
case 'mm':
return 1.0;
case 'cm':
return 10.0;
case 'm':
return 1000.0;
case 'in':
return 25.4;
case 'ft':
return 12 * static::getUnitConversionFactor('in');
case 'yd':
return 3 * static::getUnitConversionFactor('ft');
case 'pt':
return (1 / 72) * static::getUnitConversionFactor('in');
default:
throw new \InvalidArgumentException('Unit: \'' . $unit . '\' is not supported');
return false;
}
}
/*
* I know it's gauche to return a shitty HTML string, but this is just a helper and since it will be the same every single time,
* it seemed pretty safe to do here. Don't you judge me.
*/
public static function showDemoModeFieldWarning() {
if (Helper::isDemoMode()) {
return "<p class=\"text-warning\"><i class=\"fas fa-lock\"></i>" . trans('general.feature_disabled') . "</p>";
}
}
/**
* Ah, legacy code.
*
* This corrects the original mistakes from 2013 where we used the wrong locale codes. Hopefully we
* can get rid of this in a future version, but this should at least give us the belt and suspenders we need
* to be sure this change is not too disruptive.
*
* In this array, we ONLY include the older languages where we weren't using the correct locale codes.
*
* @see public static $language_map in this file
* @author A. Gianotto <snipe@snipe.net>
* @since 6.3.0
*
* @param $language_code
* @return string []
*/
public static function mapLegacyLocale($language_code = null)
{
if (strlen($language_code) > 4) {
return $language_code;
}
foreach (self::$language_map as $legacy => $new) {
if ($language_code == $legacy) {
Log::debug('Current language is '.$legacy.', using '.$new.' instead');
return $new;
}
}
// Return US english if we don't have a match
return 'en-US';
}
public static function mapBackToLegacyLocale($new_locale = null)
{
if (strlen($new_locale) <= 4) {
return $new_locale; //"new locale" apparently wasn't quite so new
}
// This does a *reverse* search against our new language map array - given the value, find the *key* for it
$legacy_locale = array_search($new_locale, self::$language_map);
if($legacy_locale !== false) {
return $legacy_locale;
}
return $new_locale; // better that you have some weird locale that doesn't fit into our mappings anywhere than 'void'
}
static public function getRedirectOption($request, $id, $table, $asset_id = null)
{
$redirect_option = Session::get('redirect_option');
$checkout_to_type = Session::get('checkout_to_type');
//return to index
if ($redirect_option == '0') {
switch ($table) {
case "Assets":
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.checkout.success'));
}
}
//return to thing being assigned
if ($redirect_option == '1') {
switch ($table) {
case "Assets":
return redirect()->route('hardware.show', $id ? $id : $asset_id)->with('success', trans('admin/hardware/message.checkout.success'));
}
}
//return to thing being assigned to
if ($redirect_option == '2') {
switch ($checkout_to_type) {
case 'user':
return redirect()->route('users.show', $request->assigned_user)->with('success', trans('admin/hardware/message.checkout.success'));
case 'location':
return redirect()->route('locations.show', $request->assigned_location)->with('success', trans('admin/hardware/message.checkout.success'));
case 'asset':
return redirect()->route('hardware.show', $request->assigned_asset)->with('success', trans('admin/hardware/message.checkout.success'));
}
}
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
}
}

View File

@@ -9,9 +9,7 @@ use App\Models\Accessory;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Redirect;
use Illuminate\Support\Facades\Log;
/** This controller handles all actions related to Accessories for
* the Snipe-IT Asset Management application.
@@ -58,13 +56,12 @@ class AccessoriesController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param ImageUploadRequest $request
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function store(ImageUploadRequest $request)
{
$this->authorize(Accessory::class);
// create a new model instance
$accessory = new Accessory();
@@ -78,12 +75,13 @@ class AccessoriesController extends Controller
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = request('purchase_cost');
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
$accessory->qty = request('qty');
$accessory->user_id = Auth::user()->id;
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
$accessory = $request->handleImages($accessory);
// Was the accessory created?
@@ -116,34 +114,6 @@ class AccessoriesController extends Controller
}
/**
* Returns a view that presents a form to clone an accessory.
*
* @author [J. Vinsmoke]
* @param int $accessoryId
* @since [v6.0]
* @return View
*/
public function getClone($accessoryId = null)
{
$this->authorize('create', Accessory::class);
// Check if the asset exists
if (is_null($accessory_to_clone = Accessory::find($accessoryId))) {
// Redirect to the asset management page
return redirect()->route('accessories.index')
->with('error', trans('admin/accessories/message.does_not_exist', ['id' => $accessoryId]));
}
$accessory = clone $accessory_to_clone;
$accessory->id = null;
$accessory->location_id = null;
return view('accessories/edit')
->with('item', $accessory);
}
/**
* Save edited Accessory from form post
@@ -151,52 +121,39 @@ class AccessoriesController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param ImageUploadRequest $request
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(ImageUploadRequest $request, $accessoryId = null)
{
if ($accessory = Accessory::withCount('users as users_count')->find($accessoryId)) {
$this->authorize($accessory);
$validator = Validator::make($request->all(), [
"qty" => "required|numeric|min:$accessory->users_count"
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
// Update the accessory data
$accessory->name = request('name');
$accessory->location_id = request('location_id');
$accessory->min_amt = request('min_amt');
$accessory->category_id = request('category_id');
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->order_number = request('order_number');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = request('purchase_cost');
$accessory->qty = request('qty');
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
$accessory = $request->handleImages($accessory);
// Was the accessory updated?
if ($accessory->save()) {
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.update.success'));
}
} else {
if (is_null($accessory = Accessory::find($accessoryId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$this->authorize($accessory);
// Update the accessory data
$accessory->name = request('name');
$accessory->location_id = request('location_id');
$accessory->min_amt = request('min_amt');
$accessory->category_id = request('category_id');
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->order_number = request('order_number');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
$accessory->qty = request('qty');
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
$accessory = $request->handleImages($accessory);
// Was the accessory updated?
if ($accessory->save()) {
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
}
@@ -205,7 +162,7 @@ class AccessoriesController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($accessoryId)
@@ -225,7 +182,7 @@ class AccessoriesController extends Controller
try {
Storage::disk('public')->delete('accessories'.'/'.$accessory->image);
} catch (\Exception $e) {
Log::debug($e);
\Log::debug($e);
}
}
@@ -248,7 +205,7 @@ class AccessoriesController extends Controller
*/
public function show($accessoryID = null)
{
$accessory = Accessory::withCount('users as users_count')->find($accessoryID);
$accessory = Accessory::find($accessoryID);
$this->authorize('view', $accessory);
if (isset($accessory->id)) {
return view('accessories/view', compact('accessory'));

View File

@@ -1,159 +0,0 @@
<?php
namespace App\Http\Controllers\Accessories;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Accessory;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Accessory\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
class AccessoriesFilesController extends Controller
{
/**
* Validates and stores files associated with a accessory.
*
* @param UploadFileRequest $request
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $accessoryId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('accessories.show', ['accessory'=>$accessoryId])->with('error', trans('general.feature_disabled'));
}
$accessory = Accessory::find($accessoryId);
if (isset($accessory->id)) {
$this->authorize('accessories.files', $accessory);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/accessories')) {
Storage::makeDirectory('private_uploads/accessories', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
//Log the upload to the log
$accessory->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('accessories.show', $accessory->id)->with('success', trans('general.file_upload_success'));
}
return redirect()->route('accessories.show', $accessory->id)->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('accessories.index')
->with('error', trans('general.file_does_not_exist'));
}
/**
* Deletes the selected accessory file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $accessoryId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($accessoryId = null, $fileId = null)
{
$accessory = Accessory::find($accessoryId);
// the asset is valid
if (isset($accessory->id)) {
$this->authorize('update', $accessory);
$log = Actionlog::find($fileId);
// Remove the file if one exists
if (Storage::exists('accessories/'.$log->filename)) {
try {
Storage::delete('accessories/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the licence management page
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $accessoryId
* @param int $fileId
* @return \Symfony\Accessory\HttpFoundation\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($accessoryId = null, $fileId = null, $download = true)
{
Log::debug('Private filesystem is: '.config('filesystems.default'));
$accessory = Accessory::find($accessoryId);
// the accessory is valid
if (isset($accessory->id)) {
$this->authorize('view', $accessory);
$this->authorize('accessories.files', $accessory);
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
return redirect()->route('accessories.index')->with('error', trans('admin/users/message.log_record_not_found'));
}
$file = 'private_uploads/accessories/'.$log->filename;
if (Storage::missing($file)) {
Log::debug('FILE DOES NOT EXISTS for '.$file);
Log::debug('URL should be '.Storage::url($file));
return response('File '.$file.' ('.Storage::url($file).') not found on server', 404)
->header('Content-Type', 'text/plain');
} else {
// Display the file inline
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
// We have to override the URL stuff here, since local defaults in Laravel's Flysystem
// won't work, as they're not accessible via the web
if (config('filesystems.default') == 'local') { // TODO - is there any way to fix this at the StorageHelper layer?
return StorageHelper::downloader($file);
}
}
}
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -44,7 +44,7 @@ class AccessoryCheckinController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param null $accessoryUserId
* @param string $backto
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
* @internal param int $accessoryId
*/
@@ -60,10 +60,9 @@ class AccessoryCheckinController extends Controller
$this->authorize('checkin', $accessory);
$checkin_hours = date('H:i:s');
$checkin_at = date('Y-m-d H:i:s');
$checkin_at = date('Y-m-d');
if ($request->filled('checkin_at')) {
$checkin_at = $request->input('checkin_at').' '.$checkin_hours;
$checkin_at = $request->input('checkin_at');
}
// Was the accessory updated?

View File

@@ -18,36 +18,26 @@ class AccessoryCheckoutController extends Controller
* Return the form to checkout an Accessory to a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
* @param int $accessoryId
* @return View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create($id)
public function create($accessoryId)
{
if ($accessory = Accessory::withCount('users as users_count')->find($id)) {
$this->authorize('checkout', $accessory);
if ($accessory->category) {
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0){
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
// Return the checkout view
return view('accessories/checkout', compact('accessory'));
}
// Invalid category
return redirect()->route('accessories.edit', ['accessory' => $accessory->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.accessory')]));
// Check if the accessory exists
if (is_null($accessory = Accessory::find($accessoryId))) {
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
// Not found
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
if ($accessory->category) {
$this->authorize('checkout', $accessory);
// Get the dropdown of users and then pass it to the checkout view
return view('accessories/checkout', compact('accessory'));
}
return redirect()->back()->with('error', 'The category type for this accessory is not valid. Edit the accessory and select a valid accessory category.');
}
/**
@@ -59,29 +49,23 @@ class AccessoryCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function store(Request $request, $accessoryId)
{
// Check if the accessory exists
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
if (is_null($accessory = Accessory::find($accessoryId))) {
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.user_not_found'));
}
$this->authorize('checkout', $accessory);
if (!$user = User::find($request->input('assigned_to'))) {
return redirect()->route('accessories.checkout.show', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist'));
if (! $user = User::find($request->input('assigned_to'))) {
return redirect()->route('checkout/accessory', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist'));
}
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0){
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
// Update the accessory data
$accessory->assigned_to = e($request->input('assigned_to'));

View File

@@ -7,29 +7,13 @@ use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\Contracts\Acceptable;
use App\Models\Setting;
use App\Models\User;
use App\Models\AssetModel;
use App\Models\Accessory;
use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use phpDocumentor\Reflection\Types\Compound;
use Illuminate\Support\Facades\Log;
class AcceptanceController extends Controller
{
@@ -55,7 +39,6 @@ class AcceptanceController extends Controller
{
$acceptance = CheckoutAcceptance::find($id);
if (is_null($acceptance)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
}
@@ -69,7 +52,7 @@ class AcceptanceController extends Controller
}
if (! Company::isCurrentUserHasAccess($acceptance->checkoutable)) {
return redirect()->route('account.accept')->with('error', trans('general.error_user_company'));
return redirect()->route('account.accept')->with('error', trans('general.insufficient_permissions'));
}
return view('account/accept.create', compact('acceptance'));
@@ -80,7 +63,7 @@ class AcceptanceController extends Controller
*
* @param Request $request
* @param int $id
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
*/
public function store(Request $request, $id)
{
@@ -113,227 +96,29 @@ class AcceptanceController extends Controller
Storage::makeDirectory('private_uploads/signatures', 775);
}
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
$display_model = '';
$pdf_view_route = '';
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
$sig_filename='';
$sig_filename = '';
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-'.Str::uuid().'-'.date('Y-m-d-his').'.png';
$data_uri = e($request->input('signature_output'));
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/'.$sig_filename, (string) $decoded_image);
}
if ($request->input('asset_acceptance') == 'accepted') {
$acceptance->accept($sig_filename);
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
if (Setting::getSettings()->require_accept_signature == '1') {
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$pdf_view_route ='account.accept.accept-asset-eula';
$asset_model = AssetModel::find($item->model_id);
if (!$asset_model) {
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
$display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id);
$display_model = $license->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
// $pdf_view_route ='account.accept.accept-asset-eula';
// $asset_model = AssetModel::find($item->model_id);
// $display_model = $asset_model->name;
// $assigned_to = User::find($item->assigned_to)->present()->fullName;
//
// } elseif ($acceptance->checkoutable_type== 'App\Models\Accessory') {
// $pdf_view_route ='account.accept.accept-accessory-eula';
// $accessory = Accessory::find($item->id);
// $display_model = $accessory->name;
// $assigned_to = User::find($item->assignedTo);
//
// }
/**
* Gather the data for the PDF. We fire this whether there is a signature required or not,
* since we want the moment-in-time proof of what the EULA was when they accepted it.
*/
$branding_settings = SettingsController::getPDFBranding();
if (is_null($branding_settings->logo)){
$path_logo = "";
} else {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_to,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
event(new CheckoutAccepted($acceptance));
$return_msg = trans('admin/users/message.accepted');
} else {
$acceptance->decline($sig_filename);
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
if (Setting::getSettings()->require_accept_signature == '1') {
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Format the data to send the declined notification
$branding_settings = SettingsController::getPDFBranding();
// This is the most horriblest
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$asset_model = AssetModel::find($item->model_id);
$display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'note' => $request->input('note'),
'declined_date' => Carbon::parse($acceptance->declined_at)->format('Y-m-d'),
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'assigned_to' => $assigned_to,
'company_name' => $branding_settings->site_name,
'date_settings' => $branding_settings->date_display_format,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
return redirect()->to('account/accept')->with('success', $return_msg);
}
}

View File

@@ -3,10 +3,8 @@
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Response;
class ActionlogController extends Controller
{
public function displaySig($filename)
@@ -15,30 +13,17 @@ class ActionlogController extends Controller
// file_get_contents, so we set the error reporting for just this class
error_reporting(0);
$disk = config('filesystems.default');
switch (config("filesystems.disks.$disk.driver")) {
case 's3':
$file = 'private_uploads/signatures/'.$filename;
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
default:
$this->authorize('view', \App\Models\Asset::class);
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
$contents = file_get_contents($file, false, stream_context_create(['http' => ['ignore_errors' => true]]));
if ($contents === false) {
Log::warning('File '.$file.' not found');
return false;
} else {
return Response::make($contents)->header('Content-Type', $filetype);
}
}
}
public function getStoredEula($filename){
$this->authorize('view', \App\Models\Asset::class);
$file = config('app.private_uploads').'/eula-pdfs/'.$filename;
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
return Response::download($file);
$contents = file_get_contents($file, false, stream_context_create(['http' => ['ignore_errors' => true]]));
if ($contents === false) {
\Log::warn('File '.$file.' not found');
return false;
} else {
return Response::make($contents)->header('Content-Type', $filetype);
}
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\AccessoriesTransformer;
@@ -10,9 +9,9 @@ use App\Http\Transformers\SelectlistTransformer;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use DB;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
@@ -27,11 +26,9 @@ class AccessoriesController extends Controller
*/
public function index(Request $request)
{
if ($request->user()->cannot('reports.view')) {
$this->authorize('view', Accessory::class);
}
$this->authorize('view', Accessory::class);
$allowed_columns = ['id', 'name', 'model_number', 'eol', 'notes', 'created_at', 'min_amt', 'company_id'];
// This array is what determines which fields should be allowed to be sorted on ON the table itself, no relations
// Relations will be handled in query scopes a little further down.
$allowed_columns =
@@ -45,13 +42,10 @@ class AccessoriesController extends Controller
'min_amt',
'company_id',
'notes',
'users_count',
'qty',
];
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier')
->withCount('users as users_count');
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier');
if ($request->filled('search')) {
$accessories = $accessories->TextSearch($request->input('search'));
@@ -81,9 +75,12 @@ class AccessoriesController extends Controller
$accessories->where('notes','=',$request->input('notes'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($accessories) && ($request->get('offset') > $accessories->count())) ? $accessories->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
@@ -151,7 +148,7 @@ class AccessoriesController extends Controller
public function show($id)
{
$this->authorize('view', Accessory::class);
$accessory = Accessory::withCount('users as users_count')->findOrFail($id);
$accessory = Accessory::findOrFail($id);
return (new AccessoriesTransformer)->transformAccessory($accessory);
}
@@ -274,12 +271,12 @@ class AccessoriesController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
*/
public function checkout(Request $request, $accessoryId)
{
// Check if the accessory exists
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
if (is_null($accessory = Accessory::find($accessoryId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
}
@@ -303,7 +300,7 @@ class AccessoriesController extends Controller
'note' => $request->get('note'),
]);
event(new CheckoutableCheckedOut($accessory, $user, Auth::user(), $request->input('note')));
$accessory->logCheckout($request->input('note'), $user);
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
}
@@ -320,7 +317,7 @@ class AccessoriesController extends Controller
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @internal param int $accessoryId
*/
public function checkin(Request $request, $accessoryUserId = null)
@@ -332,7 +329,7 @@ class AccessoriesController extends Controller
$accessory = Accessory::find($accessory_user->accessory_id);
$this->authorize('checkin', $accessory);
$logaction = $accessory->logCheckin(User::find($accessory_user->assigned_to), $request->input('note'));
$logaction = $accessory->logCheckin(User::find($accessory_user->user_id), $request->input('note'));
// Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {

View File

@@ -1,219 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Actionlog;
use \Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use DB;
use Illuminate\Http\Request;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log;
use Input;
use Paginator;
use Slack;
use Str;
use TCPDF;
use Validator;
use Route;
/**
* 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
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null)
{
// 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
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list($assetId = null)
{
// 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);
}
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that there are some uploads on this asset that can be listed
if ($asset->uploads->count() > 0) {
$files = array();
foreach ($asset->uploads as $upload) {
array_push($files, $upload);
}
// Give the list of files back to the user
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/hardware/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/hardware/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error')), 500);
}
/**
* 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($assetId = null, $fileId = null)
{
// 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);
}
// 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
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy($assetId = null, $fileId = null)
{
// 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);
}
$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

@@ -8,7 +8,7 @@ use App\Http\Transformers\AssetMaintenancesTransformer;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Auth;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Input;
@@ -35,9 +35,7 @@ class AssetMaintenancesController extends Controller
public function index(Request $request)
{
$this->authorize('view', Asset::class);
$maintenances = AssetMaintenance::select('asset_maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'admin');
$maintenances = AssetMaintenance::with('asset', 'asset.model', 'asset.location', 'supplier', 'asset.company', 'admin');
if ($request->filled('search')) {
$maintenances = $maintenances->TextSearch($request->input('search'));
@@ -47,18 +45,12 @@ class AssetMaintenancesController extends Controller
$maintenances->where('asset_id', '=', $request->input('asset_id'));
}
if ($request->filled('supplier_id')) {
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($maintenances) && ($request->get('offset') > $maintenances->count())) ? $maintenances->count() : $request->get('offset', 0);
if ($request->filled('asset_maintenance_type')) {
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$allowed_columns = [
'id',
@@ -71,13 +63,8 @@ class AssetMaintenancesController extends Controller
'notes',
'asset_tag',
'asset_name',
'serial',
'user_id',
'supplier',
'is_warranty',
'status_label',
];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
@@ -85,21 +72,12 @@ class AssetMaintenancesController extends Controller
case 'user_id':
$maintenances = $maintenances->OrderAdmin($order);
break;
case 'supplier':
$maintenances = $maintenances->OrderBySupplier($order);
break;
case 'asset_tag':
$maintenances = $maintenances->OrderByTag($order);
break;
case 'asset_name':
$maintenances = $maintenances->OrderByAssetName($order);
break;
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
default:
$maintenances = $maintenances->orderBy($sort, $order);
break;
@@ -126,17 +104,41 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
// create a new model instance
$maintenance = new AssetMaintenance();
$maintenance->fill($request->all());
$maintenance->user_id = Auth::id();
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(e($request->input('asset_id')));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot add a maintenance for that asset'));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->user_id = Auth::id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
// Was the asset maintenance created?
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
}
@@ -144,39 +146,65 @@ class AssetMaintenancesController extends Controller
* Validates and stores an update to an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $id
* @param int $assetMaintenanceId
* @param int $request
* @version v1.0
* @since [v4.0]
* @return string JSON
*/
public function update(Request $request, $id)
public function update(Request $request, $assetMaintenanceId = null)
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
// Can this user manage this asset?
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
}
// The asset this miantenance is attached to is not valid or has been deleted
if (!$maintenance->asset) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
}
$maintenance->fill($request->all());
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
$assetMaintenance->supplier_id = e($request->input('supplier_id'));
$assetMaintenance->is_warranty = e($request->input('is_warranty'));
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
if (($assetMaintenance->completion_date == null)
) {
if (($assetMaintenance->asset_maintenance_time !== 0)
|| (! is_null($assetMaintenance->asset_maintenance_time))
) {
$assetMaintenance->asset_maintenance_time = null;
}
}
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
}
/**

View File

@@ -12,7 +12,6 @@ use App\Models\AssetModel;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
/**
* This class controls all actions related to asset models for
@@ -39,7 +38,6 @@ class AssetModelsController extends Controller
'image',
'name',
'model_number',
'min_amt',
'eol',
'notes',
'created_at',
@@ -47,7 +45,6 @@ class AssetModelsController extends Controller
'requestable',
'assets_count',
'category',
'fieldset',
];
$assetmodels = AssetModel::select([
@@ -55,7 +52,6 @@ class AssetModelsController extends Controller
'models.image',
'models.name',
'model_number',
'min_amt',
'eol',
'requestable',
'models.notes',
@@ -67,24 +63,23 @@ class AssetModelsController extends Controller
'models.deleted_at',
'models.updated_at',
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues')
->with('category', 'depreciation', 'manufacturer', 'fieldset')
->withCount('assets as assets_count');
if ($request->input('status')=='deleted') {
$assetmodels->onlyTrashed();
}
if ($request->filled('category_id')) {
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
}
if ($request->filled('search')) {
$assetmodels->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $assetmodels->count()) ? $assetmodels->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($assetmodels) && ($request->get('offset') > $assetmodels->count())) ? $assetmodels->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'models.created_at';
@@ -96,9 +91,6 @@ class AssetModelsController extends Controller
case 'category':
$assetmodels->OrderCategory($order);
break;
case 'fieldset':
$assetmodels->OrderFieldset($order);
break;
default:
$assetmodels->orderBy($sort, $order);
break;
@@ -225,7 +217,7 @@ class AssetModelsController extends Controller
try {
Storage::disk('public')->delete('assetmodels/'.$assetmodel->image);
} catch (\Exception $e) {
Log::info($e);
\Log::info($e);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@ use App\Models\Category;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
class CategoriesController extends Controller
{
@@ -24,76 +23,21 @@ class CategoriesController extends Controller
public function index(Request $request)
{
$this->authorize('view', Category::class);
$allowed_columns = [
'id',
'name',
'category_type',
'category_type',
'use_default_eula',
'eula_text',
'require_acceptance',
'checkin_email',
'assets_count',
'accessories_count',
'consumables_count',
'components_count',
'licenses_count',
'image',
];
$allowed_columns = ['id', 'name', 'category_type', 'category_type', 'use_default_eula', 'eula_text', 'require_acceptance', 'checkin_email', 'assets_count', 'accessories_count', 'consumables_count', 'components_count', 'licenses_count', 'image'];
$categories = Category::select([
'id',
'created_at',
'updated_at',
'name', 'category_type',
'use_default_eula',
'eula_text',
'require_acceptance',
'checkin_email',
'image'
])->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
/*
* This checks to see if we should override the Admin Setting to show archived assets in list.
* We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they
* may actually need to fetch assets that are archived.
*
* @see \App\Models\Category::showableAssets()
*/
if ($request->input('archived')=='true') {
$categories = $categories->withCount('assets as assets_count');
} else {
$categories = $categories->withCount('showableAssets as assets_count');
}
$categories = Category::select(['id', 'created_at', 'updated_at', 'name', 'category_type', 'use_default_eula', 'eula_text', 'require_acceptance', 'checkin_email', 'image'])
->withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
if ($request->filled('search')) {
$categories = $categories->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$categories->where('name', '=', $request->input('name'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($categories) && ($request->get('offset') > $categories->count())) ? $categories->count() : $request->get('offset', 0);
if ($request->filled('category_type')) {
$categories->where('category_type', '=', $request->input('category_type'));
}
if ($request->filled('use_default_eula')) {
$categories->where('use_default_eula', '=', $request->input('use_default_eula'));
}
if ($request->filled('require_acceptance')) {
$categories->where('require_acceptance', '=', $request->input('require_acceptance'));
}
if ($request->filled('checkin_email')) {
$categories->where('checkin_email', '=', $request->input('checkin_email'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $categories->count()) ? $categories->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'assets_count';
@@ -141,7 +85,7 @@ class CategoriesController extends Controller
public function show($id)
{
$this->authorize('view', 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::findOrFail($id);
return (new CategoriesTransformer)->transformCategory($category);
}
@@ -160,14 +104,8 @@ class CategoriesController extends Controller
{
$this->authorize('update', Category::class);
$category = Category::findOrFail($id);
// Don't allow the user to change the category_type once it's been created
if (($request->filled('category_type')) && ($category->category_type != $request->input('category_type'))) {
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.update.cannot_change_category_type'))
);
}
$category->fill($request->all());
$category->category_type = strtolower($request->input('category_type'));
$category = $request->handleImages($category);
if ($category->save()) {
@@ -188,7 +126,7 @@ class CategoriesController extends Controller
public function destroy($id)
{
$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::findOrFail($id);
if (! $category->isDeletable()) {
return response()->json(

View File

@@ -27,9 +27,6 @@ class CompaniesController extends Controller
$allowed_columns = [
'id',
'name',
'phone',
'fax',
'email',
'created_at',
'updated_at',
'users_count',
@@ -40,27 +37,18 @@ class CompaniesController extends Controller
'components_count',
];
$companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow();
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
$companies = Company::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
if ($request->filled('search')) {
$companies->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$companies->where('name', '=', $request->input('name'));
}
if ($request->filled('email')) {
$companies->where('email', '=', $request->input('email'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $companies->count()) ? $companies->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($companies) && ($request->get('offset') > $companies->count())) ? $companies->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -175,7 +163,6 @@ class CompaniesController extends Controller
$companies = Company::select([
'companies.id',
'companies.name',
'companies.email',
'companies.image',
]);

View File

@@ -12,9 +12,6 @@ use App\Http\Requests\ImageUploadRequest;
use App\Events\CheckoutableCheckedIn;
use App\Events\ComponentCheckedIn;
use App\Models\Asset;
use Illuminate\Support\Facades\Validator;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Log;
class ComponentsController extends Controller
{
@@ -46,17 +43,14 @@ class ComponentsController extends Controller
'notes',
];
$components = Component::select('components.*')
->with('company', 'location', 'category', 'assets', 'supplier');
$components = Company::scopeCompanyables(Component::select('components.*')
->with('company', 'location', 'category', 'assets'));
if ($request->filled('search')) {
$components = $components->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$components->where('name', '=', $request->input('name'));
}
if ($request->filled('company_id')) {
$components->where('company_id', '=', $request->input('company_id'));
}
@@ -65,10 +59,6 @@ class ComponentsController extends Controller
$components->where('category_id', '=', $request->input('category_id'));
}
if ($request->filled('supplier_id')) {
$components->where('supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('location_id')) {
$components->where('location_id', '=', $request->input('location_id'));
}
@@ -77,10 +67,14 @@ class ComponentsController extends Controller
$components->where('notes','=',$request->input('notes'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $components->count()) ? $components->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($components) && ($request->get('offset') > $components->count())) ? $components->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
@@ -95,9 +89,6 @@ class ComponentsController extends Controller
case 'company':
$components = $components->OrderCompany($order);
break;
case 'supplier':
$components = $components->OrderSupplier($order);
break;
default:
$components = $components->orderBy($column_sort, $order);
break;
@@ -205,29 +196,12 @@ class ComponentsController extends Controller
$this->authorize('view', \App\Models\Asset::class);
$component = Component::findOrFail($id);
$assets = $component->assets();
$offset = request('offset', 0);
$limit = $request->input('limit', 50);
if ($request->filled('search')) {
$assets = $component->assets()
->where(function ($query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->where('name', 'like', $search_str)
->orWhereIn('model_id', function (Builder $query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->selectRaw('id')->from('models')->where('name', 'like', $search_str);
})
->orWhere('asset_tag', 'like', $search_str);
})
->get();
$total = $assets->count();
} else {
$assets = $component->assets();
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
}
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
return (new ComponentsTransformer)->transformCheckedoutComponents($assets, $total);
}
@@ -247,30 +221,20 @@ class ComponentsController extends Controller
public function checkout(Request $request, $componentId)
{
// Check if the component exists
if (!$component = Component::find($componentId)) {
if (is_null($component = Component::find($componentId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.does_not_exist')));
}
$this->authorize('checkout', $component);
$validator = Validator::make($request->all(), [
'assigned_to' => 'required|exists:assets,id',
'assigned_qty' => "required|numeric|min:1|digits_between:1,".$component->numRemaining(),
]);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', $validator->errors()));
}
// Make sure there is at least one available to checkout
if ($component->numRemaining() < $request->get('assigned_qty')) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
}
if ($component->numRemaining() >= $request->get('assigned_qty')) {
$asset = Asset::find($request->input('assigned_to'));
if (!$asset = Asset::find($request->input('assigned_to'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
}
// Update the accessory data
$component->assigned_to = $request->input('assigned_to');
$component->assets()->attach($component->id, [
@@ -278,8 +242,7 @@ class ComponentsController extends Controller
'created_at' => \Carbon::now(),
'assigned_qty' => $request->get('assigned_qty', 1),
'user_id' => \Auth::id(),
'asset_id' => $request->get('assigned_to'),
'note' => $request->get('note'),
'asset_id' => $request->get('assigned_to')
]);
$component->logCheckout($request->input('note'), $asset);
@@ -287,7 +250,7 @@ class ComponentsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
return response()->json(Helper::formatStandardApiResponse('error', null, 'Not enough components remaining: '.$component->numRemaining().' remaining, '.$request->get('assigned_qty').' requested.'));
}
/**
@@ -332,7 +295,7 @@ class ComponentsController extends Controller
// actually checked out.
$component_assets->assigned_qty = $qty_remaining_in_checkout;
Log::debug($component_asset_id.' - '.$qty_remaining_in_checkout.' remaining in record '.$component_assets->id);
\Log::debug($component_asset_id.' - '.$qty_remaining_in_checkout.' remaining in record '.$component_assets->id);
\DB::table('components_assets')->where('id',
$component_asset_id)->update(['assigned_qty' => $qty_remaining_in_checkout]);

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ConsumablesTransformer;
@@ -12,8 +11,6 @@ use App\Models\Consumable;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ConsumablesController extends Controller
{
@@ -48,17 +45,16 @@ class ConsumablesController extends Controller
'notes',
];
$consumables = Consumable::select('consumables.*')
->with('company', 'location', 'category', 'users', 'manufacturer');
$consumables = Company::scopeCompanyables(
Consumable::select('consumables.*')
->with('company', 'location', 'category', 'users', 'manufacturer')
);
if ($request->filled('search')) {
$consumables = $consumables->TextSearch(e($request->input('search')));
}
if ($request->filled('name')) {
$consumables->where('name', '=', $request->input('name'));
}
if ($request->filled('company_id')) {
$consumables->where('company_id', '=', $request->input('company_id'));
}
@@ -75,10 +71,6 @@ class ConsumablesController extends Controller
$consumables->where('manufacturer_id', '=', $request->input('manufacturer_id'));
}
if ($request->filled('supplier_id')) {
$consumables->where('supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('location_id')) {
$consumables->where('location_id','=',$request->input('location_id'));
}
@@ -88,9 +80,12 @@ class ConsumablesController extends Controller
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $consumables->count()) ? $consumables->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($consumables) && ($request->get('offset') > $consumables->count())) ? $consumables->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$allowed_columns = ['id', 'name', 'order_number', 'min_amt', 'purchase_date', 'purchase_cost', 'company', 'category', 'model_number', 'item_no', 'manufacturer', 'location', 'qty', 'image'];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -112,9 +107,6 @@ class ConsumablesController extends Controller
case 'company':
$consumables = $consumables->OrderCompany($order);
break;
case 'supplier':
$components = $consumables->OrderSupplier($order);
break;
default:
$consumables = $consumables->orderBy($column_sort, $order);
break;
@@ -158,7 +150,7 @@ class ConsumablesController extends Controller
public function show($id)
{
$this->authorize('view', Consumable::class);
$consumable = Consumable::with('users')->findOrFail($id);
$consumable = Consumable::findOrFail($id);
return (new ConsumablesTransformer)->transformConsumable($consumable);
}
@@ -232,11 +224,9 @@ class ConsumablesController extends Controller
foreach ($consumable->consumableAssignments as $consumable_assignment) {
$rows[] = [
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
'admin' => ($consumable_assignment->admin) ? $consumable_assignment->admin->present()->nameUrl() : null,
'admin' => ($consumable_assignment->admin) ? $consumable_assignment->admin->present()->nameUrl() : '',
];
}
@@ -257,46 +247,44 @@ class ConsumablesController extends Controller
public function checkout(Request $request, $id)
{
// Check if the consumable exists
if (!$consumable = Consumable::with('users')->find($id)) {
if (is_null($consumable = Consumable::find($id))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.does_not_exist')));
}
$this->authorize('checkout', $consumable);
// Make sure there is at least one available to checkout
if ($consumable->numRemaining() <= 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable')));
if ($consumable->qty > 0) {
// Check if the user exists
$assigned_to = $request->input('assigned_to');
if (is_null($user = User::find($assigned_to))) {
// Return error message
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
}
// Update the consumable data
$consumable->assigned_to = e($assigned_to);
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'user_id' => $user->id,
'assigned_to' => $assigned_to,
]);
// Log checkout event
$logaction = $consumable->logCheckout(e($request->input('note')), $user);
$data['log_id'] = $logaction->id;
$data['eula'] = $consumable->getEula();
$data['first_name'] = $user->first_name;
$data['item_name'] = $consumable->name;
$data['checkout_date'] = $logaction->created_at;
$data['note'] = $logaction->note;
$data['require_acceptance'] = $consumable->requireAcceptance();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
}
// Make sure there is a valid category
if (!$consumable->category){
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.invalid_item_category_single', ['type' => trans('general.consumable')])));
}
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
if (!$user = User::find($request->input('assigned_to'))) {
// Return error message
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
Log::debug('No valid user');
}
// Update the consumable data
$consumable->assigned_to = $request->input('assigned_to');
$consumable->users()->attach($consumable->id,
[
'consumable_id' => $consumable->id,
'user_id' => $user->id,
'assigned_to' => $request->input('assigned_to'),
'note' => $request->input('note'),
]
);
event(new CheckoutableCheckedOut($consumable, $user, Auth::user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('error', null, 'No consumables remaining'));
}
/**

View File

@@ -96,7 +96,7 @@ class CustomFieldsController extends Controller
$data = $request->all();
$regex_format = null;
if ((array_key_exists('format', $data)) && (str_contains($data['format'], 'regex:'))) {
if (str_contains($data['format'], 'regex:')) {
$regex_format = $data['format'];
}
@@ -179,7 +179,7 @@ class CustomFieldsController extends Controller
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v1.8]
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
*/
public function destroy($field_id)
{

View File

@@ -7,7 +7,6 @@ use App\Http\Controllers\Controller;
use App\Http\Transformers\CustomFieldsetsTransformer;
use App\Http\Transformers\CustomFieldsTransformer;
use App\Models\CustomFieldset;
use App\Models\CustomField;
use Illuminate\Http\Request;
use Redirect;
use View;
@@ -34,7 +33,7 @@ class CustomFieldsetsController extends Controller
*/
public function index()
{
$this->authorize('index', CustomField::class);
$this->authorize('index', CustomFieldset::class);
$fieldsets = CustomFieldset::withCount('fields as fields_count', 'models as models_count')->get();
return (new CustomFieldsetsTransformer)->transformCustomFieldsets($fieldsets, $fieldsets->count());
@@ -50,7 +49,7 @@ class CustomFieldsetsController extends Controller
*/
public function show($id)
{
$this->authorize('view', CustomField::class);
$this->authorize('view', CustomFieldset::class);
if ($fieldset = CustomFieldset::find($id)) {
return (new CustomFieldsetsTransformer)->transformCustomFieldset($fieldset);
}
@@ -69,7 +68,7 @@ class CustomFieldsetsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('update', CustomField::class);
$this->authorize('update', CustomFieldset::class);
$fieldset = CustomFieldset::findOrFail($id);
$fieldset->fill($request->all());
@@ -90,23 +89,11 @@ class CustomFieldsetsController extends Controller
*/
public function store(Request $request)
{
$this->authorize('create', CustomField::class);
$this->authorize('create', CustomFieldset::class);
$fieldset = new CustomFieldset;
$fieldset->fill($request->all());
if ($fieldset->save()) {
// Sync fieldset with auto_add_to_fieldsets
$fields = CustomField::select('id')->where('auto_add_to_fieldsets', '=', '1')->get();
if ($fields->count() > 0) {
foreach ($fields as $field) {
$field_ids[] = $field->id;
}
$fieldset->fields()->sync($field_ids);
}
return response()->json(Helper::formatStandardApiResponse('success', $fieldset, trans('admin/custom_fields/message.fieldset.create.success')));
}
@@ -118,11 +105,11 @@ class CustomFieldsetsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
*/
public function destroy($id)
{
$this->authorize('delete', CustomField::class);
$this->authorize('delete', CustomFieldset::class);
$fieldset = CustomFieldset::findOrFail($id);
$modelsCount = $fieldset->models->count();
@@ -149,7 +136,7 @@ class CustomFieldsetsController extends Controller
*/
public function fields($id)
{
$this->authorize('view', CustomField::class);
$this->authorize('view', CustomFieldset::class);
$set = CustomFieldset::findOrFail($id);
$fields = $set->fields;
@@ -166,7 +153,7 @@ class CustomFieldsetsController extends Controller
*/
public function fieldsWithDefaultValues($fieldsetId, $modelId)
{
$this->authorize('view', CustomField::class);
$this->authorize('view', CustomFieldset::class);
$set = CustomFieldset::findOrFail($fieldsetId);

View File

@@ -8,7 +8,7 @@ use App\Http\Transformers\DepartmentsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Company;
use App\Models\Department;
use Illuminate\Support\Facades\Auth;
use Auth;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage;
@@ -27,42 +27,27 @@ class DepartmentsController extends Controller
$this->authorize('view', Department::class);
$allowed_columns = ['id', 'name', 'image', 'users_count'];
$departments = Department::select(
$departments = Company::scopeCompanyables(Department::select(
'departments.id',
'departments.name',
'departments.phone',
'departments.fax',
'departments.location_id',
'departments.company_id',
'departments.manager_id',
'departments.created_at',
'departments.updated_at',
'departments.image'
)->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
'departments.image'),
"company_id", "departments")->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
if ($request->filled('search')) {
$departments = $departments->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$departments->where('name', '=', $request->input('name'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($departments) && ($request->get('offset') > $departments->count())) ? $departments->count() : $request->get('offset', 0);
if ($request->filled('company_id')) {
$departments->where('company_id', '=', $request->input('company_id'));
}
if ($request->filled('manager_id')) {
$departments->where('manager_id', '=', $request->input('manager_id'));
}
if ($request->filled('location_id')) {
$departments->where('location_id', '=', $request->input('location_id'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $departments->count()) ? $departments->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';

View File

@@ -28,9 +28,12 @@ class DepreciationsController extends Controller
$depreciations = $depreciations->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $depreciations->count()) ? $depreciations->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($depreciations) && ($request->get('offset') > $depreciations->count())) ? $depreciations->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';

View File

@@ -7,8 +7,6 @@ use App\Http\Controllers\Controller;
use App\Http\Transformers\GroupsTransformer;
use App\Models\Group;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class GroupsController extends Controller
{
@@ -21,24 +19,21 @@ class GroupsController extends Controller
*/
public function index(Request $request)
{
$this->authorize('superadmin');
$this->authorize('view', Group::class);
$allowed_columns = ['id', 'name', 'created_at', 'users_count'];
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at', 'created_by')->with('admin')->withCount('users as users_count');
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at')->withCount('users as users_count');
if ($request->filled('search')) {
$groups = $groups->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$groups->where('name', '=', $request->input('name'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($groups) && ($request->get('offset') > $groups->count())) ? $groups->count() : $request->get('offset', 0);
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $groups->count()) ? $groups->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -60,18 +55,12 @@ class GroupsController extends Controller
*/
public function store(Request $request)
{
$this->authorize('superadmin');
$this->authorize('create', Group::class);
$group = new Group;
// Get all the available permissions
$permissions = config('permissions');
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
$group->name = $request->input('name');
$group->created_by = Auth::user()->id;
$group->permissions = json_encode($request->input('permissions', $groupPermissions));
$group->fill($request->all());
if ($group->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new GroupsTransformer)->transformGroup($group), trans('admin/groups/message.success.create')));
return response()->json(Helper::formatStandardApiResponse('success', $group, trans('admin/groups/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $group->getErrors()));
@@ -87,8 +76,9 @@ class GroupsController extends Controller
*/
public function show($id)
{
$this->authorize('superadmin');
$this->authorize('view', Group::class);
$group = Group::findOrFail($id);
return (new GroupsTransformer)->transformGroup($group);
}
@@ -103,14 +93,12 @@ class GroupsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('superadmin');
$this->authorize('update', Group::class);
$group = Group::findOrFail($id);
$group->name = $request->input('name');
$group->permissions = $request->input('permissions'); // Todo - some JSON validation stuff here
$group->fill($request->all());
if ($group->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new GroupsTransformer)->transformGroup($group), trans('admin/groups/message.success.update')));
return response()->json(Helper::formatStandardApiResponse('success', $group, trans('admin/groups/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $group->getErrors()));
@@ -126,8 +114,9 @@ class GroupsController extends Controller
*/
public function destroy($id)
{
$this->authorize('superadmin');
$this->authorize('delete', Group::class);
$group = Group::findOrFail($id);
$this->authorize('delete', $group);
$group->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/groups/message.delete.success')));

View File

@@ -9,14 +9,12 @@ use App\Http\Transformers\ImportsTransformer;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Import;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Artisan;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use League\Csv\Reader;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Illuminate\Support\Facades\Log;
class ImportController extends Controller
{
@@ -37,7 +35,7 @@ class ImportController extends Controller
* Process and store a CSV upload file.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function store()
{
@@ -58,7 +56,7 @@ class ImportController extends Controller
'text/tsv', ])) {
$results['error'] = 'File type must be CSV. Uploaded file is '.$file->getMimeType();
return response()->json(Helper::formatStandardApiResponse('error', null, $results['error']), 422);
return response()->json(Helper::formatStandardApiResponse('error', null, $results['error']), 500);
}
//TODO: is there a lighter way to do this?
@@ -66,19 +64,7 @@ class ImportController extends Controller
ini_set('auto_detect_line_endings', '1');
}
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
try {
$import->header_row = $reader->fetchOne(0);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
'error',
null,
trans('admin/hardware/message.import.header_row_has_malformed_characters')
),
422
);
}
$import->header_row = $reader->fetchOne(0);
//duplicate headers check
$duplicate_headers = [];
@@ -96,22 +82,11 @@ class ImportController extends Controller
}
}
if (count($duplicate_headers) > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, implode('; ', $duplicate_headers)),422);
return response()->json(Helper::formatStandardApiResponse('error', null, implode('; ', $duplicate_headers)), 500); //should this be '4xx'?
}
try {
// Grab the first row to display via ajax as the user picks fields
$import->first_row = $reader->fetchOne(1);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
'error',
null,
trans('admin/hardware/message.import.content_row_has_malformed_characters')
),
422
);
}
// Grab the first row to display via ajax as the user picks fields
$import->first_row = $reader->fetchOne(1);
$date = date('Y-m-d-his');
$fixed_filename = str_slug($file->getClientOriginalName());
@@ -127,25 +102,18 @@ class ImportController extends Controller
}
$file_name = date('Y-m-d-his').'-'.$fixed_filename;
$import->file_path = $file_name;
$import->filesize = null;
if (!file_exists($path.'/'.$file_name)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 500);
}
$import->filesize = filesize($path.'/'.$file_name);
$import->save();
$results[] = $import;
}
$results = (new ImportsTransformer)->transformImports($results);
return response()->json([
return [
'files' => $results,
]);
];
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.feature_disabled')), 422);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.feature_disabled')), 500);
}
/**
@@ -159,21 +127,14 @@ class ImportController extends Controller
$this->authorize('import');
// Run a backup immediately before processing
if ($request->get('run-backup')) {
Log::debug('Backup manually requested via importer');
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
if ($request->has('run-backup')) {
\Log::debug('Backup manually requested via importer');
Artisan::call('backup:run');
} else {
Log::debug('NO BACKUP requested via importer');
\Log::debug('NO BACKUP requested via importer');
}
$import = Import::find($import_id);
if(is_null($import)){
$error[0][0] = trans("validation.exists", ["attribute" => "file"]);
return response()->json(Helper::formatStandardApiResponse('import-errors', null, $error), 500);
}
$errors = $request->import($import);
$errors = $request->import(Import::find($import_id));
$redirectTo = 'hardware.index';
switch ($request->get('import-type')) {
case 'asset':
@@ -194,9 +155,6 @@ class ImportController extends Controller
case 'user':
$redirectTo = 'users.index';
break;
case 'location':
$redirectTo = 'locations.index';
break;
}
if ($errors) { //Failure

View File

@@ -1,71 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\LabelsTransformer;
use App\Models\Labels\Label;
use Illuminate\Http\Request;
use Illuminate\Support\ItemNotFoundException;
use Illuminate\Support\Facades\Auth;
class LabelsController extends Controller
{
/**
* Returns JSON listing of all labels.
*
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
* @return JsonResponse
*/
public function index(Request $request)
{
$this->authorize('view', Label::class);
$labels = Label::find();
if ($request->filled('search')) {
$search = $request->get('search');
$labels = $labels->filter(function ($label, $index) use ($search) {
return stripos($label->getName(), $search) !== false;
});
}
$total = $labels->count();
$offset = $request->get('offset', 0);
$offset = ($offset > $total) ? $total : $offset;
$maxLimit = config('app.max_results');
$limit = $request->get('limit', $maxLimit);
$limit = ($limit > $maxLimit) ? $maxLimit : $limit;
$labels = $labels->skip($offset)->take($limit);
return (new LabelsTransformer)->transformLabels($labels, $total, $request);
}
/**
* Returns JSON with information about a label for detail view.
*
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
* @param string $labelName
* @return JsonResponse
*/
public function show(string $labelName)
{
$labelName = str_replace('/', '\\', $labelName);
try {
$label = Label::find($labelName);
} catch(ItemNotFoundException $e) {
return response()
->json(
Helper::formatStandardApiResponse('error', null, trans('admin/labels/message.does_not_exist')),
404
);
}
$this->authorize('view', $label);
return (new LabelsTransformer)->transformLabel($label);
}
}

View File

@@ -9,7 +9,7 @@ use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Auth;
use Illuminate\Http\Request;
class LicenseSeatsController extends Controller
@@ -39,15 +39,8 @@ class LicenseSeatsController extends Controller
}
$total = $seats->count();
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $seats->count()) ? $seats->count() : app('api_offset_value');
if ($offset >= $total ){
$offset = 0;
}
$limit = app('api_limit_value');
$offset = (($seats) && (request('offset') > $total)) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$seats = $seats->skip($offset)->take($limit)->get();
@@ -123,20 +116,16 @@ class LicenseSeatsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) {
$target = $is_checkin ? $oldUser : User::find($licenseSeat->assigned_to);
}
if ($licenseSeat->isDirty('asset_id')) {
$target = $is_checkin ? $oldAsset : Asset::find($licenseSeat->asset_id);
}
if (is_null($target)){
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
}
if ($licenseSeat->save()) {
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
$changes = $licenseSeat->getChanges();
if (array_key_exists('assigned_to', $changes)) {
$target = $is_checkin ? $oldUser : User::find($changes['assigned_to']);
}
if (array_key_exists('asset_id', $changes)) {
$target = $is_checkin ? $oldAsset : Asset::find($changes['asset_id']);
}
if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('note'));

View File

@@ -26,8 +26,7 @@ class LicensesController extends Controller
public function index(Request $request)
{
$this->authorize('view', License::class);
$licenses = License::with('company', 'manufacturer', 'supplier','category')->withCount('freeSeats as free_seats_count');
$licenses = Company::scopeCompanyables(License::with('company', 'manufacturer', 'supplier', 'category')->withCount('freeSeats as free_seats_count'));
if ($request->filled('company_id')) {
$licenses->where('company_id', '=', $request->input('company_id'));
@@ -73,6 +72,9 @@ class LicensesController extends Controller
$licenses->where('depreciation_id', '=', $request->input('depreciation_id'));
}
if ($request->filled('supplier_id')) {
$licenses->where('supplier_id', '=', $request->input('supplier_id'));
}
if (($request->filled('maintained')) && ($request->input('maintained')=='true')) {
$licenses->where('maintained','=',1);
@@ -94,9 +96,12 @@ class LicensesController extends Controller
$licenses->onlyTrashed();
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($licenses) && ($request->get('offset') > $licenses->count())) ? $licenses->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -136,7 +141,6 @@ class LicensesController extends Controller
'seats',
'termination_date',
'depreciation_id',
'min_amt',
];
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$licenses = $licenses->orderBy($sort, $order);
@@ -144,10 +148,9 @@ class LicensesController extends Controller
}
$total = $licenses->count();
$licenses = $licenses->skip($offset)->take($limit)->get();
return (new LicensesTransformer)->transformLicenses($licenses, $total);
return (new LicensesTransformer)->transformLicenses($licenses, $total);
}
/**

View File

@@ -25,27 +25,9 @@ class LocationsController extends Controller
{
$this->authorize('view', Location::class);
$allowed_columns = [
'id',
'name',
'address',
'address2',
'city',
'state',
'country',
'zip',
'created_at',
'updated_at',
'manager_id',
'image',
'assigned_assets_count',
'users_count',
'assets_count',
'assigned_assets_count',
'assets_count',
'rtd_assets_count',
'currency',
'ldap_ou',
];
'id', 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', 'created_at',
'updated_at', 'manager_id', 'image',
'assigned_assets_count', 'users_count', 'assets_count', 'currency', 'ldap_ou', ];
$locations = Location::with('parent', 'manager', 'children')->select([
'locations.id',
@@ -55,8 +37,6 @@ class LocationsController extends Controller
'locations.city',
'locations.state',
'locations.zip',
'locations.phone',
'locations.fax',
'locations.country',
'locations.parent_id',
'locations.manager_id',
@@ -67,51 +47,20 @@ class LocationsController extends Controller
'locations.currency',
])->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count');
if ($request->filled('search')) {
$locations = $locations->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$locations->where('locations.name', '=', $request->input('name'));
}
$offset = (($locations) && (request('offset') > $locations->count())) ? $locations->count() : request('offset', 0);
if ($request->filled('address')) {
$locations->where('locations.address', '=', $request->input('address'));
}
if ($request->filled('address2')) {
$locations->where('locations.address2', '=', $request->input('address2'));
}
if ($request->filled('city')) {
$locations->where('locations.city', '=', $request->input('city'));
}
if ($request->filled('zip')) {
$locations->where('locations.zip', '=', $request->input('zip'));
}
if ($request->filled('country')) {
$locations->where('locations.country', '=', $request->input('country'));
}
if ($request->filled('manager_id')) {
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
switch ($request->input('sort')) {
case 'parent':
$locations->OrderParent($order);
@@ -184,9 +133,7 @@ class LocationsController extends Controller
])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('users as users_count')
->findOrFail($id);
->withCount('users as users_count')->findOrFail($id);
return (new LocationsTransformer)->transformLocation($location);
}
@@ -235,16 +182,10 @@ class LocationsController extends Controller
public function destroy($id)
{
$this->authorize('delete', Location::class);
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->findOrFail($id);
$location = Location::findOrFail($id);
if (! $location->isDeletable()) {
return response()
->json(Helper::formatStandardApiResponse('error', null, trans('admin/locations/message.assoc_users')));
->json(Helper::formatStandardApiResponse('error', null, trans('admin/companies/message.assoc_users')));
}
$this->authorize('delete', $location);
$location->delete();
@@ -282,12 +223,8 @@ class LocationsController extends Controller
*/
public function selectlist(Request $request)
{
// If a user is in the process of editing their profile, as determined by the referrer,
// then we check that they have permission to edit their own location.
// Otherwise, we do our normal check that they can view select lists.
$request->headers->get('referer') === route('profile')
? $this->authorize('self.edit_location')
: $this->authorize('view.selectlists');
$this->authorize('view.selectlists');
$locations = Location::select([
'locations.id',

View File

@@ -6,11 +6,9 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ManufacturersTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Actionlog;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class ManufacturersController extends Controller
@@ -25,10 +23,10 @@ class ManufacturersController extends Controller
public function index(Request $request)
{
$this->authorize('view', Manufacturer::class);
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'warranty_lookup_url', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
$manufacturers = Manufacturer::select(
['id', 'name', 'url', 'support_url', 'warranty_lookup_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
)->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('consumables as consumables_count')->withCount('accessories as accessories_count');
if ($request->input('deleted') == 'true') {
@@ -39,33 +37,12 @@ class ManufacturersController extends Controller
$manufacturers = $manufacturers->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$manufacturers->where('name', '=', $request->input('name'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($manufacturers) && ($request->get('offset') > $manufacturers->count())) ? $manufacturers->count() : $request->get('offset', 0);
if ($request->filled('url')) {
$manufacturers->where('url', '=', $request->input('url'));
}
if ($request->filled('support_url')) {
$manufacturers->where('support_url', '=', $request->input('support_url'));
}
if ($request->filled('warranty_lookup_url')) {
$manufacturers->where('warranty_lookup_url', '=', $request->input('warranty_lookup_url'));
}
if ($request->filled('support_phone')) {
$manufacturers->where('support_phone', '=', $request->input('support_phone'));
}
if ($request->filled('support_email')) {
$manufacturers->where('support_email', '=', $request->input('support_email'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $manufacturers->count()) ? $manufacturers->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -161,44 +138,6 @@ class ManufacturersController extends Controller
}
/**
* Restore a given Manufacturer (mark as un-deleted)
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.3.4]
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function restore($id)
{
$this->authorize('delete', Manufacturer::class);
if ($manufacturer = Manufacturer::withTrashed()->find($id)) {
if ($manufacturer->deleted_at == '') {
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.manufacturer')])), 200);
}
if ($manufacturer->restore()) {
$logaction = new Actionlog();
$logaction->item_type = Manufacturer::class;
$logaction->item_id = $manufacturer->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/manufacturers/message.restore.success')), 200);
}
// Check validation to make sure we're not restoring an item with the same unique attributes as a non-deleted one
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.manufacturer'), 'error' => $manufacturer->getErrors()->first()])), 200);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.does_not_exist')));
}
/**
* Gets a paginated collection for the select2 menus
*

View File

@@ -29,10 +29,8 @@ class PredefinedKitsController extends Controller
$kits = $kits->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $kits->count()) ? $kits->count() : app('api_offset_value');
$limit = app('api_limit_value');
$offset = $request->input('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'desc' ? 'desc' : 'asc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'name';
$kits->orderBy($sort, $order);

View File

@@ -5,38 +5,10 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\CheckoutRequest;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\Gate;
use App\Models\CustomField;
use Illuminate\Support\Facades\DB;
use Auth;
class ProfileController extends Controller
{
/**
* The token repository implementation.
*
* @var \Laravel\Passport\TokenRepository
*/
protected $tokenRepository;
/**
* Create a controller instance.
*
* @param \Laravel\Passport\TokenRepository $tokenRepository
* @param \Illuminate\Contracts\Validation\Factory $validation
* @return void
*/
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
{
$this->validation = $validation;
$this->tokenRepository = $tokenRepository;
}
/**
* Display a listing of requested assets.
*
@@ -49,129 +21,25 @@ class ProfileController extends Controller
{
$checkoutRequests = CheckoutRequest::where('user_id', '=', Auth::user()->id)->get();
$results = array();
$show_field = array();
$showable_fields = array();
$results = [];
$results['total'] = $checkoutRequests->count();
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
if (($field->field_encrypted=='0') && ($field->show_in_requestable_list=='1')) {
$showable_fields[] = $field->db_column_name();
}
}
foreach ($checkoutRequests as $checkoutRequest) {
// Make sure the asset and request still exist
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
$assets = [
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
'name' => e($checkoutRequest->itemRequested()->present()->name()),
'type' => e($checkoutRequest->itemType()),
'qty' => (int) $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
$results['rows'][] = [
'image' => $checkoutRequest->itemRequested()->present()->getImageUrl(),
'name' => $checkoutRequest->itemRequested()->present()->name(),
'type' => $checkoutRequest->itemType(),
'qty' => $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? $checkoutRequest->location()->name : null,
'expected_checkin' => Helper::getFormattedDateObject($checkoutRequest->itemRequested()->expected_checkin, 'datetime'),
'request_date' => Helper::getFormattedDateObject($checkoutRequest->created_at, 'datetime'),
];
foreach ($showable_fields as $showable_field_name) {
$show_field['custom_fields.'.$showable_field_name] = $checkoutRequest->itemRequested()->{$showable_field_name};
}
// Merge the plain asset data and the custom fields data
$results['rows'][] = array_merge($assets, $show_field);
}
}
return $results;
}
/**
* Delete an API token
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0.5]
*
* @return \Illuminate\Http\Response
*/
public function createApiToken(Request $request) {
if (!Gate::allows('self.api')) {
abort(403);
}
$accessTokenName = $request->input('name', 'Auth Token');
if ($accessToken = Auth::user()->createToken($accessTokenName)->accessToken) {
// Get the ID so we can return that with the payload
$token = DB::table('oauth_access_tokens')->where('user_id', '=', Auth::user()->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first();
$accessTokenData['id'] = $token->id;
$accessTokenData['token'] = $accessToken;
$accessTokenData['name'] = $accessTokenName;
return response()->json(Helper::formatStandardApiResponse('success', $accessTokenData, 'Personal access token '.$accessTokenName.' created successfully'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Token could not be created.'));
}
/**
* Delete an API token
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0.5]
*
* @return \Illuminate\Http\Response
*/
public function deleteApiToken($tokenId) {
if (!Gate::allows('self.api')) {
abort(403);
}
$token = $this->tokenRepository->findForUser(
$tokenId, Auth::user()->getAuthIdentifier()
);
if (is_null($token)) {
return new Response('', 404);
}
$token->revoke();
return new Response('', Response::HTTP_NO_CONTENT);
}
/**
* Show user's API tokens
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0.5]
*
* @return \Illuminate\Http\Response
*/
public function showApiTokens(Request $request) {
if (!Gate::allows('self.api')) {
abort(403);
}
$tokens = $this->tokenRepository->forUser(Auth::user()->getAuthIdentifier());
$token_values = $tokens->load('client')->filter(function ($token) {
return $token->client->personal_access_client && ! $token->revoked;
})->values();
return response()->json(Helper::formatStandardApiResponse('success', $token_values, null));
}
}

View File

@@ -20,7 +20,7 @@ class ReportsController extends Controller
{
$this->authorize('reports.view');
$actionlogs = Actionlog::with('item', 'user', 'admin', 'target', 'location');
$actionlogs = Actionlog::with('item', 'user', 'target', 'location');
if ($request->filled('search')) {
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
@@ -32,34 +32,14 @@ class ReportsController extends Controller
}
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
$actionlogs = $actionlogs->where(function($query) use ($request)
{
$query->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')))
->orWhere(function($query) use ($request)
{
$query->where('target_id', '=', $request->input('item_id'))
->where('target_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
});
});
$actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
}
if ($request->filled('action_type')) {
$actionlogs = $actionlogs->where('action_type', '=', $request->input('action_type'))->orderBy('created_at', 'desc');
}
if ($request->filled('user_id')) {
$actionlogs = $actionlogs->where('user_id', '=', $request->input('user_id'));
}
if ($request->filled('action_source')) {
$actionlogs = $actionlogs->where('action_source', '=', $request->input('action_source'))->orderBy('created_at', 'desc');
}
if ($request->filled('remote_ip')) {
$actionlogs = $actionlogs->where('remote_ip', '=', $request->input('remote_ip'))->orderBy('created_at', 'desc');
}
if ($request->filled('uploads')) {
$actionlogs = $actionlogs->whereNotNull('filename')->orderBy('created_at', 'desc');
}
@@ -72,21 +52,13 @@ class ReportsController extends Controller
'accept_signature',
'action_type',
'note',
'remote_ip',
'user_agent',
'action_source',
];
$total = $actionlogs->count();
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
$limit = app('api_limit_value');
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$order = ($request->input('order') == 'asc') ? 'asc' : 'desc';
$offset = request('offset', 0);
$limit = request('limit', 50);
$total = $actionlogs->count();
$actionlogs = $actionlogs->orderBy($sort, $order)->skip($offset)->take($limit)->get();
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);

View File

@@ -2,9 +2,6 @@
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Transformers\DatatablesTransformer;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ldap;
@@ -21,7 +18,6 @@ use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use App\Http\Requests\SlackSettingsRequest;
use App\Http\Transformers\LoginAttemptsTransformer;
class SettingsController extends Controller
@@ -33,18 +29,18 @@ class SettingsController extends Controller
$settings = Setting::getSettings();
if ($settings->ldap_enabled!='1') {
Log::debug('LDAP is not enabled cannot test.');
\Log::debug('LDAP is not enabled cannot test.');
return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400);
}
Log::debug('Preparing to test LDAP connection');
\Log::debug('Preparing to test LDAP connection');
$message = []; //where we collect together test messages
try {
$connection = Ldap::connectToLdap();
try {
$message['bind'] = ['message' => 'Successfully bound to LDAP server.'];
Log::debug('attempting to bind to LDAP for LDAP test');
\Log::debug('attempting to bind to LDAP for LDAP test');
Ldap::bindAdminToLdap($connection);
$message['login'] = [
'message' => 'Successfully connected to LDAP server.',
@@ -75,13 +71,13 @@ class SettingsController extends Controller
return response()->json($message, 200);
} catch (\Exception $e) {
Log::debug('Bind failed');
Log::debug("Exception was: ".$e->getMessage());
\Log::debug('Bind failed');
\Log::debug("Exception was: ".$e->getMessage());
return response()->json(['message' => $e->getMessage()], 400);
//return response()->json(['message' => $e->getMessage()], 500);
}
} catch (\Exception $e) {
Log::debug('Connection failed but we cannot debug it any further on our end.');
\Log::debug('Connection failed but we cannot debug it any further on our end.');
return response()->json(['message' => $e->getMessage()], 500);
}
@@ -92,7 +88,7 @@ class SettingsController extends Controller
{
if (Setting::getSettings()->ldap_enabled != '1') {
Log::debug('LDAP is not enabled. Cannot test.');
\Log::debug('LDAP is not enabled. Cannot test.');
return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400);
}
@@ -104,51 +100,92 @@ class SettingsController extends Controller
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
Log::debug('LDAP Validation test failed.');
\Log::debug('LDAP Validation test failed.');
$validation_errors = implode(' ',$validator->errors()->all());
return response()->json(['message' => $validator->errors()->all()], 400);
}
Log::debug('Preparing to test LDAP login');
\Log::debug('Preparing to test LDAP login');
try {
$connection = Ldap::connectToLdap();
try {
Ldap::bindAdminToLdap($connection);
Log::debug('Attempting to bind to LDAP for LDAP test');
\Log::debug('Attempting to bind to LDAP for LDAP test');
try {
$ldap_user = Ldap::findAndBindUserLdap($request->input('ldaptest_user'), $request->input('ldaptest_password'));
if ($ldap_user) {
Log::debug('It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.');
\Log::debug('It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.');
return response()->json(['message' => 'It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.'], 200);
}
return response()->json(['message' => 'Login Failed. '. $request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400);
} catch (\Exception $e) {
Log::debug('LDAP login failed');
\Log::debug('LDAP login failed');
return response()->json(['message' => $e->getMessage()], 400);
}
} catch (\Exception $e) {
Log::debug('Bind failed');
\Log::debug('Bind failed');
return response()->json(['message' => $e->getMessage()], 400);
//return response()->json(['message' => $e->getMessage()], 500);
}
} catch (\Exception $e) {
Log::debug('Connection failed');
\Log::debug('Connection failed');
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function slacktest(SlackSettingsRequest $request)
{
$validator = Validator::make($request->all(), [
'slack_endpoint' => 'url|required_with:slack_channel|starts_with:https://hooks.slack.com/|nullable',
'slack_channel' => 'required_with:slack_endpoint|starts_with:#|nullable',
]);
if ($validator->fails()) {
return response()->json(['message' => 'Validation failed', 'errors' => $validator->errors()], 422);
}
// If validation passes, continue to the curl request
$slack = new Client([
'base_url' => e($request->input('slack_endpoint')),
'defaults' => [
'exceptions' => false,
],
]);
$payload = json_encode(
[
'channel' => e($request->input('slack_channel')),
'text' => trans('general.slack_test_msg'),
'username' => e($request->input('slack_botname')),
'icon_emoji' => ':heart:',
]);
try {
$slack->post($request->input('slack_endpoint'), ['body' => $payload]);
return response()->json(['message' => 'Success'], 200);
} catch (\Exception $e) {
return response()->json(['message' => 'Please check the channel name and webhook endpoint URL ('.e($request->input('slack_endpoint')).'). Slack responded with: '.$e->getMessage()], 400);
}
//}
return response()->json(['message' => 'Something went wrong :( '], 400);
}
/**
* Test the email configuration
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return JsonResponse
* @return Redirect
*/
public function ajaxTestEmail()
{
@@ -170,7 +207,7 @@ class SettingsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0]
* @return JsonResponse
* @return Response
*/
public function purgeBarcodes()
{
@@ -181,19 +218,19 @@ class SettingsController extends Controller
$file_parts = explode('.', $file);
$extension = end($file_parts);
Log::debug($extension);
\Log::debug($extension);
// Only generated barcodes would have a .png file extension
if ($extension == 'png') {
Log::debug('Deleting: '.$file);
\Log::debug('Deleting: '.$file);
try {
Storage::disk('public')->delete($file);
Log::debug('Deleting: '.$file);
\Log::debug('Deleting: '.$file);
$file_count++;
} catch (\Exception $e) {
Log::debug($e);
\Log::debug($e);
}
}
}
@@ -211,7 +248,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0]
* @param \Illuminate\Http\Request $request
* @return array | JsonResponse
* @return array
*/
public function showLoginAttempts(Request $request)
{
@@ -227,99 +264,4 @@ class SettingsController extends Controller
return (new LoginAttemptsTransformer)->transformLoginAttempts($login_attempt_results, $total);
}
/**
* Lists backup files
*
* @author [A. Gianotto]
* @return array | JsonResponse
*/
public function listBackups() {
$settings = Setting::getSettings();
$path = 'app/backups';
$backup_files = Storage::files($path);
$files_raw = [];
$count = 0;
if (count($backup_files) > 0) {
for ($f = 0; $f < count($backup_files); $f++) {
// Skip dotfiles like .gitignore and .DS_STORE
if ((substr(basename($backup_files[$f]), 0, 1) != '.')) {
$file_timestamp = Storage::lastModified($backup_files[$f]);
$files_raw[] = [
'filename' => basename($backup_files[$f]),
'filesize' => Setting::fileSizeConvert(Storage::size($backup_files[$f])),
'modified_value' => $file_timestamp,
'modified_display' => date($settings->date_display_format.' '.$settings->time_display_format, $file_timestamp),
'backup_url' => config('app.url').'/settings/backups/download/'.basename($backup_files[$f]),
];
$count++;
}
}
}
$files = array_reverse($files_raw);
return (new DatatablesTransformer)->transformDatatables($files, $count);
}
/**
* Downloads a backup file.
* We use response()->download() here instead of Storage::download() because Storage::download()
* exhausts memory on larger files.
*
* @author [A. Gianotto]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadBackup($file) {
$path = storage_path('app/backups');
if (Storage::exists('app/backups/'.$file)) {
$headers = ['ContentType' => 'application/zip'];
return response()->download($path.'/'.$file, $file, $headers);
} else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
}
}
/**
* Determines and downloads the latest backup
*
* @author [A. Gianotto]
* @since [v6.3.1]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadLatestBackup() {
$fileData = collect();
foreach (Storage::files('app/backups') as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) == 'zip') {
$fileData->push([
'file' => $file,
'date' => Storage::lastModified($file)
]);
}
}
$newest = $fileData->sortByDesc('date')->first();
if (Storage::exists($newest['file'])) {
$headers = ['ContentType' => 'application/zip'];
return response()->download(storage_path($newest['file']), basename($newest['file']), $headers);
} else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
}
}
}

View File

@@ -5,13 +5,10 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\StatuslabelsTransformer;
use App\Models\Asset;
use App\Models\Statuslabel;
use Illuminate\Http\Request;
use App\Http\Transformers\PieChartTransformer;
use Illuminate\Support\Arr;
class StatuslabelsController extends Controller
{
@@ -33,27 +30,26 @@ class StatuslabelsController extends Controller
$statuslabels = $statuslabels->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$statuslabels->where('name', '=', $request->input('name'));
}
// if a status_type is passed, filter by that
if ($request->filled('status_type')) {
if (strtolower($request->input('status_type')) == 'pending') {
if (strtolower($request->input('status_type'))== 'pending') {
$statuslabels = $statuslabels->Pending();
} elseif (strtolower($request->input('status_type')) == 'archived') {
} elseif (strtolower($request->input('status_type'))== 'archived') {
$statuslabels = $statuslabels->Archived();
} elseif (strtolower($request->input('status_type')) == 'deployable') {
} elseif (strtolower($request->input('status_type'))== 'deployable') {
$statuslabels = $statuslabels->Deployable();
} elseif (strtolower($request->input('status_type')) == 'undeployable') {
} elseif (strtolower($request->input('status_type'))== 'undeployable') {
$statuslabels = $statuslabels->Undeployable();
}
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $statuslabels->count()) ? $statuslabels->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($statuslabels) && ($request->get('offset') > $statuslabels->count())) ? $statuslabels->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -98,8 +94,8 @@ class StatuslabelsController extends Controller
if ($statuslabel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $statuslabel, trans('admin/statuslabels/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $statuslabel->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', null, $statuslabel->getErrors()));
}
/**
@@ -118,7 +114,6 @@ class StatuslabelsController extends Controller
return (new StatuslabelsTransformer)->transformStatuslabel($statuslabel);
}
/**
* Update the specified resource in storage.
*
@@ -135,7 +130,6 @@ class StatuslabelsController extends Controller
$request->except('deployable', 'pending', 'archived');
if (! $request->filled('type')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Status label type is required.'));
}
@@ -181,62 +175,48 @@ class StatuslabelsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/statuslabels/message.assoc_assets')));
}
/**
* Show a count of assets by status label for pie chart
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return array
* @return \Illuminate\Http\Response
*/
public function getAssetCountByStatuslabel()
{
$this->authorize('view', Statuslabel::class);
$statuslabels = Statuslabel::withCount('assets')->get();
$total = Array();
$labels = [];
$points = [];
$default_color_count = 0;
$colors_array = [];
foreach ($statuslabels as $statuslabel) {
if ($statuslabel->assets_count > 0) {
$labels[] = $statuslabel->name.' ('.number_format($statuslabel->assets_count).')';
$points[] = $statuslabel->assets_count;
$total[$statuslabel->name]['label'] = $statuslabel->name;
$total[$statuslabel->name]['count'] = $statuslabel->assets_count;
if ($statuslabel->color != '') {
$total[$statuslabel->name]['color'] = $statuslabel->color;
if ($statuslabel->color != '') {
$colors_array[] = $statuslabel->color;
} else {
$colors_array[] = Helper::defaultChartColors($default_color_count);
}
$default_color_count++;
}
}
return (new PieChartTransformer())->transformPieChartDate($total);
$result = [
'labels' => $labels,
'datasets' => [[
'data' => $points,
'backgroundColor' => $colors_array,
'hoverBackgroundColor' => $colors_array,
]],
];
}
/**
* Show a count of assets by meta status type for pie chart
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0.11]
* @return array
*/
public function getAssetCountByMetaStatus()
{
$this->authorize('view', Statuslabel::class);
$total['rtd']['label'] = trans('general.ready_to_deploy');
$total['rtd']['count'] = Asset::RTD()->count();
$total['deployed']['label'] = trans('general.deployed');
$total['deployed']['count'] = Asset::Deployed()->count();
$total['archived']['label'] = trans('general.archived');
$total['archived']['count'] = Asset::Archived()->count();
$total['pending']['label'] = trans('general.pending');
$total['pending']['count'] = Asset::Pending()->count();
$total['undeployable']['label'] = trans('general.undeployable');
$total['undeployable']['count'] = Asset::Undeployable()->count();
return (new PieChartTransformer())->transformPieChartDate($total);
return $result;
}
/**
@@ -292,45 +272,4 @@ class StatuslabelsController extends Controller
return '0';
}
/**
* Gets a paginated collection for the select2 menus
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1]
* @see \App\Http\Transformers\SelectlistTransformer
*/
public function selectlist(Request $request)
{
$this->authorize('view.selectlists');
$statuslabels = Statuslabel::orderBy('default_label', 'desc')->orderBy('name', 'asc')->orderBy('deployable', 'desc');
if ($request->filled('search')) {
$statuslabels = $statuslabels->where('name', 'LIKE', '%'.$request->get('search').'%');
}
if ($request->filled('deployable')) {
$statuslabels = $statuslabels->where('deployable', '=', '1');
}
if ($request->filled('pending')) {
$statuslabels = $statuslabels->where('pending', '=', '1');
}
if ($request->filled('archived')) {
$statuslabels = $statuslabels->where('archived', '=', '1');
}
$statuslabels = $statuslabels->orderBy('name', 'ASC')->paginate(50);
// Loop through and set some custom properties for the transformer to use.
// This lets us have more flexibility in special cases like assets, where
// they may not have a ->name value but we want to display something anyway
foreach ($statuslabels as $statuslabel) {
$statuslabels->use_text = $statuslabel->name;
}
return (new SelectlistTransformer)->transformSelectlist($statuslabels);
}
}

View File

@@ -23,79 +23,23 @@ class SuppliersController extends Controller
public function index(Request $request)
{
$this->authorize('view', Supplier::class);
$allowed_columns = ['
id',
'name',
'address',
'phone',
'contact',
'fax',
'email',
'image',
'assets_count',
'licenses_count',
'accessories_count',
'components_count',
'consumables_count',
'url',
];
$allowed_columns = ['id', 'name', 'address', 'phone', 'contact', 'fax', 'email', 'image', 'assets_count', 'licenses_count', 'accessories_count', 'url'];
$suppliers = Supplier::select(
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url'])
->withCount('assets as assets_count')
->withCount('licenses as licenses_count')
->withCount('accessories as accessories_count')
->withCount('components as components_count')
->withCount('consumables as consumables_count');
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes']
)->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('accessories as accessories_count');
if ($request->filled('search')) {
$suppliers = $suppliers->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$suppliers->where('name', '=', $request->input('name'));
}
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($suppliers) && ($request->get('offset') > $suppliers->count())) ? $suppliers->count() : $request->get('offset', 0);
if ($request->filled('address')) {
$suppliers->where('address', '=', $request->input('address'));
}
if ($request->filled('address2')) {
$suppliers->where('address2', '=', $request->input('address2'));
}
if ($request->filled('city')) {
$suppliers->where('city', '=', $request->input('city'));
}
if ($request->filled('zip')) {
$suppliers->where('zip', '=', $request->input('zip'));
}
if ($request->filled('country')) {
$suppliers->where('country', '=', $request->input('country'));
}
if ($request->filled('fax')) {
$suppliers->where('fax', '=', $request->input('fax'));
}
if ($request->filled('email')) {
$suppliers->where('email', '=', $request->input('email'));
}
if ($request->filled('url')) {
$suppliers->where('url', '=', $request->input('url'));
}
if ($request->filled('notes')) {
$suppliers->where('notes', '=', $request->input('notes'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $suppliers->count()) ? $suppliers->count() : app('api_offset_value');
$limit = app('api_limit_value');
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';

View File

@@ -11,19 +11,14 @@ use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\UsersTransformer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Accessory;
use App\Models\Consumable;
use App\Models\Company;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
use Illuminate\Support\Facades\Auth;
use Auth;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use App\Http\Requests\DeleteUserRequest;
class UsersController extends Controller
{
@@ -33,7 +28,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
*
* @return array
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
@@ -41,7 +36,6 @@ class UsersController extends Controller
$users = User::select([
'users.activated',
'users.created_by',
'users.address',
'users.avatar',
'users.city',
@@ -71,18 +65,16 @@ class UsersController extends Controller
'users.zip',
'users.remote',
'users.ldap_import',
'users.start_date',
'users.end_date',
'users.vip',
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables')
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count');
$users = Company::scopeCompanyables($users);
if ($request->filled('search') != '') {
$users = $users->TextSearch($request->input('search'));
if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
$users = $users->onlyTrashed();
} elseif (($request->filled('all')) && ($request->input('all') == 'true')) {
$users = $users->withTrashed();
}
if ($request->filled('activated')) {
@@ -97,10 +89,6 @@ class UsersController extends Controller
$users = $users->where('users.location_id', '=', $request->input('location_id'));
}
if ($request->filled('created_by')) {
$users = $users->where('users.created_by', '=', $request->input('created_by'));
}
if ($request->filled('email')) {
$users = $users->where('users.email', '=', $request->input('email'));
}
@@ -129,10 +117,6 @@ class UsersController extends Controller
$users = $users->where('users.country', '=', $request->input('country'));
}
if ($request->filled('website')) {
$users = $users->where('users.website', '=', $request->input('website'));
}
if ($request->filled('zip')) {
$users = $users->where('users.zip', '=', $request->input('zip'));
}
@@ -157,26 +141,6 @@ class UsersController extends Controller
$users = $users->where('remote', '=', $request->input('remote'));
}
if ($request->filled('vip')) {
$users = $users->where('vip', '=', $request->input('vip'));
}
if ($request->filled('two_factor_enrolled')) {
$users = $users->where('two_factor_enrolled', '=', $request->input('two_factor_enrolled'));
}
if ($request->filled('two_factor_optin')) {
$users = $users->where('two_factor_optin', '=', $request->input('two_factor_optin'));
}
if ($request->filled('start_date')) {
$users = $users->where('users.start_date', '=', $request->input('start_date'));
}
if ($request->filled('end_date')) {
$users = $users->where('users.end_date', '=', $request->input('end_date'));
}
if ($request->filled('assets_count')) {
$users->has('assets', '=', $request->input('assets_count'));
}
@@ -193,26 +157,20 @@ class UsersController extends Controller
$users->has('accessories', '=', $request->input('accessories_count'));
}
if ($request->filled('manages_users_count')) {
$users->has('manages_users_count', '=', $request->input('manages_users_count'));
}
if ($request->filled('manages_locations_count')) {
$users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
}
if ($request->filled('autoassign_licenses')) {
$users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
}
if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
$users = $users->onlyTrashed();
} elseif (($request->filled('all')) && ($request->input('all') == 'true')) {
$users = $users->withTrashed();
if ($request->filled('search')) {
$users = $users->TextSearch($request->input('search'));
}
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$offset = (($users) && (request('offset') > $users->count())) ? 0 : request('offset', 0);
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
// case we override with the actual count, so we should return 0 items.
$offset = (($users) && ($request->get('offset') > $users->count())) ? $users->count() : $request->get('offset', 0);
// Check to make sure the limit is not higher than the max allowed
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
switch ($request->input('sort')) {
case 'manager':
@@ -224,70 +182,24 @@ class UsersController extends Controller
case 'department':
$users = $users->OrderDepartment($order);
break;
case 'created_by':
$users = $users->OrderByCreatedBy($order);
break;
case 'company':
$users = $users->OrderCompany($order);
break;
case 'first_name':
$users->orderBy('first_name', $order);
$users->orderBy('last_name', $order);
break;
case 'last_name':
$users->orderBy('last_name', $order);
$users->orderBy('first_name', $order);
break;
default:
$allowed_columns =
[
'last_name',
'first_name',
'email',
'jobtitle',
'username',
'employee_num',
'groups',
'activated',
'created_at',
'two_factor_enrolled',
'two_factor_optin',
'last_login',
'assets_count',
'licenses_count',
'consumables_count',
'accessories_count',
'manages_users_count',
'manages_locations_count',
'phone',
'address',
'city',
'state',
'country',
'zip',
'id',
'ldap_import',
'two_factor_optin',
'two_factor_enrolled',
'remote',
'vip',
'start_date',
'end_date',
'autoassign_licenses',
'website',
'last_name', 'first_name', 'email', 'jobtitle', 'username', 'employee_num',
'assets', 'accessories', 'consumables', 'licenses', 'groups', 'activated', 'created_at',
'two_factor_enrolled', 'two_factor_optin', 'last_login', 'assets_count', 'licenses_count',
'consumables_count', 'accessories_count', 'phone', 'address', 'city', 'state',
'country', 'zip', 'id', 'ldap_import', 'remote',
];
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
$sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name';
$users = $users->orderBy($sort, $order);
break;
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
$limit = app('api_limit_value');
$total = $users->count();
$users = $users->skip($offset)->take($limit)->get();
@@ -316,12 +228,12 @@ class UsersController extends Controller
]
)->where('show_in_list', '=', '1');
$users = Company::scopeCompanyables($users);
if ($request->filled('search')) {
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
});
$users = $users->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
}
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
@@ -357,7 +269,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param \Illuminate\Http\Request $request
* @return array | \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function store(SaveUserRequest $request)
{
@@ -365,7 +277,6 @@ class UsersController extends Controller
$user = new User;
$user->fill($request->all());
$user->created_by = Auth::user()->id;
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
@@ -377,12 +288,8 @@ class UsersController extends Controller
$user->permissions = $permissions_array;
}
//
if ($request->filled('password')) {
$user->password = bcrypt($request->get('password'));
} else {
$user->password = $user->noPassword();
}
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20);
$user->password = bcrypt($request->get('password', $tmp_pass));
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
@@ -404,19 +311,14 @@ class UsersController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
* @return array | \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$this->authorize('view', User::class);
$user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count')->findOrFail($id);
if ($user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count')->find($id)) {
$this->authorize('view', $user);
return (new UsersTransformer)->transformUser($user);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
return (new UsersTransformer)->transformUser($user);
}
@@ -427,89 +329,82 @@ class UsersController extends Controller
* @since [v4.0]
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function update(SaveUserRequest $request, $id)
{
$this->authorize('update', User::class);
if ($user = User::find($id)) {
$user = User::findOrFail($id);
/**
* This is a janky hack to prevent people from changing admin demo user data on the public demo.
*
* The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
*
* Thanks, jerks. You are why we can't have nice things. - snipe
*
*/
$this->authorize('update', $user);
/**
* This is a janky hack to prevent people from changing admin demo user data on the public demo.
* The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
* Thanks, jerks. You are why we can't have nice things. - snipe
*
*/
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
}
$user->fill($request->all());
if ($user->id == $request->input('manager_id')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// We need to use has() instead of filled()
// here because we need to overwrite permissions
// if someone needs to null them out
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
// Strip out the individual superuser permission if the API user isn't a superadmin
if (!Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
}
$user->permissions = $permissions_array;
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
if (($request->has('groups')) && (Auth::user()->isSuperUser())) {
$validator = Validator::make($request->only('groups'), [
'groups.*' => 'integer|exists:permission_groups,id',
]);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()));
}
// Sync the groups since the user is a superuser and the groups pass validation
$user->groups()->sync($request->input('groups'));
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
$user->fill($request->all());
if ($user->id == $request->input('manager_id')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// We need to use has() instead of filled()
// here because we need to overwrite permissions
// if someone needs to null them out
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
// Strip out the superuser permission if the API user isn't a superadmin
if (! Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
}
$user->permissions = $permissions_array;
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
if ($user->save()) {
// Sync group memberships:
// This was changed in Snipe-IT v4.6.x to 4.7, since we upgraded to Laravel 5.5
// which changes the behavior of has vs filled.
// The $request->has method will now return true even if the input value is an empty string or null.
// A new $request->filled method has was added that provides the previous behavior of the has method.
// Check if the request has groups passed and has a value
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
// The groups field has been passed but it is null, so we should blank it out
} elseif ($request->has('groups')) {
$user->groups()->sync([]);
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
/**
@@ -518,36 +413,45 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function destroy(DeleteUserRequest $request, $id)
public function destroy($id)
{
$this->authorize('delete', User::class);
$user = User::findOrFail($id);
$this->authorize('delete', $user);
if ($user = User::withTrashed()->find($id)) {
$this->authorize('delete', $user);
if ($user->delete()) {
// Remove the user's avatar if they have one
if (Storage::disk('public')->exists('avatars/' . $user->avatar)) {
try {
Storage::disk('public')->delete('avatars/' . $user->avatar);
} catch (\Exception $e) {
Log::debug($e);
}
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
if (($user->assets) && ($user->assets->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete_has_assets')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')));
if (($user->licenses) && ($user->licenses->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->licenses->count().' license(s) associated with them and cannot be deleted.'));
}
if (($user->accessories) && ($user->accessories->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->accessories->count().' accessories associated with them.'));
}
if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->managedLocations()->count().' locations that they manage.'));
}
if ($user->delete()) {
// Remove the user's avatar if they have one
if (Storage::disk('public')->exists('avatars/'.$user->avatar)) {
try {
Storage::disk('public')->delete('avatars/'.$user->avatar);
} catch (\Exception $e) {
\Log::debug($e);
}
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
}
/**
@@ -556,72 +460,17 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function assets(Request $request, $id)
{
$this->authorize('view', User::class);
$this->authorize('view', Asset::class);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model')->get();
if ($user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id)) {
$this->authorize('view', $user);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');
// Filter on category ID
if ($request->filled('category_id')) {
$assets = $assets->InCategory($request->input('category_id'));
}
// Filter on model ID
if ($request->filled('model_id')) {
$model_ids = $request->input('model_id');
if (!is_array($model_ids)) {
$model_ids = array($model_ids);
}
$assets = $assets->InModelList($model_ids);
}
$assets = $assets->get();
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
/**
* Notify a specific user via email with all of their assigned assets.
*
* @author [Lukas Fehling] [<lukas.fehling@adabay.rocks>]
* @since [v6.0.13]
* @param Request $request
* @param $id
* @return string JSON
*/
public function emailAssetList(Request $request, $id)
{
$this->authorize('update', User::class);
if ($user = User::find($id)) {
$this->authorize('update', $user);
if (empty($user->email)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
}
$user->notify((new CurrentInventory($user)));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.inventorynotification.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
}
/**
* Return JSON containing a list of consumables assigned to a user.
@@ -629,14 +478,13 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function consumables(Request $request, $id)
{
$this->authorize('view', User::class);
$this->authorize('view', Consumable::class);
$user = User::findOrFail($id);
$this->authorize('view', $user);
$consumables = $user->consumables;
return (new ConsumablesTransformer)->transformConsumables($consumables, $consumables->count(), $request);
}
@@ -647,13 +495,12 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.6.14]
* @param $userId
* @return array
* @return string JSON
*/
public function accessories($id)
{
$this->authorize('view', User::class);
$user = User::findOrFail($id);
$this->authorize('view', $user);
$this->authorize('view', Accessory::class);
$accessories = $user->accessories;
@@ -666,21 +513,16 @@ class UsersController extends Controller
* @author [N. Mathar] [<snipe@snipe.net>]
* @since [v5.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function licenses($id)
{
$this->authorize('view', User::class);
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
$user = User::where('id', $id)->withTrashed()->first();
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
/**
@@ -698,20 +540,9 @@ class UsersController extends Controller
if ($request->filled('id')) {
try {
$user = User::find($request->get('id'));
$this->authorize('update', $user);
$user->two_factor_secret = null;
$user->two_factor_enrolled = 0;
$user->saveQuietly();
// Log the reset
$logaction = new Actionlog();
$logaction->target_type = User::class;
$logaction->target_id = $user->id;
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('2FA reset');
$user->save();
return response()->json(['message' => trans('admin/settings/general.two_factor_reset_success')], 200);
} catch (\Exception $e) {
@@ -729,7 +560,7 @@ class UsersController extends Controller
* @author [Juan Font] [<juanfontalonso@gmail.com>]
* @since [v4.4.2]
* @param \Illuminate\Http\Request $request
* @return array
* @return \Illuminate\Http\Response
*/
public function getCurrentUserInfo(Request $request)
{
@@ -742,35 +573,21 @@ class UsersController extends Controller
* @author [E. Taylor] [<dev@evantaylor.name>]
* @param int $userId
* @since [v6.0.0]
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function restore($userId)
public function restore($userId = null)
{
$this->authorize('delete', User::class);
if ($user = User::withTrashed()->find($userId)) {
$this->authorize('delete', $user);
if ($user->deleted_at == '') {
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.user')])), 200);
}
if ($user->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')), 200);
}
// Get asset information
$user = User::withTrashed()->find($userId);
$this->authorize('delete', $user);
if (isset($user->id)) {
// Restore the user
User::withTrashed()->where('id', $userId)->restore();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
$id = $userId;
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))), 200);
}
}

View File

@@ -6,7 +6,7 @@ use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Auth;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Slack;
@@ -101,7 +101,7 @@ class AssetMaintenancesController extends Controller
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find($request->input('asset_id'));
@@ -148,20 +148,30 @@ class AssetMaintenancesController extends Controller
*/
public function edit($assetMaintenanceId = null)
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
// Redirect to the improvement management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! $assetMaintenance->asset) {
return redirect()->route('maintenances.index')
->with('error', 'The asset associated with this maintenance does not exist.');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
if ($assetMaintenance->completion_date == '0000-00-00') {
$assetMaintenance->completion_date = null;
}
if ($assetMaintenance->start_date == '0000-00-00') {
$assetMaintenance->start_date = null;
}
if ($assetMaintenance->cost == '0.00') {
$assetMaintenance->cost = null;
}
// Prepare Improvement Type List
$assetMaintenanceType = [
@@ -193,17 +203,15 @@ class AssetMaintenancesController extends Controller
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find(request('asset_id'));

View File

@@ -4,19 +4,14 @@ namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Request;
use Storage;
use Symfony\Component\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* This class controls all actions related to asset models for
@@ -66,7 +61,7 @@ class AssetModelsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param ImageUploadRequest $request
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function store(ImageUploadRequest $request)
@@ -80,15 +75,14 @@ class AssetModelsController extends Controller
$model->depreciation_id = $request->input('depreciation_id');
$model->name = $request->input('name');
$model->model_number = $request->input('model_number');
$model->min_amt = $request->input('min_amt');
$model->manufacturer_id = $request->input('manufacturer_id');
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->user_id = Auth::id();
$model->requestable = $request->has('requestable');
$model->requestable = Request::has('requestable');
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
if ($request->input('custom_fieldset') != '') {
$model->fieldset_id = e($request->input('custom_fieldset'));
}
$model = $request->handleImages($model);
@@ -96,11 +90,10 @@ class AssetModelsController extends Controller
// Was it created?
if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
}
// Redirect to the new model page
return redirect()->route('models.index')->with('success', trans('admin/models/message.create.success'));
}
@@ -139,7 +132,7 @@ class AssetModelsController extends Controller
* @since [v1.0]
* @param ImageUploadRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(ImageUploadRequest $request, $modelId = null)
@@ -157,7 +150,6 @@ class AssetModelsController extends Controller
$model->eol = $request->input('eol');
$model->name = $request->input('name');
$model->model_number = $request->input('model_number');
$model->min_amt = $request->input('min_amt');
$model->manufacturer_id = $request->input('manufacturer_id');
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
@@ -165,28 +157,17 @@ class AssetModelsController extends Controller
$this->removeCustomFieldsDefaultValues($model);
$model->fieldset_id = $request->input('fieldset_id');
if ($request->input('custom_fieldset') == '') {
$model->fieldset_id = null;
} else {
$model->fieldset_id = $request->input('custom_fieldset');
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
if ($this->shouldAddDefaultValues($request->input())) {
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
}
}
if ($model->save()) {
if ($model->wasChanged('eol')) {
if ($model->eol > 0) {
$newEol = $model->eol;
$model->assets()->whereNotNull('purchase_date')->where('eol_explicit', false)
->update(['asset_eol_date' => DB::raw('DATE_ADD(purchase_date, INTERVAL ' . $newEol . ' MONTH)')]);
} elseif ($model->eol == 0) {
$model->assets()->whereNotNull('purchase_date')->where('eol_explicit', false)
->update(['asset_eol_date' => DB::raw('null')]);
}
}
return redirect()->route('models.index')->with('success', trans('admin/models/message.update.success'));
}
@@ -200,7 +181,7 @@ class AssetModelsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($modelId)
@@ -208,7 +189,7 @@ class AssetModelsController extends Controller
$this->authorize('delete', AssetModel::class);
// Check if the model exists
if (is_null($model = AssetModel::find($modelId))) {
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
return redirect()->route('models.index')->with('error', trans('admin/models/message.not_found'));
}
if ($model->assets()->count() > 0) {
@@ -220,7 +201,7 @@ class AssetModelsController extends Controller
try {
Storage::disk('public')->delete('models/'.$model->image);
} catch (\Exception $e) {
Log::info($e);
\Log::info($e);
}
}
@@ -236,42 +217,22 @@ class AssetModelsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $id
* @return \Illuminate\Http\RedirectResponse
* @param int $modelId
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getRestore($id)
public function getRestore($modelId = null)
{
$this->authorize('create', AssetModel::class);
// Get user information
$model = AssetModel::withTrashed()->find($modelId);
if ($model = AssetModel::withTrashed()->find($id)) {
if (isset($model->id)) {
$model->restore();
if ($model->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset_model')]));
}
if ($model->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_id = $model->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
// Redirect them to the deleted page if there are more, otherwise the section index
$deleted_models = AssetModel::onlyTrashed()->count();
if ($deleted_models > 0) {
return redirect()->back()->with('success', trans('admin/models/message.restore.success'));
}
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
}
// Check validation
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset_model'), 'error' => $model->getErrors()->first()]));
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
}
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
return redirect()->back()->with('error', trans('admin/models/message.not_found'));
}
@@ -320,7 +281,6 @@ class AssetModelsController extends Controller
return view('models/edit')
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
->with('model_id', $model_to_clone->id)
->with('clone_model', $model_to_clone);
}
@@ -429,7 +389,7 @@ class AssetModelsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @return Redirect
*/
public function postBulkDelete(Request $request)
{
@@ -442,6 +402,7 @@ class AssetModelsController extends Controller
$del_count = 0;
foreach ($models as $model) {
\Log::debug($model->id);
if ($model->assets_count > 0) {
$del_error_count++;
@@ -451,6 +412,8 @@ class AssetModelsController extends Controller
}
}
\Log::debug($del_count);
\Log::debug($del_error_count);
if ($del_error_count == 0) {
return redirect()->route('models.index')
@@ -476,7 +439,7 @@ class AssetModelsController extends Controller
{
return ! empty($input['add_default_values'])
&& ! empty($input['default_values'])
&& ! empty($input['fieldset_id']);
&& ! empty($input['custom_fieldset']);
}
/**
@@ -486,34 +449,8 @@ class AssetModelsController extends Controller
* @param array $defaultValues
* @return void
*/
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues): bool
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues)
{
$data = array();
foreach ($defaultValues as $customFieldId => $defaultValue) {
$customField = CustomField::find($customFieldId);
$data[$customField->db_column] = $defaultValue;
}
$fieldsets = $model->fieldset->validation_rules();
$rules = array();
foreach ($fieldsets as $fieldset => $validation){
// If the field is marked as required, eliminate the rule so it doesn't interfere with the default values
// (we are at model level, the rule still applies when creating a new asset using this model)
$index = array_search('required', $validation);
if ($index !== false){
$validation[$index] = 'nullable';
}
$rules[$fieldset] = $validation;
}
$validator = Validator::make($data, $rules);
if($validator->fails()){
return false;
}
foreach ($defaultValues as $customFieldId => $defaultValue) {
if(is_array($defaultValue)){
$model->defaultValues()->attach($customFieldId, ['default_value' => implode(', ', $defaultValue)]);
@@ -521,7 +458,6 @@ class AssetModelsController extends Controller
$model->defaultValues()->attach($customFieldId, ['default_value' => $defaultValue]);
}
}
return true;
}
/**

View File

@@ -1,133 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\AssetModel;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
class AssetModelsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $modelId = null)
{
if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $model);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$model->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->with('success', trans('general.file_upload_success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
* @return View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($modelId = null, $fileId = null)
{
$model = AssetModel::find($modelId);
// the asset is valid
if (isset($model->id)) {
$this->authorize('view', $model);
if (! $log = Actionlog::find($fileId)) {
return response('No matching record for that model/file', 500)
->header('Content-Type', 'text/plain');
}
$file = 'private_uploads/assetmodels/'.$log->filename;
if (! Storage::exists($file)) {
return response('File '.$file.' not found on server', 404)
->header('Content-Type', 'text/plain');
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Prepare the error message
$error = trans('admin/hardware/message.does_not_exist', ['id' => $fileId]);
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', $error);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
* @return View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($modelId = null, $fileId = null)
{
$model = AssetModel::find($modelId);
$this->authorize('update', $model);
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($model->id)) {
$this->authorize('update', $model);
$log = Actionlog::find($fileId);
if ($log) {
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->back()
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
}

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