Compare commits

..

1 Commits

Author SHA1 Message Date
snipe
5fbafb8a6a Possible fix for logout session not being flushed
Signed-off-by: snipe <snipe@snipe.net>
2022-05-05 13:06:58 -07:00
11041 changed files with 422079 additions and 616388 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
# https://mariadb.com/kb/en/mariadb-server-docker-official-image-environment-variables/
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=develop
APP_DEBUG=true
# 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-US
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_DUMP_SKIP_SSL=true
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: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
# --------------------------------------------
#PHP_UPLOAD_LIMIT=10
#PHP_POST_MAX_SIZE=10
#PHP_UPLOAD_MAX_FILESIZE=10
#PHP_MEMORY_LIMIT=10
# --------------------------------------------
# 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=single
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,20 +1,20 @@
# -------------------------------------------- # --------------------------------------------
# REQUIRED: DOCKER SPECIFIC SETTINGS # REQUIRED: DB SETUP
# -------------------------------------------- # --------------------------------------------
APP_VERSION= MYSQL_DATABASE=snipeit
APP_PORT=8000 MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# -------------------------------------------- # --------------------------------------------
# REQUIRED: BASIC APP SETTINGS # REQUIRED: BASIC APP SETTINGS
# -------------------------------------------- # --------------------------------------------
APP_ENV=production APP_ENV=develop
APP_DEBUG=false APP_DEBUG=false
# Please regenerate the APP_KEY value by calling `docker compose run --rm app 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_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000 APP_URL=http://localhost:8000
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier
APP_TIMEZONE='UTC' APP_TIMEZONE='UTC'
APP_LOCALE=en-US APP_LOCALE=en
MAX_RESULTS=500 MAX_RESULTS=500
# -------------------------------------------- # --------------------------------------------
@@ -27,16 +27,12 @@ PUBLIC_FILESYSTEM_DISK=local_public
# REQUIRED: DATABASE SETTINGS # REQUIRED: DATABASE SETTINGS
# -------------------------------------------- # --------------------------------------------
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=db DB_HOST=mariadb
DB_SOCKET=null
DB_PORT='3306'
DB_DATABASE=snipeit DB_DATABASE=snipeit
DB_USERNAME=snipeit DB_USERNAME=snipeit
DB_PASSWORD=changeme1234 DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null DB_PREFIX=null
DB_DUMP_PATH='/usr/bin' DB_DUMP_PATH='/usr/bin'
DB_DUMP_SKIP_SSL=true
DB_CHARSET=utf8mb4 DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci DB_COLLATION=utf8mb4_unicode_ci
@@ -49,35 +45,29 @@ DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# -------------------------------------------- # --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS # REQUIRED: OUTGOING MAIL SERVER SETTINGS
# -------------------------------------------- # --------------------------------------------
MAIL_MAILER=smtp MAIL_DRIVER=smtp
MAIL_HOST=mailhog MAIL_HOST=mailhog
MAIL_PORT=1025 MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_TLS_VERIFY_PEER=true MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT' MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT' MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment' MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: DATA PROTECTION
# --------------------------------------------
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# -------------------------------------------- # --------------------------------------------
# REQUIRED: IMAGE LIBRARY # REQUIRED: IMAGE LIBRARY
# This should be gd or imagick # This should be gd or imagick
# -------------------------------------------- # --------------------------------------------
IMAGE_LIB=gd IMAGE_LIB=gd
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: BACKUP SETTINGS # OPTIONAL: BACKUP SETTINGS
# -------------------------------------------- # --------------------------------------------
@@ -85,14 +75,6 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
# --------------------------------------------
#PHP_UPLOAD_LIMIT=10
#PHP_POST_MAX_SIZE=10
#PHP_UPLOAD_MAX_FILESIZE=10
#PHP_MEMORY_LIMIT=10
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SESSION SETTINGS # OPTIONAL: SESSION SETTINGS
@@ -108,7 +90,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS # OPTIONAL: SECURITY HEADER SETTINGS
# -------------------------------------------- # --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.16.0.0/12 APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin REFERRER_POLICY=same-origin
ENABLE_CSP=false ENABLE_CSP=false
@@ -126,7 +108,7 @@ CACHE_PREFIX=snipeit
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: REDIS SETTINGS # OPTIONAL: REDIS SETTINGS
# -------------------------------------------- # --------------------------------------------
REDIS_HOST=null REDIS_HOST=redis
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
@@ -156,29 +138,20 @@ PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=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 # OPTIONAL: LOGIN THROTTLING
# -------------------------------------------- # --------------------------------------------
LOGIN_MAX_ATTEMPTS=5 LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60 LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900 RESET_PASSWORD_LINK_EXPIRES=900
INVITE_PASSWORD_LINK_EXPIRES=1500
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: MISC # OPTIONAL: MISC
# -------------------------------------------- # --------------------------------------------
LOG_CHANNEL=stderr APP_LOG=stderr
LOG_MAX_DAYS=10 APP_LOG_MAX_FILES=10
APP_LOCKED=false APP_LOCKED=false
APP_CIPHER=AES-256-CBC APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API= GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600 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-US
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

View File

@@ -6,7 +6,7 @@ APP_DEBUG=false
APP_KEY=ChangeMe APP_KEY=ChangeMe
APP_URL=null APP_URL=null
APP_TIMEZONE='UTC' APP_TIMEZONE='UTC'
APP_LOCALE='en-US' APP_LOCALE=en
MAX_RESULTS=500 MAX_RESULTS=500
# -------------------------------------------- # --------------------------------------------
@@ -15,27 +15,18 @@ MAX_RESULTS=500
PRIVATE_FILESYSTEM_DISK=local PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public PUBLIC_FILESYSTEM_DISK=local_public
#PRIVATE_FILESYSTEM_DISK=s3_private
#PUBLIC_FILESYSTEM_DISK=s3_public
# -------------------------------------------- # --------------------------------------------
# REQUIRED: DATABASE SETTINGS # REQUIRED: DATABASE SETTINGS
# -------------------------------------------- # --------------------------------------------
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_SOCKET=null
DB_PORT=3306
DB_DATABASE=null DB_DATABASE=null
DB_USERNAME=null DB_USERNAME=null
DB_PASSWORD=null DB_PASSWORD=null
DB_PREFIX=null DB_PREFIX=null
DB_DUMP_PATH='/usr/bin' DB_DUMP_PATH='/usr/bin'
DB_DUMP_SKIP_SSL=false
DB_CHARSET=utf8mb4 DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci DB_COLLATION=utf8mb4_unicode_ci
DB_SANITIZE_BY_DEFAULT=false
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS # OPTIONAL: SSL DATABASE SETTINGS
@@ -46,26 +37,21 @@ DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# -------------------------------------------- # --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS # REQUIRED: OUTGOING MAIL SERVER SETTINGS
# -------------------------------------------- # --------------------------------------------
MAIL_MAILER=smtp MAIL_DRIVER=smtp
MAIL_HOST=email-smtp.us-west-2.amazonaws.com MAIL_HOST=email-smtp.us-west-2.amazonaws.com
MAIL_PORT=587 MAIL_PORT=587
MAIL_USERNAME=YOURUSERNAME MAIL_USERNAME=YOURUSERNAME
MAIL_PASSWORD=YOURPASSWORD MAIL_PASSWORD=YOURPASSWORD
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT' MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT' MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment' 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 # REQUIRED: IMAGE LIBRARY
@@ -80,29 +66,18 @@ IMAGE_LIB=gd
MAIL_BACKUP_NOTIFICATION_DRIVER=null MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true BACKUP_ENV=true
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
ALL_BACKUP_KEEP_DAYS=7
DAILY_BACKUP_KEEP_DAYS=16
WEEKLY_BACKUP_KEEP_WEEKS=8
MONTHLY_BACKUP_KEEP_MONTHS=4
YEARLY_BACKUP_KEEP_YEARS=2
BACKUP_PURGE_OLDEST_AT_MEGS=5000
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SESSION SETTINGS # OPTIONAL: SESSION SETTINGS
# -------------------------------------------- # --------------------------------------------
SESSION_DRIVER=file
SESSION_LIFETIME=12000 SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false EXPIRE_ON_CLOSE=false
ENCRYPT=false ENCRYPT=false
COOKIE_NAME=snipeit_session COOKIE_NAME=snipeit_session
PASSPORT_COOKIE_NAME='snipeit_passport_token'
COOKIE_DOMAIN=null COOKIE_DOMAIN=null
SECURE_COOKIES=false SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15 API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=localStorage
BS_TABLE_DEEPLINK=true
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS # OPTIONAL: SECURITY HEADER SETTINGS
@@ -111,7 +86,6 @@ APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin REFERRER_POLICY=same-origin
ENABLE_CSP=false ENABLE_CSP=false
ADDITIONAL_CSP_URLS=null
CORS_ALLOWED_ORIGINS=null CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false ENABLE_HSTS=false
@@ -119,6 +93,7 @@ ENABLE_HSTS=false
# OPTIONAL: CACHE SETTINGS # OPTIONAL: CACHE SETTINGS
# -------------------------------------------- # --------------------------------------------
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit CACHE_PREFIX=snipeit
@@ -155,34 +130,18 @@ PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=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 # OPTIONAL: LOGIN THROTTLING
# -------------------------------------------- # --------------------------------------------
LOGIN_MAX_ATTEMPTS=5 LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60 LOGIN_LOCKOUT_DURATION=60
LOGIN_AUTOCOMPLETE=false RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: FORGOTTEN PASSWORD SETTINGS
# --------------------------------------------
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
INVITE_PASSWORD_LINK_EXPIRES=1500
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: MISC # OPTIONAL: MISC
# -------------------------------------------- # --------------------------------------------
LOG_CHANNEL=single APP_LOG=single
LOG_DEPRECATIONS=false APP_LOG_MAX_FILES=10
LOG_MAX_DAYS=10
APP_LOCKED=false APP_LOCKED=false
APP_CIPHER=AES-256-CBC APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false APP_FORCE_TLS=false
@@ -190,32 +149,8 @@ APP_ALLOW_INSECURE_HOSTS=false
GOOGLE_MAPS_API= GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600 LDAP_TIME_LIM=600
BACKUP_TIME_LIMIT=600
IMPORT_TIME_LIMIT=600 IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000 REPORT_TIME_LIMIT=12000
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
LIVEWIRE_URL_PREFIX=null
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
REQUIRE_SAML=false REQUIRE_SAML=false
SAML_KEY_SIZE=2048 API_THROTTLE_PER_MINUTE=120
# --------------------------------------------
# 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

@@ -1,29 +1,34 @@
# -------------------------------------------- # --------------------------------------------
# REQUIRED: BASIC APP SETTINGS # REQUIRED: BASIC APP SETTINGS
# -------------------------------------------- # --------------------------------------------
APP_ENV='testing-ci' APP_ENV=testing-ci
APP_DEBUG=false APP_DEBUG=false
APP_KEY='base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=' APP_KEY=ChangeMe
APP_URL='http://localhost:8000' APP_URL=http://localhost:8000
APP_TIMEZONE='US/Pacific' APP_TIMEZONE='US/Pacific'
APP_LOCALE='en-US' APP_LOCALE=en
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
# -------------------------------------------- # --------------------------------------------
# REQUIRED: DATABASE SETTINGS # REQUIRED: DATABASE SETTINGS
# -------------------------------------------- # --------------------------------------------
DB_CONNECTION=sqlite DB_CONNECTION=mysql
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_DATABASE=snipeit_unit
DB_DATABASE='sqlite_testing'
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD=null DB_PASSWORD=null
# -------------------------------------------- # --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS # REQUIRED: OUTGOING MAIL SERVER SETTINGS
# -------------------------------------------- # --------------------------------------------
MAIL_MAILER=log 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 # REQUIRED: IMAGE LIBRARY
@@ -32,7 +37,37 @@ MAIL_MAILER=log
IMAGE_LIB=gd 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 # 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_CONNECTION=mysql
DB_DEFAULT=mysql DB_DEFAULT=mysql
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=snipeittests DB_DATABASE=snipeittests
DB_USERNAME=snipeit DB_USERNAME=snipeit
DB_PASSWORD=snipe DB_PASSWORD=snipe
@@ -18,6 +17,6 @@ APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
LOGIN_MAX_ATTEMPTS=1000000 LOGIN_MAX_ATTEMPTS=1000000
LOGIN_LOCKOUT_DURATION=100000000 LOGIN_LOCKOUT_DURATION=100000000
MAIL_MAILER=log MAIL_DRIVER=log
MAIL_FROM_ADDR=you@example.com MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT MAIL_FROM_NAME=Snipe-IT

View File

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

2
.github/CODEOWNERS vendored
View File

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

38
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,38 @@
#### Expected Behavior (or desired behavior if a feature request)
(what you expect to happen goes here)
-----
#### Actual Behavior
(what actually happens goes here)
-----
#### Please confirm you have done the following before posting your bug report:
- [ ] I have enabled debug mode
- [ ] I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
-----
#### Provide answers to these questions:
- Is this a fresh install or an upgrade?
- Version of Snipe-IT you're running
- Version of PHP you're running
- Version of MySQL/MariaDB you're running
- What OS and web server you're running Snipe-IT on
- What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
- WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
- What specific Snipe-IT page you're on, and what specific element you're interacting with to trigger the error
- If a stacktrace is provided in the error, include that too.
- Any errors that appear in your browser's error console.
- Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
- Include any additional information you can find in `storage/logs` and your webserver's logs.
- Include what you've done so far in the installation, and if you got any error messages along the way.
- Indicate whether or not you've manually edited any data directly in the database
Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.
https://snipe-it.readme.io/docs/getting-help

View File

@@ -1,163 +0,0 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
projects: ["grokability/snipe-it"]
type: bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Most issues are documented in the [Snipe-IT repository's issues](https://github.com/grokability/snipe-it/issues) or in the official [Common Issues section of the Documentation](https://snipe-it.readme.io/docs/common-issues#/) and are due to the following:
- `.env` misconfiguration
- [Server Permissions](https://snipe-it.readme.io/docs/debugging-permissions#/)
- [Database Migrations](https://snipe-it.readme.io/docs/database-issues#run-migrations)
Please make sure you've checked these resources before submitting a new issue. If you find an existing issue, please add your context to it instead of opening a new issue. If your issue is more of a question, consider [opening a new discussion](https://github.com/grokability/snipe-it/discussions) or [pop by our Discord](https://discord.gg/yZFtShAcKk) instead of creating an issue.
**Please write your bug report in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you seeing this issue on? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.2 - build 19577 (master)
validations:
required: true
- type: input
id: php-version
attributes:
label: PHP Version
description: What version of PHP are you running? You can find the version of PHP your webserver is running in the `Admin Settings` section in the footer, and the cli version by running `php -v` via command line .
placeholder: ex. v8.3.1 (web), PHP 8.4.12 (cli)
validations:
required: true
- type: input
id: composer-version
attributes:
label: Composer Version
description: What version of composer are you running? You can find the version number by running `composer --version`.
placeholder: ex. 2.8.10
validations:
required: true
- type: input
id: db-version
attributes:
label: MySQL/MariaDB version
description: What database are you using, and what version?
placeholder: ex. MySQL 5.7
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: How did you install Snipe-IT?
options:
- Git install
- Manual install (downloading zip/tar.gz)
- Docker
- install.sh
- Hosted by Grokability
- Other
- Not sure
validations:
required: true
- type: dropdown
id: upgrade-or-fresh
attributes:
label: Is this a fresh install or an upgrade?
options:
- Fresh install
- Upgrade
- NA
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see! (Be nice!)
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other
- type: dropdown
id: on-demo
attributes:
label: Can you reproduce this on the public demo?
description: You can check this at https://demo.snipeitapp.com.
options:
- 'Yes'
- 'No'
- N/A
validations:
required: true
- type: dropdown
id: fmcs
attributes:
label: Do you have full multiple company support enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: fmcs-location
attributes:
label: If you have full multiple company support enabled, do you have location scoping to company enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
- I do not have full multiple company support enabled
validations:
required: true
- type: textarea
id: server-logs
attributes:
label: Application log output
description: Please copy and paste any relevant log output from `storage/logs/laravel.log`. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: browser-logs
attributes:
label: Browser console output
description: Please copy and paste any relevant log output from your browser console. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: common-issues
attributes:
label: Common Issues
description: Please make sure you have done the following before submitting your issue.
options:
- label: I have searched this repo for existing issues related to my issue (including closed issues)
required: true
- label: My APP_URL is set correctly in my .env file (including http or https and no trailing slash)
required: true
- label: I have searched the official Snipe-IT documentation and have checked the Common Issues documentation (where applicable)
required: true
- label: I have run database migrations (where applicable).
required: true
- label: I have attached screenshots and/or videos of the issue (where applicable)
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -1,38 +0,0 @@
name: Feature Request
description: Request a new feature.
title: "[Feature]: "
projects: ["grokability/snipe-it"]
type: feature
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please make sure to search the existing issues in this repository to see if your feature has already been requested, and feel free to add your context to any existing requests.
**Please write your issue in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you currently running? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.1 - build 19577 (master)
validations:
required: true
- type: textarea
id: feature-description
attributes:
label: How can we help?
description: Let us know in detail what feature you'd like to see added. While we can't promise to implement every feature request, we do read every one and take them into consideration when planning future releases.
placeholder: Tell us what you'd like to see in Snipe-IT! (Be nice!)
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

129
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,129 @@
name: Bug Report
description: Create a report to help us improve
body:
- type: checkboxes
attributes:
label: Debug mode
description: Please confirm you have done the following before posting your bug report
options:
- label: I have enabled debug mode
required: true
- label: I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Steps to reproduce the behavior.
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Screenshots
description: 'If applicable, add screenshots to help explain your problem.'
- type: markdown
attributes:
value: "### Server"
- 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: markdown
attributes:
value: "### Desktop"
- type: input
id: desktop_operatingSystem
attributes:
label: Operating System
description: 'e.g. Ubuntu, Windows'
- type: input
id: desktop_browser
attributes:
label: Browser
description: 'e.g. Google Chrome, Safari'
- type: input
id: desktop_version
attributes:
label: Version
description: 'e.g. 93'
- type: markdown
attributes:
value: "### Mobile"
- type: input
attributes:
label: Device
description: 'e.g. iPhone 6, Pixel 4a'
- type: input
id: mobile_operatingSystem
attributes:
label: Operating System
description: 'e.g. iOS 8.1, Android 9'
- type: input
id: mobile_browser
attributes:
label: Browser
description: 'e.g. Google Chrome, Safari'
- type: input
id: mobile_version
attributes:
label: Version
description: 'e.g. 93'
- type: textarea
attributes:
label: Error messages
description: |
WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
If a stacktrace is provided in the error, include that too.
Any errors that appear in your browser's error console.
Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
Include any additional information you can find in `storage/logs` and your webserver's logs.
Include the output from `php -m` (this should display what modules you have enabled.)
render: shell
- type: textarea
attributes:
label: Additional context
description: |
Is this a fresh install or an upgrade?
What OS and web server you're running Snipe-IT on
What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
Include what you've done so far in the installation, and if you got any error messages along the way.
Indicate whether or not you've manually edited any data directly in the database
Add any other context about the problem here.
- type: markdown
attributes:
value: Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,27 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature Request]: "
labels: ["feature request"]
assignees:
- snipe
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.

View File

@@ -1,22 +1,4 @@
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*"] frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*"]
skins: ["*.js", "*.css", "*.scss", "*.less"] backend: ["/app", "*.php"]
css: ["*.css","*.scss", "*.less"] legal: ["LICENSE*", "NOTICES*"]
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"]
config: .github config: .github

View File

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

40
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,40 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context, providing screenshots where practical. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
**Test Configuration**:
* PHP version:
* MySQL version
* Webserver version
* OS version
# Checklist:
- [ ] I have read the Contributing documentation available here: https://snipe-it.readme.io/docs/contributing-overview
- [ ] I have formatted this PR according to the project guidelines: https://snipe-it.readme.io/docs/contributing-overview#pull-request-guidelines
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes

43
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- :woman_technologist: ready for dev
- :moneybag: bounty
- :hand: bug
- "🔐 security"
- "👩‍💻 ready for dev"
- "💰 bounty"
- "✋ bug"
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
only: issues
# Comment to post when removing the stale label.
unmarkComment: >
Okay, it looks like this issue or feature request might still be important. We'll re-open
it for now. Thank you for letting us know!
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.

1
.github/travis-memory.ini vendored Normal file
View File

@@ -0,0 +1 @@
memory_limit= 2048M

7
.github/weekly-digest.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
# Configuration for weekly-digest - https://github.com/apps/weekly-digest
publishDay: sun
canPublishIssues: true
canPublishPullRequests: true
canPublishContributors: true
canPublishStargazers: true
canPublishCommits: true

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@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View File

@@ -10,33 +10,25 @@ name: Codacy Security Scan
on: on:
push: push:
branches: [ develop ] branches: [ master ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ develop ] branches: [ master ]
schedule: schedule:
- cron: '36 23 * * 3' - cron: '36 23 * * 3'
permissions:
contents: read
jobs: jobs:
codacy-security-scan: codacy-security-scan:
# Ensure schedule job never runs on forked repos. It's only executed for 'grokability/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 == 'grokability/snipe-it') || ((github.repository != 'grokability/snipe-it') && (github.event_name != 'schedule'))
name: Codacy Security Scan name: Codacy Security Scan
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Checkout the repository to the GitHub Actions runner # Checkout the repository to the GitHub Actions runner
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v2
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI - name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.7 uses: codacy/codacy-analysis-cli-action@1.1.0
with: with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations # You can also omit the token and run the tools that support default configurations
@@ -52,6 +44,6 @@ jobs:
# Upload the SARIF file generated in the previous step # Upload the SARIF file generated in the previous step
- name: Upload SARIF results file - name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@v1
with: with:
sarif_file: results.sarif 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@v5
- 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,13 +15,10 @@ on:
pull_request: pull_request:
permissions:
contents: read
jobs: jobs:
docker: docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it' # Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'grokability/snipe-it' if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
@@ -32,7 +29,6 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine 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=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=tag,suffix=-alpine type=ref,event=tag,suffix=-alpine
type=semver,pattern=v{{major}}-latest-alpine
# Define default tag "flavor" for docker/metadata-action per # Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input # https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default. # We turn off 'latest' tag by default.
@@ -42,17 +38,17 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v5 uses: actions/checkout@v2
# https://github.com/docker/setup-buildx-action # https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v1
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Login to DockerHub - name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push # Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -64,7 +60,7 @@ jobs:
# Get Metadata for docker_build step below # Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image - name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build id: meta_build
uses: docker/metadata-action@v5 uses: docker/metadata-action@v3
with: with:
images: snipe/snipe-it images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }} tags: ${{ env.IMAGE_TAGS }}
@@ -73,11 +69,11 @@ jobs:
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image - name: Build and push 'snipe-it' image
id: docker_build id: docker_build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./Dockerfile.alpine 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), # 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 # but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

View File

@@ -1,86 +0,0 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker images (Ubuntu)
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
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.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# 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
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# 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
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
# 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' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}

82
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker images
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
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
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v2
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
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@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# 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@v3
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
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' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}

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@v5
- 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,40 +0,0 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
# contents: write # only for delete-branch option
issues: write
# pull-requests: write
steps:
- uses: actions/stale@v10
with:
debug-only: true
ascending: true
operations-per-run: 1000 # just while we're debugging
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
exempt-all-milestones: true
stale-issue-message: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
close-issue-message: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.
# There doesn't seem to be a 'reopen issue message'?
# Since there is no 'stale-pr-message' - PR's should not be stale'd
stale-issue-label: stale
exempt-issue-labels: >
pinned,security,:woman_technologist: ready for dev,:moneybag: bounty,:hand: bug,🔐 security,👩‍💻 ready for dev,💰 bounty,✋ bug

View File

@@ -1,91 +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.2"
- "8.3"
- "8.4"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- 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 --no-interaction
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
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -1,90 +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"
- "8.3"
- "8.4"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- 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 --no-interaction
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
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -1,76 +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.3"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- 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: Create database file
run: touch database/database.sqlite
- 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
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

6
.gitignore vendored
View File

@@ -1,8 +1,6 @@
.couscous .couscous
.DS_Store .DS_Store
.env .env
.env.testing
phpstan.neon
.idea .idea
/bin/ /bin/
/bootstrap/compiled.php /bootstrap/compiled.php
@@ -47,7 +45,6 @@ storage/private_uploads/users/*
tests/_data/scenarios tests/_data/scenarios
tests/_output/* tests/_output/*
tests/_support/_generated/* tests/_support/_generated/*
tests/coverage/*
/npm-debug.log /npm-debug.log
/storage/oauth-private.key /storage/oauth-private.key
/storage/oauth-public.key /storage/oauth-public.key
@@ -68,6 +65,3 @@ _ide_helper_models.php
/.phplint-cache /.phplint-cache
storage/ldap_client_tls.cert storage/ldap_client_tls.cert
storage/ldap_client_tls.key storage/ldap_client_tls.key
/storage/framework/testing
/.phpunit.cache

2
.nvmrc
View File

@@ -1 +1 @@
v18.16.0 v10.15.1

View File

@@ -1,240 +0,0 @@
{
"standard": "WCAG2AA",
"level": "error",
"defaults": {
"useIncognitoBrowserContext": false,
"timeout": 500000,
"wait": 5000,
"ignore" : [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.Fail",
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
],
"viewport": {
"width": 1280,
"height": 1024
}
},
"urls": [
{
"__NOTE" : "this should always be FIRST (if browser context is preserved)",
"url": "https://snipe-it.test/login",
"actions": [
"navigate to https://snipe-it.test/login",
"screen capture tests/pa11y/login.png",
"set field input[name='username'] to admin",
"set field input[name='password'] to password",
"click element button[type=submit]",
"wait for url to be https://snipe-it.test/",
"screen capture tests/pa11y/dashboard.png"
]
},
{
"url" : "https://snipe-it.test/admin",
"actions" : [
"navigate to https://snipe-it.test/admin",
"screen capture tests/pa11y/admin-settings.png"
]
},
{
"url" : "https://snipe-it.test/admin/branding",
"actions" : [
"navigate to https://snipe-it.test/admin/branding",
"screen capture tests/pa11y/admin-branding.png"
]
},
{
"url" : "https://snipe-it.test/admin/general",
"actions" : [
"navigate to https://snipe-it.test/admin/general",
"screen capture tests/pa11y/admin-general.png"
]
},
{
"url" : "https://snipe-it.test/hardware/create",
"actions" : [
"navigate to https://snipe-it.test/hardware/create",
"screen capture tests/pa11y/asset-create.png"
]
},
{
"url" : "https://snipe-it.test/hardware",
"actions" : [
"navigate to https://snipe-it.test/hardware",
"screen capture tests/pa11y/asset-list.png"
]
},
{
"url" : "https://snipe-it.test/hardware/1",
"actions" : [
"navigate to https://snipe-it.test/hardware/1",
"screen capture tests/pa11y/asset-detail.png"
]
},
{
"url" : "https://snipe-it.test/account/view-assets",
"actions" : [
"navigate to https://snipe-it.test/account/view-assets",
"screen capture tests/pa11y/profile.png"
]
},
{
"url" : "https://snipe-it.test/licences",
"actions" : [
"navigate to https://snipe-it.test/licenses",
"screen capture tests/pa11y/license-list.png"
]
},
{
"url" : "https://snipe-it.test/licences/create",
"actions" : [
"navigate to https://snipe-it.test/licenses/create",
"screen capture tests/pa11y/license-create.png"
]
},
{
"url" : "https://snipe-it.test/licences/1",
"actions" : [
"navigate to https://snipe-it.test/licenses/1",
"screen capture tests/pa11y/license-view.png"
]
},
{
"url" : "https://snipe-it.test/consumables",
"actions" : [
"navigate to https://snipe-it.test/consumables",
"screen capture tests/pa11y/consumable-list.png"
]
},
{
"url" : "https://snipe-it.test/consumables/create",
"actions" : [
"navigate to https://snipe-it.test/consumables/create",
"screen capture tests/pa11y/consumable-create.png"
]
},
{
"url" : "https://snipe-it.test/consumables/1",
"actions" : [
"navigate to https://snipe-it.test/consumables/1",
"screen capture tests/pa11y/consumable-view.png"
]
},
{
"url" : "https://snipe-it.test/accessories",
"actions" : [
"navigate to https://snipe-it.test/accessories",
"screen capture tests/pa11y/accessory-list.png"
]
},
{
"url" : "https://snipe-it.test/accessories/create",
"actions" : [
"navigate to https://snipe-it.test/accessories/create",
"screen capture tests/pa11y/accessory-create.png"
]
},
{
"url" : "https://snipe-it.test/accessories/1",
"actions" : [
"navigate to https://snipe-it.test/accessories/1",
"screen capture tests/pa11y/accessory-view.png"
]
},
{
"url" : "https://snipe-it.test/locations",
"actions" : [
"navigate to https://snipe-it.test/locations",
"screen capture tests/pa11y/location-list.png"
]
},
{
"url" : "https://snipe-it.test/locations/create",
"actions" : [
"navigate to https://snipe-it.test/locations/create",
"screen capture tests/pa11y/location-create.png"
]
},
{
"url" : "https://snipe-it.test/locations/1",
"actions" : [
"navigate to https://snipe-it.test/locations/1",
"screen capture tests/pa11y/location-view.png"
]
},
{
"url" : "https://snipe-it.test/models",
"actions" : [
"navigate to https://snipe-it.test/models",
"screen capture tests/pa11y/model-list.png"
]
},
{
"url" : "https://snipe-it.test/models/create",
"actions" : [
"navigate to https://snipe-it.test/models/create",
"screen capture tests/pa11y/model-create.png"
]
},
{
"url" : "https://snipe-it.test/models/1",
"actions" : [
"navigate to https://snipe-it.test/models/1",
"screen capture tests/pa11y/model-view.png"
]
},
{
"url" : "https://snipe-it.test/companies",
"actions" : [
"navigate to https://snipe-it.test/companies",
"screen capture tests/pa11y/company-list.png"
]
},
{
"url" : "https://snipe-it.test/companies/create",
"actions" : [
"navigate to https://snipe-it.test/companies/create",
"screen capture tests/pa11y/company-create.png"
]
},
{
"url" : "https://snipe-it.test/companies/1",
"actions" : [
"navigate to https://snipe-it.test/companies/1",
"screen capture tests/pa11y/company-view.png"
]
},
{
"url" : "https://snipe-it.test/departments",
"actions" : [
"navigate to https://snipe-it.test/departments",
"screen capture tests/pa11y/department-list.png"
]
},
{
"url" : "https://snipe-it.test/departments/create",
"actions" : [
"navigate to https://snipe-it.test/departments/create",
"screen capture tests/pa11y/department-create.png"
]
},
{
"url" : "https://snipe-it.test/departments/1",
"actions" : [
"navigate to https://snipe-it.test/departments/1",
"screen capture tests/pa11y/department-view.png"
]
},
{
"url" : "https://snipe-it.test/invalid-url",
"actions" : [
"navigate to https://snipe-it.test/invalid-url",
"screen capture tests/pa11y/404.png"
]
}
]
}

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.2.0",
"php_max_major_minor": "8.4",
"php_max_wontwork": "8.5.0",
"current_snipeit_version": "8.0"
}

View File

@@ -1,75 +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 -->
| [<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") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/qveensi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
| [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
| [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
| [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
| [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=swift2512 "Code") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
| [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3302372?v=4" width="110px;"/><br /><sub>Jared Busch</sub>](https://github.com/sorvani)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [<img src="https://avatars.githubusercontent.com/u/111956991?v=4" width="110px;"/><br /><sub>seanborg-codethink</sub>](https://github.com/seanborg-codethink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [<img src="https://avatars.githubusercontent.com/u/160669961?v=4" width="110px;"/><br /><sub>dkaatz</sub>](https://github.com/dkaatz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [<img src="https://avatars.githubusercontent.com/u/827205?v=4" width="110px;"/><br /><sub>Daniel Ruf</sub>](https://threema.id/74SF7MW6?text=)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [<img src="https://avatars.githubusercontent.com/u/38883201?v=4" width="110px;"/><br /><sub>ahpaleus</sub>](https://github.com/ahpaleus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [<img src="https://avatars.githubusercontent.com/u/22906055?v=4" width="110px;"/><br /><sub>Anh DAO-DUY</sub>](https://github.com/mink-adao-duy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [<img src="https://avatars.githubusercontent.com/u/4723453?v=4" width="110px;"/><br /><sub>Andres Gutierrez</sub>](https://github.com/Serdnad)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111083379?v=4" width="110px;"/><br /><sub>Warren White</sub>](https://github.com/wewhite)<br />[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [<img src="https://avatars.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://robintemme.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [<img src="https://avatars.githubusercontent.com/u/47008367?v=4" width="110px;"/><br /><sub>herroworrd</sub>](https://github.com/herroworrd)<br />[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [<img src="https://avatars.githubusercontent.com/u/28558609?v=4" width="110px;"/><br /><sub>vicleos</sub>](https://mubiu.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [<img src="https://avatars.githubusercontent.com/u/1016780?v=4" width="110px;"/><br /><sub>Bob Clough</sub>](http://thinkl33t.co.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [<img src="https://avatars.githubusercontent.com/u/10648463?v=4" width="110px;"/><br /><sub>Brandon Daniel Bailey</sub>](https://github.com/brandon-bailey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [<img src="https://avatars.githubusercontent.com/u/23556080?v=4" width="110px;"/><br /><sub>Marc Bartelt</sub>](https://github.com/marcquark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
| [<img src="https://avatars.githubusercontent.com/u/18286893?v=4" width="110px;"/><br /><sub>manu-crealytics</sub>](https://github.com/manu-crealytics)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [<img src="https://avatars.githubusercontent.com/u/685167?v=4" width="110px;"/><br /><sub>Deloz</sub>](https://deloz.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [<img src="https://avatars.githubusercontent.com/u/2682426?v=4" width="110px;"/><br /><sub>Martin Berg</sub>](https://github.com/mbrrg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [<img src="https://avatars.githubusercontent.com/u/3694534?v=4" width="110px;"/><br /><sub>Richard Schwab</sub>](https://github.com/Nothing4You)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [<img src="https://avatars.githubusercontent.com/u/8959676?v=4" width="110px;"/><br /><sub>Rick Heil</sub>](https://rickheil.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [<img src="https://avatars.githubusercontent.com/u/397106?v=4" width="110px;"/><br /><sub>Ross Crawford-d'Heureuse</sub>](https://github.com/rosscdh)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1621107?v=4" width="110px;"/><br /><sub>Ryan McGuire</sub>](https://github.com/McG800)<br />[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [<img src="https://avatars.githubusercontent.com/u/77835667?v=4" width="110px;"/><br /><sub>SBrown2021</sub>](https://github.com/SBrown2021)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [<img src="https://avatars.githubusercontent.com/u/8780913?v=4" width="110px;"/><br /><sub>Serkan</sub>](https://github.com/serkanerip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [<img src="https://avatars.githubusercontent.com/u/63188620?v=4" width="110px;"/><br /><sub>Shanks</sub>](https://www.yudelei.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [<img src="https://avatars.githubusercontent.com/u/198525698?v=4" width="110px;"/><br /><sub>cendai-mis</sub>](https://github.com/cendai-mis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [<img src="https://avatars.githubusercontent.com/u/8724583?v=4" width="110px;"/><br /><sub>Shaun McPeck</sub>](https://smcpeck.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [<img src="https://avatars.githubusercontent.com/u/1378836?v=4" width="110px;"/><br /><sub>Stephen</sub>](https://github.com/snazy2000)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/4462739?v=4" width="110px;"/><br /><sub>Steven</sub>](http://nevets82.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [<img src="https://avatars.githubusercontent.com/u/29017267?v=4" width="110px;"/><br /><sub>Mateus Villar</sub>](https://mateusvillar.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [<img src="https://avatars.githubusercontent.com/u/12749393?v=4" width="110px;"/><br /><sub>Matthew Zackschewski</sub>](https://github.com/mzack5020)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [<img src="https://avatars.githubusercontent.com/u/12660103?v=4" width="110px;"/><br /><sub>Matthias Frei</sub>](https://www.frei.media/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [<img src="https://avatars.githubusercontent.com/u/824840?v=4" width="110px;"/><br /><sub>Nenad Ticaric</sub>](https://github.com/nticaric)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [<img src="https://avatars.githubusercontent.com/u/706439?v=4" width="110px;"/><br /><sub>Nikolay Didenko</sub>](https://github.com/Scorcher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [<img src="https://avatars.githubusercontent.com/u/5457236?v=4" width="110px;"/><br /><sub>Nuno Maduro</sub>](https://nunomaduro.com/sponsorships)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
| [<img src="https://avatars.githubusercontent.com/u/8883074?v=4" width="110px;"/><br /><sub>Oliver Walerys</sub>](https://tektikhq.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [<img src="https://avatars.githubusercontent.com/u/3102039?v=4" width="110px;"/><br /><sub>R. Christian McDonald</sub>](https://keybase.io/rcmcdonald91)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [<img src="https://avatars.githubusercontent.com/u/1525581?v=4" width="110px;"/><br /><sub>nix</sub>](https://nnix.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [<img src="https://avatars.githubusercontent.com/u/55462380?v=4" width="110px;"/><br /><sub>octobunny</sub>](https://github.com/octobunny)<br />[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [<img src="https://avatars.githubusercontent.com/u/8558670?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/sreyemnayr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>p3nj</sub>](https://benji.ltd/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [<img src="https://avatars.githubusercontent.com/u/6201617?v=4" width="110px;"/><br /><sub>Tim White</sub>](https://github.com/timwsuqld)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
| [<img src="https://avatars.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>yannikp</sub>](https://github.com/yannikp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [<img src="https://avatars.githubusercontent.com/u/20525448?v=4" width="110px;"/><br /><sub>victoria</sub>](https://github.com/viclou)<br />[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [<img src="https://avatars.githubusercontent.com/u/40685314?v=4" width="110px;"/><br /><sub>Valentyn Tulub</sub>](https://github.com/valentyntu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [<img src="https://avatars.githubusercontent.com/u/864520?v=4" width="110px;"/><br /><sub>Wouter van Os</sub>](http://wouter0100.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [<img src="https://avatars.githubusercontent.com/u/3946540?v=4" width="110px;"/><br /><sub>Wyatt Teeter</sub>](https://www.linkedin.com/in/wyatt-teeter)<br />[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [<img src="https://avatars.githubusercontent.com/u/1596124?v=4" width="110px;"/><br /><sub>Yorick Terweijden</sub>](https://github.com/terwey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [<img src="https://avatars.githubusercontent.com/u/69298836?v=4" width="110px;"/><br /><sub>bmkalle</sub>](https://github.com/bmkalle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28403467?v=4" width="110px;"/><br /><sub>bricelabelle</sub>](https://github.com/bricelabelle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [<img src="https://avatars.githubusercontent.com/u/97770090?v=4" width="110px;"/><br /><sub>corydlamb</sub>](https://github.com/corydlamb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [<img src="https://avatars.githubusercontent.com/u/1154133?v=4" width="110px;"/><br /><sub>Diogenes S. Jesus</sub>](http://twitter.com/splash)<br />[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [<img src="https://avatars.githubusercontent.com/u/5826629?v=4" width="110px;"/><br /><sub>D M</sub>](https://github.com/dkmansion)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [<img src="https://avatars.githubusercontent.com/u/14837699?v=4" width="110px;"/><br /><sub>Dustin B</sub>](https://github.com/Jarli01)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [<img src="https://avatars.githubusercontent.com/u/348344?v=4" width="110px;"/><br /><sub>Fabian Grutschus</sub>](https://github.com/fabiang)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [<img src="https://avatars.githubusercontent.com/u/1491053?v=4" width="110px;"/><br /><sub>MelonSmasher</sub>](https://github.com/MelonSmasher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
| [<img src="https://avatars.githubusercontent.com/u/80526133?v=4" width="110px;"/><br /><sub>AlexanderWPapyrus</sub>](https://github.com/AlexanderWPapyrus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [<img src="https://avatars.githubusercontent.com/u/306231?v=4" width="110px;"/><br /><sub>Alexandr Hacicheant</sub>](https://github.com/disc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [<img src="https://avatars.githubusercontent.com/u/3032891?v=4" width="110px;"/><br /><sub>Hex</sub>](https://hex128.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [<img src="https://avatars.githubusercontent.com/u/8697942?v=4" width="110px;"/><br /><sub>Arunas Skirius</sub>](https://github.com/arukompas)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [<img src="https://avatars.githubusercontent.com/u/104396?v=4" width="110px;"/><br /><sub>Ben Periton</sub>](https://github.com/benperiton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [<img src="https://avatars.githubusercontent.com/u/11906832?v=4" width="110px;"/><br /><sub>Byron Wolfman</sub>](https://wolfman.dev/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [<img src="https://avatars.githubusercontent.com/u/56485508?v=4" width="110px;"/><br /><sub>Calvin</sub>](https://github.com/CalvinSchwartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "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,8 +1,8 @@
FROM ubuntu:24.04 FROM ubuntu:focal
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>" LABEL maintainer Brady Wetherington <bwetherington@grokability.com>
# No need to add `apt-get clean` here, reference: # No need to add `apt-get clean` here, reference:
# - https://github.com/grokability/snipe-it/pull/9201 # - https://github.com/snipe/snipe-it/pull/9201
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get # - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
RUN export DEBIAN_FRONTEND=noninteractive; \ RUN export DEBIAN_FRONTEND=noninteractive; \
@@ -14,17 +14,15 @@ RUN export DEBIAN_FRONTEND=noninteractive; \
apt-utils \ apt-utils \
apache2 \ apache2 \
apache2-bin \ apache2-bin \
libapache2-mod-php8.3 \ libapache2-mod-php7.4 \
php8.3-curl \ php7.4-curl \
php8.3-ldap \ php7.4-ldap \
php8.3-mysql \ php7.4-mysql \
php8.3-gd \ php7.4-gd \
php8.3-xml \ php7.4-xml \
php8.3-mbstring \ php7.4-mbstring \
php8.3-zip \ php7.4-zip \
php8.3-bcmath \ php7.4-bcmath \
php8.3-redis \
php-memcached \
patch \ patch \
curl \ curl \
wget \ wget \
@@ -38,25 +36,29 @@ gcc \
make \ make \
autoconf \ autoconf \
libc-dev \ libc-dev \
libldap-common \
pkg-config \ pkg-config \
php8.3-dev \ libmcrypt-dev \
php7.4-dev \
ca-certificates \ ca-certificates \
unzip \ unzip \
dnsutils \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar
RUN php go-pear.phar RUN php go-pear.phar
RUN pecl install mcrypt-1.0.3
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 gd
RUN phpenmod bcmath RUN phpenmod bcmath
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.3/apache2/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/8.3/cli/php.ini RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/7.4/cli/php.ini
RUN useradd -m --uid 10000 --gid 50 docker RUN useradd -m --uid 1000 --gid 50 docker
RUN echo export APACHE_RUN_USER=docker >> /etc/apache2/envvars RUN echo export APACHE_RUN_USER=docker >> /etc/apache2/envvars
RUN echo export APACHE_RUN_GROUP=staff >> /etc/apache2/envvars RUN echo export APACHE_RUN_GROUP=staff >> /etc/apache2/envvars
@@ -99,7 +101,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.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" \ && 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 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 \ && chmod +x /var/www/html/artisan \
&& echo "Finished setting up application in /var/www/html" && echo "Finished setting up application in /var/www/html"
@@ -110,7 +112,7 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Get dependencies # Get dependencies
USER docker USER docker
RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html && rm -rf /var/www/html/vendor/*/*/.git RUN composer install --no-dev --working-dir=/var/www/html
USER root USER root
############### APPLICATION INSTALL/INIT ################# ############### APPLICATION INSTALL/INIT #################

View File

@@ -1,35 +1,30 @@
FROM alpine:3.19 FROM alpine:3
# Apache + PHP # Apache + PHP
RUN apk add --no-cache \ RUN apk add --no-cache \
apache2 \ apache2 \
php82 \ php7 \
php82-common \ php7-common \
php82-apache2 \ php7-apache2 \
php82-curl \ php7-curl \
php82-ldap \ php7-ldap \
php82-mysqli \ php7-mysqli \
php82-gd \ php7-gd \
php82-xml \ php7-xml \
php82-mbstring \ php7-mbstring \
php82-zip \ php7-zip \
php82-ctype \ php7-ctype \
php82-tokenizer \ php7-tokenizer \
php82-pdo_mysql \ php7-pdo_mysql \
php82-openssl \ php7-openssl \
php82-bcmath \ php7-bcmath \
php82-phar \ php7-phar \
php82-json \ php7-json \
php82-iconv \ php7-iconv \
php82-fileinfo \ php7-fileinfo \
php82-simplexml \ php7-simplexml \
php82-session \ php7-session \
php82-dom \ php7-dom \
php82-xmlwriter \ php7-xmlwriter \
php82-xmlreader \
php82-sodium \
php82-redis \
php82-pecl-memcached \
php82-exif \
curl \ curl \
wget \ wget \
vim \ vim \
@@ -42,7 +37,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives # Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2 RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php82/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 COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite # Enable mod_rewrite
@@ -73,18 +68,18 @@ RUN mkdir -p /var/www/.composer && chown apache /var/www/.composer
# Install dependencies # Install dependencies
USER apache USER apache
RUN COMPOSER_CACHE_DIR=/dev/null composer install --working-dir=/var/www/html RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html
USER root USER root
VOLUME ["/var/lib/snipeit"] VOLUME ["/var/lib/snipeit"]
# Startup script # Entrypoints
COPY docker/startup_alpine.sh /startup.sh COPY docker/entrypoint_alpine.sh /entrypoint.sh
RUN chmod +x /startup.sh RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/startup.sh"] CMD ["/entrypoint.sh"]
EXPOSE 80 EXPOSE 80

View File

@@ -1,8 +1,8 @@
ARG ENVIRONMENT=production ARG ENVIRONMENT=production
ARG SNIPEIT_RELEASE=6.1.0 ARG SNIPEIT_RELEASE=5.1.3
ARG PHP_VERSION=8.2 ARG PHP_VERSION=7.4.16
ARG PHP_ALPINE_VERSION=3.17 ARG PHP_ALPINE_VERSION=3.13
ARG COMPOSER_VERSION=2 ARG COMPOSER_VERSION=2.0.11
# Cannot use arguments with 'COPY --from' workaround # Cannot use arguments with 'COPY --from' workaround
# https://github.com/moby/moby/issues/34482#issuecomment-454716952 # https://github.com/moby/moby/issues/34482#issuecomment-454716952
@@ -52,7 +52,7 @@ RUN { \
# Install php extensions inside docker containers easily # Install php extensions inside docker containers easily
# https://github.com/mlocati/docker-php-extension-installer # 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; \ RUN set -eux; \
install-php-extensions \ install-php-extensions \
bcmath \ bcmath \
@@ -70,19 +70,19 @@ COPY --from=composer /usr/bin/composer /usr/local/bin
ARG COMPOSER_ALLOW_SUPERUSER=1 ARG COMPOSER_ALLOW_SUPERUSER=1
RUN set -eux; \ RUN set -eux; \
# Download and extract snipeit tarball # Download and extract snipeit tarball
curl -o snipeit.tar.gz -fL "https://github.com/grokability/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \ curl -o snipeit.tar.gz -fL "https://github.com/snipe/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \ tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
rm snipeit.tar.gz; \ rm snipeit.tar.gz; \
# Install composer php dependencies # Install composer php dependencies
if [ "$ENVIRONMENT" = "production" ]; then \ if [ "$ENVIRONMENT" = "production" ]; then \
echo "production environment detected!"; \ echo "production enviroment detected!"; \
composer update \ composer update \
--no-cache \ --no-cache \
--no-dev \ --no-dev \
--optimize-autoloader \ --optimize-autoloader \
--working-dir=/var/www/html; \ --working-dir=/var/www/html; \
else \ else \
echo "development environment detected!"; \ echo "development enviroment detected!"; \
apk add --no-cache \ apk add --no-cache \
${DEV_PACKAGES}; \ ${DEV_PACKAGES}; \
composer update \ composer update \
@@ -97,7 +97,7 @@ RUN set -eux; \
VOLUME [ "/var/lib/snipeit" ] VOLUME [ "/var/lib/snipeit" ]
COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/startup_alpine_fpm.sh /startup.sh 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 COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/startup.sh" ] ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/startup.sh", "php-fpm" ] CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]

161
README.md
View File

@@ -1,33 +1,15 @@
![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602) ![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-282-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)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System ## 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. 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 11](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/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).) 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.
-----
### Table of Contents
* [Installation](#installation)
* [User's Manual](#users-manual)
* [Bug Reports & Feature Requests](#bug-reports--feature-requests)
* [Security](#security)
* [Upgrading](#upgrading)
* [Translations!](#translations-)
* [Libraries, Modules & Related Projects](#libraries-modules--related-projects)
* [Join the Community!](#join-the-community)
* [Contributing](#contributing)
* [Announcement List](#announcement-list)
__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.
----- -----
@@ -37,6 +19,8 @@ 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. 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)
----- -----
### User's Manual ### User's Manual
For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.io/docs/overview). For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.io/docs/overview).
@@ -44,25 +28,23 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
----- -----
### Bug Reports & Feature Requests ### Bug Reports & Feature Requests
Feel free to check out the [GitHub Issues for this project](https://github.com/grokability/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. 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.**
----- -----
### Security
> [!IMPORTANT]
> **To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.**
-----
### Upgrading ### Upgrading
Please see the [upgrading documentation](https://snipe-it.readme.io/docs/upgrading) for instructions on upgrading Snipe-IT. Please see the [upgrading documentation](https://snipe-it.readme.io/docs/upgrading) for instructions on upgrading Snipe-IT.
------ ------
### Announcement List
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
------
### Translations! ### Translations!
Please see the [translations documentation](https://snipe-it.readme.io/docs/translations) for information about available languages and how to add translations to Snipe-IT. Please see the [translations documentation](https://snipe-it.readme.io/docs/translations) for information about available languages and how to add translations to Snipe-IT.
@@ -73,75 +55,84 @@ 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. Since the release of the JSON REST API, several third-party developers have been developing modules and libraries to work with Snipe-IT.
> [!NOTE] - [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
> 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. :)
#### Libraries & Modules
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey) - [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 - [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 - [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
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT - [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) - [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. - [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 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. - [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://www.edtechirl.com/p/snipe-it-and-azure-asset-management) originally 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](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks. 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. :)
#### Mobile Apps
We're currently working on our own mobile app, but in the meantime, check out these third-party apps that work with Snipe-IT:
- [SnipeMate](https://snipemate.app/) (iOS, Google Play, Huawei AppGallery) by Mars Technology
- [Snipe-Scan](https://apps.apple.com/do/app/snipe-scan/id6744179400?uo=2) (iOS) by Nicolas Maton
- [Snipe-IT Assets Management](https://play.google.com/store/apps/details?id=com.diegogarciadev.assetsmanager.snipeit&hl=en&pli=1) (Google Play) by DiegoGarciaDEV
- [AssetX](https://apps.apple.com/my/app/assetx-for-snipe-it/id6741996196?uo=2) (iOS) for Snipe-IT by Rishi Gupta
-----
### Join the Community!
- **[Join our Discord](https://discord.gg/yZFtShAcKk)!** Its full of great people. We even wrote about it [here](https://grokstar.dev/culture/2024/06/the-unlikely-rise-of-discord-as-a-support-channel/)!
- **Follow us on Bluesky** at [@snipeitapp.com](https://bsky.app/profile/snipeitapp.com)
- **Follow us on Mastodon** at [hachyderm.io/@grokability](https://hachyderm.io/@grokability)
- **Follow our blog** at [Grokstar.Dev](https://grokstar.dev)
- **Subscribe here** on Github for notifications about new releases. (We recommend selecting "Releases" only for most users - this repo can get noisy.)
----- -----
### Contributing ### Contributing
**Please refrain from submitting issues or pull requests generated by fully-automated tools. Maintainers reserve the right, at their sole discretion, to close such submissions and to block any account responsible for them.** Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
Contributions should follow from a human-to-human discussion in the form of an issue for the best chances of being merged into the core project. (Sometimes we might already be working on that feature, sometimes we've decided against )
Please see the complete 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.
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).
Be sure to check out all of the [amazing people](CONTRIBUTORS.md) that have contributed to Snipe-IT over the years!
----- -----
### Star History ### Security
[![Star History Chart](https://api.star-history.com/svg?repos=grokability/snipe-it&type=Date)](https://www.star-history.com/#grokability/snipe-it&Date) To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.
------ -----
### Announcement List
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important. ### 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/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/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/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/917232?v=4" width="110px;"/><br /><sub>Simona Avornicesei</sub>](http://www.avornicesei.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=savornicesei "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
We also usually make smaller announcements on our social accounts, our Discord, and our blog, so be sure to subscribe to those if you're looking for more granular announcements.

View File

@@ -10,13 +10,10 @@ however there are times when library dependencies and/or PHP/MySQL dependencies
make it impossible to backport security fixes on older versions. make it impossible to backport security fixes on older versions.
| Version | Supported | | Version | Supported |
|---------| ------------------ | | ------- | ------------------ |
| 8.x | :white_check_mark: | | 5.1.x | :white_check_mark: |
| 7.x | :white_check_mark: |
| 6.x | :x: |
| 5.1.x | :x: |
| 5.0.x | :x: | | 5.0.x | :x: |
| 4.0.x | :x: | | 4.0.x | :white_check_mark: |
| < 4.0 | :x: | | < 4.0 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

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-US
# --------------------------------------------
# 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.

2
Vagrantfile vendored
View File

@@ -1,7 +1,7 @@
# -*- mode: ruby -*- # -*- mode: ruby -*-
# vi: set ft=ruby : # vi: set ft=ruby :
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/grokability/snipe-it/master/snipeit.sh" SNIPEIT_SH_URL= "https://raw.githubusercontent.com/snipe/snipe-it/master/snipeit.sh"
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)" NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
Vagrant.configure("2") do |config| Vagrant.configure("2") do |config|

View File

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

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Actions\Categories;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Category;
use Illuminate\Support\Facades\Storage;
class DestroyCategoryAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasAssetModels
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Category $category): bool
{
$category->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
'models as models_count'
]);
if ($category->assets_count > 0) {
throw new ItemStillHasAssets($category);
}
if ($category->accessories_count > 0) {
throw new ItemStillHasAccessories($category);
}
if ($category->consumables_count > 0) {
throw new ItemStillHasConsumables($category);
}
if ($category->components_count > 0) {
throw new ItemStillHasComponents($category);
}
if ($category->licenses_count > 0) {
throw new ItemStillHasLicenses($category);
}
if ($category->models_count > 0) {
throw new ItemStillHasAssetModels($category);
}
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
return true;
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetCancelation;
use Illuminate\Auth\Access\AuthorizationException;
class CancelCheckoutRequestAction
{
public static function run(Asset $asset, User $user)
{
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
}
$asset->cancelRequest();
$asset->decrement('requests_counter', 1);
$data['item'] = $asset;
$data['target'] = $user;
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = $data['item_type'] = Asset::class;
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
$logaction->target_id = $data['user_id'] = auth()->id();
$logaction->target_type = User::class;
$logaction->location_id = $user->location_id ?? null;
$logaction->logaction('request canceled');
try {
$settings->notify(new RequestAssetCancelation($data));
} catch (\Exception $e) {
\Log::warning($e);
}
return true;
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Exceptions\AssetNotRequestable;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetNotification;
use Illuminate\Auth\Access\AuthorizationException;
use Log;
class CreateCheckoutRequestAction
{
/**
* @throws AssetNotRequestable
* @throws AuthorizationException
*/
public static function run(Asset $asset, User $user): string
{
if (is_null(Asset::RequestableAssets()->find($asset->id))) {
throw new AssetNotRequestable($asset);
}
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
}
$data['item'] = $asset;
$data['target'] = $user;
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = $data['item_type'] = Asset::class;
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
$logaction->target_id = $data['user_id'] = auth()->id();
$logaction->target_type = User::class;
$logaction->location_id = $user->location_id ?? null;
$logaction->logaction('requested');
$asset->request();
$asset->increment('requests_counter', 1);
try {
$settings->notify(new RequestAssetNotification($data));
} catch (\Exception $e) {
Log::warning($e);
}
return true;
}
}

View File

@@ -1,63 +0,0 @@
<?php
namespace App\Actions\Manufacturers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DeleteManufacturerAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Manufacturer $manufacturer): bool
{
$manufacturer->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
]);
if ($manufacturer->assets_count > 0) {
throw new ItemStillHasAssets($manufacturer);
}
if ($manufacturer->accessories_count > 0) {
throw new ItemStillHasAccessories($manufacturer);
}
if ($manufacturer->consumables_count > 0) {
throw new ItemStillHasConsumables($manufacturer);
}
if ($manufacturer->components_count > 0) {
throw new ItemStillHasComponents($manufacturer);
}
if ($manufacturer->licenses_count > 0) {
throw new ItemStillHasLicenses($manufacturer);
}
if ($manufacturer->image) {
try {
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
Log::info($e);
}
}
$manufacturer->delete();
//dd($manufacturer);
return true;
}
}

View File

@@ -1,72 +0,0 @@
<?php
namespace App\Actions\Suppliers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Models\Supplier;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasLicenses;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DestroySupplierAction
{
/**
*
* @throws ItemStillHasLicenses
* @throws ItemStillHasAssets
* @throws ItemStillHasMaintenances
* @throws ItemStillHasAccessories
* @throws ItemStillHasConsumables
* @throws ItemStillHasComponents
*/
static function run(Supplier $supplier): bool
{
$supplier->loadCount([
'maintenances as maintenances_count',
'assets as assets_count',
'licenses as licenses_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
]);
if ($supplier->assets_count > 0) {
throw new ItemStillHasAssets($supplier);
}
if ($supplier->maintenances_count > 0) {
throw new ItemStillHasMaintenances($supplier);
}
if ($supplier->licenses_count > 0) {
throw new ItemStillHasLicenses($supplier);
}
if ($supplier->accessories_count > 0) {
throw new ItemStillHasAccessories($supplier);
}
if ($supplier->consumables_count > 0) {
throw new ItemStillHasConsumables($supplier);
}
if ($supplier->components_count > 0) {
throw new ItemStillHasComponents($supplier);
}
if ($supplier->image) {
try {
Storage::disk('public')->delete('suppliers/'.$supplier->image);
} catch (\Exception $e) {
Log::info($e->getMessage());
}
}
$supplier->delete();
return true;
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\User;
use App\Models\License;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class CheckinLicensesFromAllUsers extends Command class CheckinLicensesFromAllUsers extends Command
@@ -41,48 +41,55 @@ class CheckinLicensesFromAllUsers extends Command
*/ */
public function handle() public function handle()
{ {
$license_id = $this->option('license_id'); $license_id = $this->option('license_id');
$notify = $this->option('notify'); $notify = $this->option('notify');
if (! $license_id) { if (!$license_id) {
$this->error('ERROR: License ID is required.'); $this->error('ERROR: License ID is required.');
return false; return false;
} }
if (! $license = License::where('id', '=', $license_id)->first()) {
$this->error('Invalid license ID');
if (!$license = License::where('id','=',$license_id)->first()) {
$this->error('Invalid license ID');
return false; return false;
} }
$this->info('Checking in ALL seats for '.$license->name); $this->info('Checking in ALL seats for '.$license->name);
$licenseSeats = LicenseSeat::where('license_id', '=', $license_id) $licenseSeats = LicenseSeat::where('license_id', '=', $license_id)
->whereNotNull('assigned_to') ->whereNotNull('assigned_to')
->with('user') ->with('user')
->get(); ->get();
$this->info(' There are '.$licenseSeats->count().' seats checked out: '); $this->info(' There are ' .$licenseSeats->count(). ' seats checked out: ');
if (! $notify) { if (!$notify) {
$this->info('No mail will be sent.'); $this->info('No mail will be sent.');
} }
foreach ($licenseSeats as $seat) { foreach ($licenseSeats as $seat) {
$this->info($seat->user->username.' has a license seat for '.$license->name); $this->info($seat->user->username .' has a license seat for '.$license->name);
$seat->assigned_to = null; $seat->assigned_to = null;
if ($seat->save()) { if ($seat->save()) {
// Override the email address so we don't notify on checkin // Override the email address so we don't notify on checkin
if (! $notify) { if (!$notify) {
$seat->user->email = null; $seat->user->email = null;
} }
// Log the checkin // Log the checkin
$seat->logCheckin($seat->user, 'Checked in via cli tool'); $seat->logCheckin($seat->user, 'Checked in via cli tool');
} }
} }
} }
} }

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\User;
use App\Models\License;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class CheckoutLicenseToAllUsers extends Command class CheckoutLicenseToAllUsers extends Command
@@ -22,7 +22,7 @@ class CheckoutLicenseToAllUsers extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Checks out licenses to all users'; protected $description = 'Command description';
/** /**
* Create a new command instance. * Create a new command instance.
@@ -41,22 +41,22 @@ class CheckoutLicenseToAllUsers extends Command
*/ */
public function handle() public function handle()
{ {
$license_id = $this->option('license_id'); $license_id = $this->option('license_id');
$notify = $this->option('notify'); $notify = $this->option('notify');
if (! $license_id) { if (!$license_id) {
$this->error('ERROR: License ID is required.'); $this->error('ERROR: License ID is required.');
return false;
return false;
} }
if (! $license = License::where('id', '=', $license_id)->with('assignedusers')->first()) {
if (!$license = License::where('id','=',$license_id)->with('assignedusers')->first()) {
$this->error('Invalid license ID'); $this->error('Invalid license ID');
return false; 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()) { if ($users->count() > $license->getAvailSeatsCountAttribute()) {
$this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. '); $this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
@@ -64,7 +64,7 @@ class CheckoutLicenseToAllUsers extends Command
$this->info('Checking out '.$users->count().' of '.$license->getAvailSeatsCountAttribute().' seats for '.$license->name); $this->info('Checking out '.$users->count().' of '.$license->getAvailSeatsCountAttribute().' seats for '.$license->name);
if (! $notify) { if (!$notify) {
$this->info('No mail will be sent.'); $this->info('No mail will be sent.');
} }
@@ -74,14 +74,14 @@ class CheckoutLicenseToAllUsers extends Command
// to them // to them
if ($user->licenses->where('id', '=', $license_id)->count()) { if ($user->licenses->where('id', '=', $license_id)->count()) {
$this->info($user->username.' already has this license checked out to them. Skipping... '); $this->info($user->username .' already has this license checked out to them. Skipping... ');
continue; continue;
} }
// If the license is valid, check that there is an available seat // If the license is valid, check that there is an available seat
if ($license->availCount()->count() < 1) { if ($license->availCount()->count() < 1) {
$this->error('ERROR: No available seats'); $this->error('ERROR: No available seats');
return false; return false;
} }
@@ -89,12 +89,13 @@ class CheckoutLicenseToAllUsers extends Command
// Get the seat ID // Get the seat ID
$licenseSeat = $license->freeSeat(); $licenseSeat = $license->freeSeat();
// Update the seat with checkout info, // Update the seat with checkout info,
$licenseSeat->assigned_to = $user->id; $licenseSeat->assigned_to = $user->id;
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
// Temporarily null the user's email address so we don't send mail if we're not supposed to // Temporarily null the user's email address so we don't send mail if we're not supposed to
if (! $notify) { if (!$notify) {
$user->email = null; $user->email = null;
} }
@@ -102,6 +103,10 @@ class CheckoutLicenseToAllUsers extends Command
$licenseSeat->logCheckout('Checked out via cli tool', $user); $licenseSeat->logCheckout('Checked out via cli tool', $user);
$this->info('License '.$license_id.' seat '.$licenseSeat->id.' checked out to '.$user->username); $this->info('License '.$license_id.' seat '.$licenseSeat->id.' checked out to '.$user->username);
} }
} }
} }
} }

View File

@@ -1,68 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
class CleanIncorrectCheckoutAcceptances extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-checkout-acceptances';
/**
* The console command description.
*
* @var string
*/
protected $description = "Delete checkout acceptances for checkouts to non-users";
/**
* Execute the console command.
*/
public function handle()
{
$deletions = 0;
$skips = 0;
// This walks *every* checkoutacceptance. That's gnarly. But necessary
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if(is_null($item)) {
$this->info("'Checkoutable' Item is null, going to next record");
return; //'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
}
if(get_class($item) == LicenseSeat::class) {
$item = $item->license;
}
foreach($item->assetlog()->where('action_type','checkout')->get() as $assetlog) {
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
//We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
//now, let's compare the _times_ - are they close?
//I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
//were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
//each other (currently 5)
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { //we're allowing for five _ish_ seconds of slop
$deletions++;
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
return;
} else {
//$this->info("The two records are too far apart");
}
} else {
//$this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
}
}
$skips++;
});
$this->error("Final deletion count: $deletions, and skip count: $skips");
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutRequest;
use Illuminate\Console\Command;
class CleanOldCheckoutRequests extends Command
{
private int $deletions = 0;
private int $skips = 0;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-old-checkout-requests';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes checkout requests that reference deleted assets or users.';
/**
* Execute the console command.
*/
public function handle()
{
$requests = CheckoutRequest::with([
'user' => function ($query) {
$query->withTrashed();
},
'requestedItem' => function ($query) {
$query->withTrashed();
},
])->get();
$this->info("Processing {$requests->count()} checkout requests");
$this->withProgressBar($requests, function ($request) {
if ($this->shouldForceDelete($request)) {
$request->forceDelete();
$this->deletions++;
return;
}
if ($this->shouldSoftDelete($request)) {
$request->delete();
$this->deletions++;
return;
}
$this->skips++;
});
$this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
return 0;
}
private function shouldForceDelete(CheckoutRequest $request)
{
// check if the requestable or user relationship is null
return !$request->requestable || !$request->user;
}
private function shouldSoftDelete(CheckoutRequest $request)
{
return $request->requestable->trashed() || $request->user->trashed();
}
}

View File

@@ -3,31 +3,15 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use \App\Models\User;
class CreateAdmin extends Command class CreateAdmin extends Command
{ {
/** @mixin User **/
/** /**
* App\Console\CreateAdmin * The name and signature of the console command.
* @property mixed $first_name *
* @property string $last_name * @var string
* @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
*/ */
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?}';
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
/** /**
* The console command description. * The console command description.
@@ -46,23 +30,25 @@ class CreateAdmin extends Command
parent::__construct(); parent::__construct();
} }
/**
* Execute the console command.
*
* @return mixed
*/
public function handle() public function handle()
{ {
$first_name = $this->option('first_name'); $first_name = $this->option('first_name');
$last_name = $this->option('last_name'); $last_name = $this->option('last_name');
$username = $this->option('username'); $username = $this->option('username');
$email = $this->option('email'); $email = $this->option('email');
$password = $this->option('password'); $password = $this->option('password');
$show_in_list = $this->argument('show_in_list'); $show_in_list = $this->argument('show_in_list');
$autoassign_licenses = $this->argument('autoassign_licenses');
if (($first_name=='') || ($last_name=='') || ($username=='') || ($email=='') || ($password=='')) {
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
$this->info('ERROR: All fields are required.'); $this->info('ERROR: All fields are required.');
} else { } else {
$user = new User; $user = new \App\Models\User;
$user->first_name = $first_name; $user->first_name = $first_name;
$user->last_name = $last_name; $user->last_name = $last_name;
$user->username = $username; $user->username = $username;
@@ -74,11 +60,6 @@ class CreateAdmin extends Command
if ($show_in_list == 'false') { if ($show_in_list == 'false') {
$user->show_in_list = 0; $user->show_in_list = 0;
} }
if ($autoassign_licenses == 'false') {
$user->autoassign_licenses = 0;
}
if ($user->save()) { if ($user->save()) {
$this->info('New user created'); $this->info('New user created');
$user->groups()->attach(1); $user->groups()->attach(1);
@@ -87,9 +68,12 @@ class CreateAdmin extends Command
$errors = $user->getErrors(); $errors = $user->getErrors();
foreach ($errors->all() as $error) { foreach ($errors->all() as $error) {
$this->info('ERROR:'.$error); $this->info('ERROR:'. $error);
} }
} }
} }
} }
} }

View File

@@ -38,7 +38,9 @@ class DisableLDAP extends Command
*/ */
public function handle() public function handle()
{ {
if ($this->confirm("\n****************************************************\nThis will disable LDAP support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) { if ($this->confirm("\n****************************************************\nThis will disable LDAP support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
$setting = Setting::getSettings(); $setting = Setting::getSettings();
$setting->ldap_enabled = 0; $setting->ldap_enabled = 0;
if ($setting->save()) { if ($setting->save()) {
@@ -49,5 +51,6 @@ class DisableLDAP extends Command
} else { } else {
$this->info('Canceled. No actions taken.'); $this->info('Canceled. No actions taken.');
} }
} }
} }

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
class DisableSAML extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:saml-disable';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This is a rescue command that can be used to turn off SAML settings in the event that you managed to lock yourself out using bad SAML settings.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm("\n****************************************************\nThis will disable SAML support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
$setting = Setting::getSettings();
$setting->saml_enabled = 0;
if ($setting->save()) {
$this->info('SAML has been set to disabled.');
} else {
$this->info('Unable to disable SAML.');
}
} else {
$this->info('Canceled. No actions taken.');
}
}
}

View File

@@ -1,151 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Accessory;
use App\Models\Actionlog;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class FixBulkAccessoryCheckinActionLogEntries extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:fix-bulk-accessory-action-log-entries {--dry-run : Run the sync process but don\'t update the database} {--skip-backup : Skip pre-execution backup}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This script attempts to fix timestamps and missing created_by values for bulk checkin entries in the log table';
private bool $dryrun = false;
private bool $skipBackup = false;
/**
* Execute the console command.
*/
public function handle()
{
$this->skipBackup = $this->option('skip-backup');
$this->dryrun = $this->option('dry-run');
if ($this->dryrun) {
$this->info('This is a DRY RUN - no changes will be saved.');
$this->newLine();
}
$logs = Actionlog::query()
// only look for accessory checkin logs
->where('item_type', Accessory::class)
// that were part of a bulk checkin
->where('note', 'Bulk checkin items')
// logs that were improperly timestamped should have created_at in the 1970s
->whereYear('created_at', '1970')
->get();
if ($logs->isEmpty()) {
$this->info('No logs found with incorrect timestamps.');
return 0;
}
$this->info('Found ' . $logs->count() . ' logs with incorrect timestamps:');
$this->table(
['ID', 'Created By', 'Created At', 'Updated At'],
$logs->map(function ($log) {
return [
$log->id,
$log->created_by,
$log->created_at,
$log->updated_at,
];
})
);
if (!$this->dryrun && !$this->confirm('Update these logs?')) {
return 0;
}
if (!$this->dryrun && !$this->skipBackup) {
$this->info('Backing up the database before making changes...');
$this->call('snipeit:backup');
}
if ($this->dryrun) {
$this->newLine();
$this->info('DRY RUN. NOT ACTUALLY UPDATING LOGS.');
}
foreach ($logs as $log) {
$this->newLine();
$this->info('Processing log id:' . $log->id);
// created_by was not being set for accessory bulk checkins
// so let's see if there was another bulk checkin log
// with the same timestamp and a created_by value we can use.
if (is_null($log->created_by)) {
$createdByFromSimilarLog = $this->getCreatedByAttributeFromSimilarLog($log);
if ($createdByFromSimilarLog) {
$this->line(vsprintf('Updating log id:%s created_by to %s', [$log->id, $createdByFromSimilarLog]));
$log->created_by = $createdByFromSimilarLog;
} else {
$this->warn(vsprintf('No created_by found for log id:%s', [$log->id]));
$this->warn('Skipping updating this log since no similar log was found to update created_by from.');
// If we can't find a similar log then let's skip updating it
continue;
}
}
$this->line(vsprintf('Updating log id:%s from %s to %s', [$log->id, $log->created_at, $log->updated_at]));
$log->created_at = $log->updated_at;
if (!$this->dryrun) {
Model::withoutTimestamps(function () use ($log) {
$log->saveQuietly();
});
}
}
$this->newLine();
if ($this->dryrun) {
$this->info('DRY RUN. NO CHANGES WERE ACTUALLY MADE.');
}
return 0;
}
/**
* Hopefully the bulk checkin included other items like assets or licenses
* so we can use one of those logs to get the correct created_by value.
*
* This method attempts to find a bulk check in log that was
* created at the same time as the log passed in.
*/
private function getCreatedByAttributeFromSimilarLog(Actionlog $log): null|int
{
$similarLog = Actionlog::query()
->whereNotNull('created_by')
->where([
'action_type' => 'checkin from',
'note' => 'Bulk checkin items',
'target_id' => $log->target_id,
'target_type' => $log->target_type,
'created_at' => $log->updated_at,
])
->first();
if ($similarLog) {
return $similarLog->created_by;
}
return null;
}
}

View File

@@ -37,43 +37,53 @@ class FixDoubleEscape extends Command
*/ */
public function handle() public function handle()
{ {
$tables = [ $tables = [
\App\Models\Asset::class => ['name'], '\App\Models\Asset' => ['name'],
\App\Models\License::class => ['name'], '\App\Models\License' => ['name', 'license_name'],
\App\Models\Consumable::class => ['name'], '\App\Models\Consumable' => ['name'],
\App\Models\Accessory::class => ['name'], '\App\Models\Accessory' => ['name'],
\App\Models\Component::class => ['name'], '\App\Models\Component' => ['name'],
\App\Models\Company::class => ['name'], '\App\Models\Company' => ['name'],
\App\Models\Manufacturer::class => ['name'], '\App\Models\Manufacturer' => ['name'],
\App\Models\Supplier::class => ['name'], '\App\Models\Supplier' => ['name'],
\App\Models\Statuslabel::class => ['name'], '\App\Models\Statuslabel' => ['name'],
\App\Models\Depreciation::class => ['name'], '\App\Models\Depreciation' => ['name'],
\App\Models\AssetModel::class => ['name'], '\App\Models\AssetModel' => ['name'],
\App\Models\Group::class => ['name'], '\App\Models\Group' => ['name'],
\App\Models\Department::class => ['name'], '\App\Models\Department' => ['name'],
\App\Models\Location::class => ['name'], '\App\Models\Location' => ['name'],
\App\Models\User::class => ['first_name', 'last_name'], '\App\Models\User' => ['first_name', 'last_name', 'jobtitle'],
]; ];
$count = []; $count = array();
foreach ($tables as $classname => $fields) {
$count[$classname] = [];
$count[$classname]['classname'] = 0;
foreach ($fields as $field) {
$count[$classname]['classname']++;
$count[$classname][$field] = 0;
foreach ($classname::where("$field", 'LIKE', '%&%')->get() as $row) { foreach ($tables as $classname => $fields) {
$this->info('Updating '.$field.' for '.$classname); $count[$classname] = array();
$row->{$field} = html_entity_decode($row->{$field}, ENT_QUOTES); $count[$classname]['classname'] = 0;
$row->save();
$count[$classname][$field]++; foreach($fields as $field) {
$count[$classname]['classname']++;
$count[$classname][$field] = 0;
foreach($classname::where("$field",'LIKE','%;%')->get() as $row) {
$fixed = html_entity_decode($row->{$field});
if ($row->save()) {
$this->info('Updating '.$field.' for '.$classname.' to '.$row->{$field}.' to '.$fixed);
} else {
$this->error('Could NOT update '.$field.' for '.$classname.' to '.$row->{$field}.' to '.$fixed.': '.$row->getErrors());
}
$count[$classname][$field]++;
}
} }
} }
}
$this->info('Update complete'); $this->info('Update complete');
} }
} }

View File

@@ -29,6 +29,7 @@ class FixMismatchedAssetsAndLogs extends Command
*/ */
private $dryrun = false; private $dryrun = false;
/** /**
* Create a new command instance. * Create a new command instance.
* *
@@ -46,29 +47,30 @@ class FixMismatchedAssetsAndLogs extends Command
*/ */
public function handle() public function handle()
{ {
if ($this->option('dryrun')) { if ($this->option('dryrun')) {
$this->dryrun = true; $this->dryrun = true;
} }
if ($this->dryrun) { if ($this->dryrun) {
$this->info('This is a DRY RUN - no changes will be saved.'); $this->info('This is a DRY RUN - no changes will be saved.' );
} }
$mismatch_count = 0; $mismatch_count = 0;
$assets = Asset::whereNotNull('assigned_to') $assets = Asset::whereNotNull('assigned_to')
->where('assigned_type', '=', \App\Models\User::class) ->where('assigned_type', '=', 'App\\Models\\User')
->orderBy('id', 'ASC')->get(); ->orderBy('id', 'ASC')->get();
foreach ($assets as $asset) { foreach ($assets as $asset) {
// get the last checkout of the asset // get the last checkout of the asset
if ($checkout_log = Actionlog::where('target_type', '=', \App\Models\User::class) if ($checkout_log = Actionlog::where('target_type', '=', 'App\\Models\\User')
->where('action_type', '=', 'checkout') ->where('action_type', '=', 'checkout')
->where('item_id', '=', $asset->id) ->where('item_id', '=', $asset->id)
->orderBy('created_at', 'DESC') ->orderBy('created_at', 'DESC')
->first()) { ->first()) {
// Now check for a subsequent checkin log - we want to ignore those // Now check for a subsequent checkin log - we want to ignore those
if (! $checkin_log = Actionlog::where('target_type', '=', \App\Models\User::class) if (!$checkin_log = Actionlog::where('target_type', '=', 'App\\Models\\User')
->where('action_type', '=', 'checkin from') ->where('action_type', '=', 'checkin from')
->where('item_id', '=', $asset->id) ->where('item_id', '=', $asset->id)
->whereDate('created_at', '>', $checkout_log->created_at) ->whereDate('created_at', '>', $checkout_log->created_at)
@@ -76,24 +78,28 @@ class FixMismatchedAssetsAndLogs extends Command
->first()) { ->first()) {
//print_r($asset); //print_r($asset);
if ($checkout_log->target_id != $asset->assigned_to) { if ($checkout_log->target_id != $asset->assigned_to) {
$this->error('Log ID: '.$checkout_log->id.' -- Asset ID '.$checkout_log->item_id.' SHOULD BE checked out to User '.$checkout_log->target_id.' but its assigned_to is '.$asset->assigned_to); $this->error('Log ID: '.$checkout_log->id.' -- Asset ID '. $checkout_log->item_id.' SHOULD BE checked out to User '.$checkout_log->target_id.' but its assigned_to is '.$asset->assigned_to );
if (! $this->dryrun) { if (!$this->dryrun) {
$asset->assigned_to = $checkout_log->target_id; $asset->assigned_to = $checkout_log->target_id;
if ($asset->save()) { if ($asset->save()) {
$this->info('Asset record updated.'); $this->info('Asset record updated.');
} else { } else {
$this->error('Error updating asset: '.$asset->getErrors()); $this->error('Error updating asset: '.$asset->getErrors());
}
} }
$mismatch_count++;
} }
$mismatch_count++; } else {
//$this->info('Asset ID '.$asset->id.': There is a checkin '.$checkin_log->created_at.' after this checkout '.$checkout_log->created_at);
} }
} else {
//$this->info('Asset ID '.$asset->id.': There is a checkin '.$checkin_log->created_at.' after this checkout '.$checkout_log->created_at);
}
} }
} }
$this->info($mismatch_count.' mismatched assets.'); $this->info($mismatch_count.' mismatched assets.');
} }
} }

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class FixUpAssignedTypeWithoutAssignedTo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-type-fixup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_type but no assigned_to';
/**
* Execute the console command.
*/
public function handle()
{
DB::table('assets')->whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
$this->info("Assets with an assigned_type but no assigned_to are fixed");
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use Illuminate\Console\Command;
class FixupAssignedToWithoutAssignedType extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-to-fixup
{--debug : Display debugging output}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_to but no assigned_type';
/**
* Execute the console command.
*/
public function handle()
{
$assets = Asset::whereNull("assigned_type")->whereNotNull("assigned_to")->withTrashed();
$this->withProgressBar($assets->get(), function (Asset $asset) {
//now check each action log, from the most recent backwards, to find the last checkin or checkout
foreach($asset->log()->orderBy("id","desc")->get() as $action_log) {
if($this->option("debug")) {
$this->info("Asset id: " . $asset->id . " action log, action type is: " . $action_log->action_type);
}
switch($action_log->action_type) {
case 'checkin from':
if($this->option("debug")) {
$this->info("Doing a checkin for ".$asset->id);
}
$asset->assigned_to = null;
// if you have a required custom field, we still want to save, and we *don't* want an action_log
$asset->saveQuietly();
return;
case 'checkout':
if($this->option("debug")) {
$this->info("Doing a checkout for " . $asset->id . " picking target type: " . $action_log->target_type);
}
if($asset->assigned_to != $action_log->target_id) {
$this->error("Asset's assigned_to does *NOT* match Action Log's target_id. \$asset->assigned_to=".$asset->assigned_to." vs. \$action_log->target_id=".$action_log->target_id);
//FIXME - do we abort here? Do we try to keep looking? I don't know, this means your data is *really* messed up...
}
$asset->assigned_type = $action_log->target_type;
$asset->saveQuietly(); // see above
return;
}
}
$asset->assigned_to = null; //asset was never checked in or out in its lifetime - it stays 'checked in'
$asset->saveQuietly(); //see above
});
$this->newLine();
$this->info("Assets assigned_type are fixed");
}
}

View File

@@ -1,94 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use Laravel\Passport\TokenRepository;
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)
{
$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

@@ -2,9 +2,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Location;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use League\Csv\Reader; use League\Csv\Reader;
use App\Models\Location;
class ImportLocations extends Command class ImportLocations extends Command
{ {
@@ -39,8 +39,10 @@ class ImportLocations extends Command
*/ */
public function handle() public function handle()
{ {
if (! ini_get('auto_detect_line_endings')) {
ini_set('auto_detect_line_endings', '1');
if (!ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1');
} }
$filename = $this->argument('filename'); $filename = $this->argument('filename');
@@ -51,17 +53,18 @@ class ImportLocations extends Command
// Import parent location names first if they don't exist // Import parent location names first if they don't exist
foreach ($results as $parent_index => $parent_row) { foreach ($results as $parent_index => $parent_row) {
if (array_key_exists('Parent Name', $parent_row)) { if (array_key_exists('Parent Name', $parent_row)) {
$parent_name = trim($parent_row['Parent Name']); $parent_name = trim($parent_row['Parent Name']);
if (array_key_exists('Name', $parent_row)) { if (array_key_exists('Name', $parent_row)) {
$this->info('- Parent: '.$parent_name.' in row as: '.trim($parent_row['Parent Name'])); $this->info('- Parent: ' . $parent_name . ' in row as: ' . trim($parent_row['Parent Name']));
} }
// Save parent location name // Save parent location name
// This creates a sort of name-stub that we'll update later on in this script // This creates a sort of name-stub that we'll update later on in this script
$parent_location = Location::firstOrCreate(['name' => $parent_name]); $parent_location = Location::firstOrCreate(array('name' => $parent_name));
if (array_key_exists('Name', $parent_row)) { if (array_key_exists('Name', $parent_row)) {
$this->info('Parent for '.$parent_row['Name'].' is '.$parent_name.'. Attempting to save '.$parent_name.'.'); $this->info('Parent for ' . $parent_row['Name'] . ' is ' . $parent_name . '. Attempting to save ' . $parent_name . '.');
} }
// Check if the record was updated or created. // Check if the record was updated or created.
@@ -71,15 +74,18 @@ class ImportLocations extends Command
} else { } else {
$this->info('- Parent location '.$parent_name.' was created.'); $this->info('- Parent location '.$parent_name.' was created.');
} }
} else { } else {
$this->info('- No Parent Name provided, so no parent location will be created.'); $this->info('- No Parent Name provided, so no parent location will be created.');
} }
} }
$this->info('----- Parents Created.... backfilling additional details... --------'); $this->info('----- Parents Created.... backfilling additional details... --------');
// Loop through ALL records and add/update them if there are additional fields // Loop through ALL records and add/update them if there are additional fields
// besides name // besides name
foreach ($results as $index => $row) { foreach ($results as $index => $row) {
if (array_key_exists('Parent Name', $row)) { if (array_key_exists('Parent Name', $row)) {
$parent_name = trim($row['Parent Name']); $parent_name = trim($row['Parent Name']);
} else { } else {
@@ -88,12 +94,11 @@ class ImportLocations extends Command
// Set the location attributes to save // Set the location attributes to save
if (array_key_exists('Name', $row)) { if (array_key_exists('Name', $row)) {
$location = Location::firstOrCreate(['name' => trim($row['Name'])]); $location = Location::firstOrCreate(array('name' => trim($row['Name'])));
$location->name = trim($row['Name']); $location->name = trim($row['Name']);
$this->info('Checking location: '.$location->name); $this->info('Checking location: '.$location->name);
} else { } else {
$this->error('Location name is required and is missing from at least one row in this dataset. Check your CSV for extra trailing rows and try again.'); $this->error('Location name is required and is missing from at least one row in this dataset. Check your CSV for extra trailing rows and try again.');
return false; return false;
} }
if (array_key_exists('Currency', $row)) { if (array_key_exists('Currency', $row)) {
@@ -121,6 +126,7 @@ class ImportLocations extends Command
$location->ldap_ou = trim($row['OU']); $location->ldap_ou = trim($row['OU']);
} }
// If a parent name is provided, we created it earlier in the script, // If a parent name is provided, we created it earlier in the script,
// so let's grab that ID // so let's grab that ID
if ($parent_name) { if ($parent_name) {
@@ -136,15 +142,21 @@ class ImportLocations extends Command
// Check if the record was updated or created. // Check if the record was updated or created.
// This is mostly for clearer debugging. // This is mostly for clearer debugging.
if ($location->exists) { if ($location->exists) {
$this->info('Location '.$location->name.' already exists. Updating...'); $this->info('Location ' . $location->name . ' already exists. Updating...');
} else { } else {
$this->info('- Location '.$location->name.' was created. '); $this->info('- Location '.$location->name.' was created. ');
} }
// If there's a validation error, display that // If there's a validation error, display that
} else { } else {
$this->error('- Non-parent Location '.$location->name.' could not be created: '.$location->getErrors()); $this->error('- Non-parent Location '.$location->name.' could not be created: '.$location->getErrors() );
} }
} }
} }
} }

516
app/Console/Commands/LdapSync.php Normal file → Executable file
View File

@@ -2,15 +2,13 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Department; use App\Models\Department;
use App\Models\Group;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Setting; use App\Models\Setting;
use App\Models\Ldap; use App\Models\Ldap;
use App\Models\User; use App\Models\User;
use App\Models\Location; use App\Models\Location;
use Illuminate\Support\Facades\Log; use Log;
class LdapSync extends Command class LdapSync extends Command
{ {
@@ -19,7 +17,7 @@ class LdapSync extends Command
* *
* @var string * @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. * The console command description.
@@ -45,180 +43,117 @@ class LdapSync extends Command
*/ */
public function handle() 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('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M')); 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;
// Map the LDAP attributes to the Snipe-IT user fields. $ldap_result_emp_num = Setting::getSettings()->ldap_emp_num;
$ldap_map = [ $ldap_result_email = Setting::getSettings()->ldap_email;
"username" => Setting::getSettings()->ldap_username_field, $ldap_result_phone = Setting::getSettings()->ldap_phone_field;
"last_name" => Setting::getSettings()->ldap_lname_field, $ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
"first_name" => Setting::getSettings()->ldap_fname_field, $ldap_result_country = Setting::getSettings()->ldap_country;
"active_flag" => Setting::getSettings()->ldap_active_flag, $ldap_result_dept = Setting::getSettings()->ldap_dept;
"emp_num" => Setting::getSettings()->ldap_emp_num,
"email" => Setting::getSettings()->ldap_email,
"phone" => Setting::getSettings()->ldap_phone_field,
"mobile" => Setting::getSettings()->ldap_mobile,
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
"address" => Setting::getSettings()->ldap_address,
"city" => Setting::getSettings()->ldap_city,
"state" => Setting::getSettings()->ldap_state,
"zip" => Setting::getSettings()->ldap_zip,
"country" => Setting::getSettings()->ldap_country,
"location" => Setting::getSettings()->ldap_location,
"dept" => Setting::getSettings()->ldap_dept,
"manager" => Setting::getSettings()->ldap_manager,
"display_name" => Setting::getSettings()->ldap_display_name,
];
$ldap_default_group = Setting::getSettings()->ldap_default_group;
$search_base = Setting::getSettings()->ldap_base_dn;
try { try {
$ldapconn = Ldap::connectToLdap(); $ldapconn = Ldap::connectToLdap();
Ldap::bindAdminToLdap($ldapconn); Ldap::bindAdminToLdap($ldapconn);
} catch (\Exception $e) { } catch (\Exception $e) {
if ($this->option('json_summary')) { if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []]; $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ];
$this->info(json_encode($json_summary)); $this->info(json_encode($json_summary));
} }
Log::info($e); LOG::info($e);
return []; return [];
} }
$summary = []; $summary = array();
try { 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') != '') { if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn'); $search_base = $this->option('base_dn');
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".'); LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".');
}
/**
* If a filter has been specified, use that, otherwise default to null
*/
if ($this->option('filter') != '') {
$filter = $this->option('filter');
} else { } else {
$filter = null; $search_base = null;
} }
$results = Ldap::findLdapUsers($search_base);
/**
* We only need to request the LDAP attributes that we process
*/
$attributes = array_values(array_filter($ldap_map));
if (Setting::getSettings()->is_ad === 1 && is_null($ldap_map['active_flag'])) {
$attributes[] = 'useraccountcontrol';
}
$results = Ldap::findLdapUsers($search_base, -1, $filter, $attributes);
} catch (\Exception $e) { } catch (\Exception $e) {
if ($this->option('json_summary')) { if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []]; $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ];
$this->info(json_encode($json_summary)); $this->info(json_encode($json_summary));
} }
Log::info($e); LOG::info($e);
return []; return [];
} }
/* Determine which location to assign users to by default. */ /* Determine which location to assign users to by default. */
$default_location = null; $location = NULL; // FIXME - this would be better called "$default_location", which is more explicit about its purpose
if ($this->option('location') != '') {
if ($default_location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to '.$default_location->name.' ('.$default_location->id.')');
}
} elseif ($this->option('location_id')) { if ($this->option('location')!='') {
//TODO - figure out how or why this is an array? $location = Location::where('name', '=', $this->option('location'))->first();
foreach($this->option('location_id') as $location_id) { LOG::debug('Location name '.$this->option('location').' passed');
if ($default_location = Location::where('id', '=', $location_id)->first()) { LOG::debug('Importing to '.$location->name.' ('.$location->id.')');
Log::debug('Location ID ' . $location_id . ' passed'); } elseif ($this->option('location_id')!='') {
Log::debug('Importing to '.$default_location->name.' ('.$default_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($default_location)) {
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.'); if (!isset($location)) {
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. */ /* 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) // 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_locations = Location::where('ldap_ou', '!=', '')->get()->toArray();
$ldap_ou_lengths = []; $ldap_ou_lengths = array();
foreach ($ldap_ou_locations as $ou_loc) { foreach ($ldap_ou_locations as $ou_loc) {
$ldap_ou_lengths[] = strlen($ou_loc['ldap_ou']); $ldap_ou_lengths[] = strlen($ou_loc["ldap_ou"]);
} }
array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations); array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations);
if (count($ldap_ou_locations) > 0) { if (sizeof($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 // Inject location information fields
for ($i = 0; $i < $results['count']; $i++) { for ($i = 0; $i < $results["count"]; $i++) {
$results[$i]['ldap_location_override'] = false; $results[$i]["ldap_location_override"] = false;
$results[$i]['location_id'] = null; $results[$i]["location_id"] = 0;
} }
// Grab subsets based on location-specific DNs, and overwrite location for these users. // Grab subsets based on location-specific DNs, and overwrite location for these users.
foreach ($ldap_ou_locations as $ldap_loc) { foreach ($ldap_ou_locations as $ldap_loc) {
try { try {
$location_users = Ldap::findLdapUsers($ldap_loc['ldap_ou']); $location_users = Ldap::findLdapUsers($ldap_loc["ldap_ou"]);
} catch (\Exception $e) { // TODO: this is stolen from line 77 or so above } catch (\Exception $e) { // FIXME: this is stolen from line 77 or so above
if ($this->option('json_summary')) { if ($this->option('json_summary')) {
$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' => []]; $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)); $this->info(json_encode($json_summary));
} }
Log::info($e); LOG::info($e);
return []; return [];
} }
$usernames = []; $usernames = array();
for ($i = 0; $i < $location_users['count']; $i++) { for ($i = 0; $i < $location_users["count"]; $i++) {
if (array_key_exists($ldap_map["username"], $location_users[$i])) {
$location_users[$i]['ldap_location_override'] = true; if (array_key_exists($ldap_result_username, $location_users[$i])) {
$location_users[$i]['location_id'] = $ldap_loc['id']; $location_users[$i]["ldap_location_override"] = true;
$usernames[] = $location_users[$i][$ldap_map["username"]][0]; $location_users[$i]["location_id"] = $ldap_loc["id"];
$usernames[] = $location_users[$i][$ldap_result_username][0];
} }
} }
// Delete located users from the general group. // Delete located users from the general group.
foreach ($results as $key => $generic_entry) { foreach ($results as $key => $generic_entry) {
if ((is_array($generic_entry)) && (array_key_exists($ldap_map["username"], $generic_entry))) { if ((is_array($generic_entry)) && (array_key_exists($ldap_result_username, $generic_entry))) {
if (in_array($generic_entry[$ldap_map["username"]][0], $usernames)) { if (in_array($generic_entry[$ldap_result_username][0], $usernames)) {
unset($results[$key]); unset($results[$key]);
} }
} }
@@ -230,279 +165,142 @@ class LdapSync extends Command
} }
} }
$manager_cache = []; /* Create user account entries in Snipe-IT */
$tmp_pass = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 20);
$pass = bcrypt($tmp_pass);
if($ldap_default_group != null) { for ($i = 0; $i < $results["count"]; $i++) {
$default = Group::find($ldap_default_group); $item = array();
if (!$default) { $item["username"] = isset($results[$i][$ldap_result_username][0]) ? $results[$i][$ldap_result_username][0] : "";
$ldap_default_group = null; // un-set the default group if that group doesn't exist $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] : "";
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields $department = Department::firstOrCreate([
for ($i = 0; $i < $results['count']; $i++) { 'name' => $item["department"],
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
$item['email'] = $results[$i][$ldap_map["email"]][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
$item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
$item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
$location = $default_location; //initially, set '$location' to the default_location (which may just be `null`)
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_map["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.
$item['createorupdate'] = 'updated';
} else {
// Creating a new user.
$user = new User;
$user->password = $user->noPassword();
$user->locale = app()->getLocale();
$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)
$item['createorupdate'] = 'created';
}
//If a sync option is not filled in on the LDAP settings don't populate the user field $user = User::where('username', $item["username"])->first();
if($ldap_map["username"] != null){
$user->username = $item['username']; if ($user) {
} // Updating an existing user.
if($ldap_map["display_name"] != null){ $item["createorupdate"] = 'updated';
$user->display_name = $item['display_name']; } else {
} // Creating a new user.
if($ldap_map["last_name"] != null){ $user = new User;
$user->last_name = $item['lastname']; $user->password = $pass;
} $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)
if($ldap_map["first_name"] != null){ $item["createorupdate"] = 'created';
$user->first_name = $item['firstname']; }
}
if($ldap_map["emp_num"] != null){ $user->first_name = $item["firstname"];
$user->employee_num = e($item['employee_number']); $user->last_name = $item["lastname"];
} $user->username = $item["username"];
if($ldap_map["email"] != null){ $user->email = $item["email"];
$user->email = $item['email']; $user->employee_num = e($item["employee_number"]);
} $user->phone = $item["telephone"];
if($ldap_map["phone"] != null){ $user->jobtitle = $item["jobtitle"];
$user->phone = $item['telephone']; $user->country = $item["country"];
}
if($ldap_map["mobile"] != null){
$user->mobile = $item['mobile'];
}
if($ldap_map["jobtitle"] != null){
$user->jobtitle = $item['jobtitle'];
}
if($ldap_map["address"] != null){
$user->address = $item['address'];
}
if($ldap_map["city"] != null){
$user->city = $item['city'];
}
if($ldap_map["state"] != null){
$user->state = $item['state'];
}
if($ldap_map["country"] != null){
$user->country = $item['country'];
}
if($ldap_map["zip"] != null){
$user->zip = $item['zip'];
}
if($ldap_map["dept"] != null){
$user->department_id = $department->id; $user->department_id = $department->id;
}
if($ldap_map["location"] != null){
$user->location_id = $location?->id;
}
if($ldap_map["manager"] != null){ if ( !empty($ldap_result_active_flag)) { // IF we have an 'active' flag set....
if($item['manager'] != null) { // ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// Check Cache first // (Specifically, we don't handle a value of '0.0' correctly)
if (isset($manager_cache[$item['manager']])) { $raw_value = @$results[$i][$ldap_result_active_flag][0];
// found in cache; use that and avoid extra lookups $filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$user->manager_id = $manager_cache[$item['manager']]; $boolean_cast = (bool)$raw_value;
} 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_map["username"] => [$item['manager']]
]
];
}
$add_manager_to_cache = true;
if ($ldap_manager["count"] > 0) {
try {
// 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_map["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;
}
} catch (\Exception $e) {
$add_manager_to_cache = false;
\Log::warning('Handling ldap manager ' . $item['manager'] . ' caused an exception: ' . $e->getMessage() . '. Continuing synchronization.');
}
}
if ($add_manager_to_cache) {
$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_map["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_map["active_flag"]][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool) $raw_value;
if (Setting::getSettings()->ldap_invert_active_flag === 1) {
// Because ldap_active_flag is set, if filter_var is true or boolean_cast is true, then user is suspended
$user->activated = !($filter_var ?? $boolean_cast);
}else{
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast $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])) { } elseif ( array_key_exists('useraccountcontrol', $results[$i]) ) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists, // ....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 // ....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
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.
/* The following is _probably_ the correct logic, but we can't use it because $useraccountcontrol = $results[$i]['useraccountcontrol'][0];
some users may have been dependent upon the previous behavior, and this if(
could cause additional access to be available to users they don't want // based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
to allow to log in. ($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$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
'4260352',// 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088',// 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
];
$user->activated = ( in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts) ) ? 1 : 0;
$useraccountcontrol = $results[$i]['useraccountcontrol'][0]; } /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
if( already-existing accounts will be however the administrator has set them */
// based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$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
'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 if ($item['ldap_location_override'] == true) {
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active. $user->location_id = $item['location_id'];
already-existing accounts will be however the administrator has set them */ } elseif ((isset($location)) && (!empty($location))) {
if ((is_array($location)) && (array_key_exists('id', $location))) {
if ($item['ldap_location_override'] == true) { $user->location_id = $location['id'];
$user->location_id = $item['location_id']; } elseif (is_object($location)) {
} elseif ((isset($location)) && (!empty($location))) { $user->location_id = $location->id;
if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id'];
} elseif (is_object($location)) {
$user->location_id = $location->id; //THIS is the magic line, this should do it.
}
}
// TODO - should we be NULLING locations if $location is really `null`, and that's what we came up with?
// will that conflict with any overriding setting that the user set? Like, if they moved someone from
// the 'null' location to somewhere, we wouldn't want to try to override that, right?
$location = null;
$user->ldap_import = 1;
$errors = '';
if ($user->save()) {
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ($item['createorupdate'] === 'created' && $ldap_default_group) {
// Check if the relationship already exists
if (!$user->groups()->where('group_id', $ldap_default_group)->exists()) {
$user->groups()->attach($ldap_default_group);
} }
} }
//updates assets location based on user's location $user->ldap_import = 1;
if ($user->wasChanged('location_id')) {
foreach ($user->assets as $asset) { $errors = '';
$asset->location_id = $user->location_id;
// TODO: somehow add note? "Asset Location Changed because of thing" if ($user->save()) {
$asset->save(); $item["note"] = $item["createorupdate"];
$item["status"]='success';
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
} }
$item["note"] = $errors;
$item["status"]='error';
} }
} else { array_push($summary, $item);
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
}
$item['note'] = $errors;
$item['status'] = 'error';
}
array_push($summary, $item);
} }
if ($this->option('summary')) { if ($this->option('summary')) {
for ($x = 0; $x < count($summary); $x++) { for ($x = 0; $x < count($summary); $x++) {
if ($summary[$x]['status'] == 'error') { if ($summary[$x]['status']=='error') {
$this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']); $this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']);
} else { } else {
$this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.'); $this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.');
} }
} }
} elseif ($this->option('json_summary')) { } else if ($this->option('json_summary')) {
$json_summary = ['error' => false, 'error_message' => '', 'summary' => $summary]; // hardcoding the error to false and the error_message to blank seems a bit weird $json_summary = [ "error" => false, "error_message" => "", "summary" => $summary ]; // hardcoding the error to false and the error_message to blank seems a bit weird
$this->info(json_encode($json_summary)); $this->info(json_encode($json_summary));
} else { } else {
return $summary; return $summary;

View File

@@ -0,0 +1,399 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Log;
use Exception;
use App\Models\User;
use App\Services\LdapAd;
use App\Models\Location;
use Illuminate\Console\Command;
use Adldap\Models\User as AdldapUser;
/**
* LDAP / AD sync command.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
class LdapSyncNg extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:ldap-sync-ng
{--location= : A location name }
{--location_id= : A location id}
{--base_dn= : A diffrent base DN to use }
{--summary : Print summary }
{--json_summary : Print summary in json format }
{--dryrun : Run the sync process but don\'t update the database}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command line LDAP/AD sync';
/**
* An LdapAd instance.
*
* @var \App\Models\LdapAd
*/
private $ldap;
/**
* LDAP settings collection.
*
* @var \Illuminate\Support\Collection
*/
private $settings = null;
/**
* A default location collection.
*
* @var \Illuminate\Support\Collection
*/
private $defaultLocation = null;
/**
* Mapped locations collection.
*
* @var \Illuminate\Support\Collection
*/
private $mappedLocations = null;
/**
* The summary collection.
*
* @var \Illuminate\Support\Collection
*/
private $summary;
/**
* Is dry-run?
*
* @var bool
*/
private $dryrun = false;
/**
* Show users to be imported.
*
* @var array
*/
private $userlist = [];
/**
* Create a new command instance.
*/
public function __construct(LdapAd $ldap)
{
parent::__construct();
$this->ldap = $ldap;
$this->settings = $this->ldap->ldapSettings;
$this->summary = collect();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$dispatcher = \Adldap\Adldap::getEventDispatcher();
// Listen for all model events.
$dispatcher->listen('Adldap\Models\Events\*', function ($eventName, array $data) {
echo $eventName; // Returns 'Adldap\Models\Events\Updating'
var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating;
\Log::debug("Event: ".$eventName." data - ".print_r($data, true));
});
$dispatcher->listen('Adldap\Auth\Events\*', function ($eventName, array $data) {
echo $eventName; // Returns 'Adldap\Models\Events\Updating'
var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating;
\Log::debug("Event: ".$eventName." data - ".print_r($data, true));
});
ini_set('max_execution_time', env('LDAP_TIME_LIM', "600")); //600 seconds = 10 minutes
ini_set('memory_limit', '500M');
$old_error_reporting = error_reporting(); // grab old error_reporting .ini setting, for later re-enablement
error_reporting($old_error_reporting & ~E_DEPRECATED); // disable deprecation warnings, for LDAP in PHP 7.4 (and greater)
if ($this->option('dryrun')) {
$this->dryrun = true;
}
$this->checkIfLdapIsEnabled();
$this->checkLdapConnection();
$this->setBaseDn();
$this->getUserDefaultLocation();
/*
* Use the default location if set, this is needed for the LDAP users sync page
*/
if (!$this->option('base_dn') && null == $this->defaultLocation) {
$this->getMappedLocations();
}
$this->processLdapUsers();
// Print table of users
if ($this->dryrun) {
$this->info('The following users will be synced!');
$headers = ['First Name', 'Last Name', 'Username', 'Email', 'Employee #', 'Location Id', 'Status'];
$this->table($headers, $this->summary->toArray());
}
error_reporting($old_error_reporting); // re-enable deprecation warnings.
return $this->getSummary();
}
/**
* Generate the LDAP sync summary.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return string
*/
private function getSummary(): string
{
if ($this->option('summary') && null === $this->dryrun) {
$this->summary->each(function ($item) {
$this->info('USER: '.$item['note']);
if ('ERROR' === $item['status']) {
$this->error('ERROR: '.$item['note']);
}
});
} elseif ($this->option('json_summary')) {
$json_summary = [
'error' => false,
'error_message' => '',
'summary' => $this->summary->toArray(),
];
$this->info(json_encode($json_summary));
}
return '';
}
/**
* Create a new user or update an existing user.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $snipeUser
*/
private function updateCreateUser(AdldapUser $snipeUser): void
{
$user = $this->ldap->processUser($snipeUser, $this->defaultLocation, $this->mappedLocations);
$summary = [
'firstname' => $user->first_name,
'lastname' => $user->last_name,
'username' => $user->username,
'employee_number' => $user->employee_num,
'email' => $user->email,
'location_id' => $user->location_id,
];
// Only update the database if is not a dry run
if (!$this->dryrun) {
if ($user->isDirty()) { //if nothing on the user changed, don't bother trying to save anything nor put anything in the summary
if ($user->save()) {
$summary['note'] = ($user->wasRecentlyCreated ? 'CREATED' : 'UPDATED');
$summary['status'] = 'SUCCESS';
} else {
$errors = '';
foreach ($user->getErrors()->getMessages() as $error) {
$errors .= implode(", ",$error);
}
$summary['note'] = $snipeUser->getDN().' was not imported. REASON: '.$errors;
$summary['status'] = 'ERROR';
}
} else {
$summary = null;
}
}
// $summary['note'] = ($user->getOriginal('username') ? 'UPDATED' : 'CREATED'); // this seems, kinda, like, superfluous, relative to the $summary['note'] thing above, yeah?
if($summary) { //if the $user wasn't dirty, $summary was set to null so that we will skip the following push()
$this->summary->push($summary);
}
}
/**
* Process the users to update / create.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
*/
private function processLdapUsers(): void
{
try {
\Log::debug("CAL:LING GET LDAP SUSERS");
$ldapUsers = $this->ldap->getLdapUsers();
\Log::debug("END CALLING GET LDAP USERS");
} catch (Exception $e) {
$this->outputError($e);
exit($e->getMessage());
}
if (0 == $ldapUsers->count()) {
$msg = 'ERROR: No users found!';
Log::error($msg);
if ($this->dryrun) {
$this->error($msg);
}
exit($msg);
}
// Process each individual users
foreach ($ldapUsers->getResults() as $user) { // AdLdap2's paginate() method is weird, it gets *everything* and ->getResults() returns *everything*
$this->updateCreateUser($user);
}
}
/**
* Get the mapped locations if a base_dn is provided.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function getMappedLocations()
{
$ldapOuLocation = Location::where('ldap_ou', '!=', '')->select(['id', 'ldap_ou'])->get();
$locations = $ldapOuLocation->sortBy(function ($ou, $key) {
return strlen($ou->ldap_ou);
});
if ($locations->count() > 0) {
$msg = 'Some locations have special OUs set. Locations will be automatically set for users in those OUs.';
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
$this->mappedLocations = $locations->pluck('ldap_ou', 'id'); // TODO: this seems ok-ish, but the key-> value is going location_id -> OU name, and the primary action here is the opposite of that - going from OU's to location ID's.
}
}
/**
* Set the base dn if supplied.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function setBaseDn(): void
{
if ($this->option('base_dn')) {
$this->ldap->baseDn = $this->option('base_dn');
$msg = sprintf('Importing users from specified base DN: "%s"', $this->ldap->baseDn);
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
}
}
/**
* Get a default location id for imported users.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function getUserDefaultLocation(): void
{
$location = $this->option('location_id') ?? $this->option('location');
if ($location) {
$userLocation = Location::where('name', '=', $location)
->orWhere('id', '=', intval($location))
->select(['name', 'id'])
->first();
if ($userLocation) {
$msg = 'Importing users with default location: '.$userLocation->name.' ('.$userLocation->id.')';
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
$this->defaultLocation = collect([
$userLocation->id => $userLocation->name,
]);
} else {
$msg = 'The supplied location is invalid!';
LOG::error($msg);
if ($this->dryrun) {
$this->error($msg);
}
exit(0);
}
}
}
/**
* Check if LDAP intergration is enabled.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function checkIfLdapIsEnabled(): void
{
if (false === $this->settings['ldap_enabled']) {
$msg = 'LDAP intergration is not enabled. Exiting sync process.';
$this->info($msg);
Log::info($msg);
exit(0);
}
}
/**
* Check to make sure we can access the server.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function checkLdapConnection(): void
{
try {
$this->ldap->testLdapAdUserConnection();
$this->ldap->testLdapAdBindConnection();
} catch (Exception $e) {
$this->outputError($e);
exit(0);
}
}
/**
* Output the json summary to the screen if enabled.
*
* @param Exception $error
*/
private function outputError($error): void
{
if ($this->option('json_summary')) {
$json_summary = [
'error' => true,
'error_message' => $error->getMessage(),
'summary' => [],
];
$this->info(json_encode($json_summary));
}
$this->error($error->getMessage());
LOG::error($error);
}
}

View File

@@ -1,627 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Setting;
use Exception;
use Illuminate\Support\Facades\Crypt;
use App\Models\Ldap;
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*/
function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
// 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
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ldap:troubleshoot
{--ldap-search : Output an ldapsearch command-line for testing your LDAP config}
{--force : Skip the interactive yes/no prompt for confirmation}
{--debug : Include debugging output (verbose)}
{--trace : Include extremely verbose LDAP trace output}
{--timeout=15 : Timeout for LDAP Bind operations}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Runs a series of non-destructive LDAP commands to help try and determine correct LDAP settings for your environment.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Output something *only* if debug is enabled
*
* @return void
*/
public function debugout($string)
{
if($this->option('debug')) {
$this->line($string);
}
}
/**
* 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.
*
* @return mixed
*/
public function handle()
{
if($this->option('trace')) {
ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);
}
$settings = Setting::getSettings();
$this->settings = $settings;
if($this->option('ldap-search')) {
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will display your LDAP password on your terminal. Are you sure this is ok?');
if(!$confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
$output = [];
if($settings->ldap_server_cert_ignore) {
$this->line("# Ignoring server certificate validity");
$output[] = "LDAPTLS_REQCERT=never";
}
if($settings->ldap_client_tls_cert && $settings->ldap_client_tls_key) {
$this->line("# Adding LDAP Client Certificate and Key");
$output[] = "LDAPTLS_CERT=storage/ldap_client_tls.cert";
$output[] = "LDAPTLS_KEY=storage/ldap_client_tls.key";
}
$output[] = "ldapsearch";
$output[] = "-H ".$settings->ldap_server;
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$output[] = "-w ". escapeshellarg($w);
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
$output[] = "-Z";
}
$output[] = "-v";
$this->line("\n");
$this->line(implode(" \\\n",$output));
exit(0);
}
//PHP Version check for warning
$php_version = phpversion();
list($major, $minor, $patch) = explode('.', $php_version);
if (
$major < 8 ||
($major == 8 && $minor < 3) ||
($major == 8 && $minor == 3 && $patch < 21) ||
($major == 8 && $minor == 4 && $patch < 7)
) {
$this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
if (!$this->confirm("Are you sure you wish to continue?")) {
$this->warn("ABORTING");
exit(-1);
}
}
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
//$this->line(print_r($settings,true));
$this->line("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
}
$ldap_conn = false;
try {
$ldap_conn = ldap_connect($settings->ldap_server);
} catch (Exception $e) {
$this->error("WARNING: Exception caught when executing 'ldap_connect()' - ".$e->getMessage().". We will try to guess.");
}
if(!$ldap_conn) {
$this->error("WARNING: LDAP Server setting of: ".$settings->ldap_server." cannot be parsed. We will try to guess.");
//exit(-1);
}
//since we never use $ldap_conn again, we don't have to ldap_unbind() it (it's not even connected, tbh - that only happens at bind-time)
$parsed = parse_url($settings->ldap_server);
if(@$parsed['scheme'] != 'ldap' && @$parsed['scheme'] != 'ldaps') {
$this->error("WARNING: LDAP URL Scheme of '".@$parsed['scheme']."' is probably incorrect; should usually be ldap or ldaps");
}
if(!@$parsed['host']) {
$this->error("ERROR: Cannot determine hostname or IP from ldap URL: ".$settings->ldap_server.". ABORTING.");
exit(-1);
} else {
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
}
$raw_ips = [];
if (inet_pton($parsed['host']) !== false) {
$this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
$raw_ips[] = $parsed['host'];
} else {
$this->line("Performing DNS lookup of: " . $parsed['host']);
$ips = dns_get_record($parsed['host']);
//$this->info("Host IP is: ".print_r($ips,true));
if (!$ips || count($ips) == 0) {
$this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
exit(-1);
}
$this->debugout("IP's? " . print_r($ips, true));
foreach ($ips as $ip) {
if (!isset($ip['ip'])) {
continue;
}
$raw_ips[] = $ip['ip'];
}
}
foreach ($raw_ips as $ip) {
if ($ip == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
}
if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
}
}
$this->line("STAGE 2: Checking basic network connectivity");
$ports = [636, 389];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
$ports[] = $parsed['port'];
}
$open_ports=[];
foreach($ports as $port ) {
$errno = 0;
$errstr = '';
$timeout = 30.0;
$result = '';
$this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) {
$this->error("Exception: ".$e->getMessage());
}
if($result) {
$this->info("Success!");
$open_ports[] = $port;
} else {
$this->error("WARNING: Cannot connect to port: $port - $errstr ($errno)");
}
}
if(count($open_ports) == 0) {
$this->error("ERROR - no open ports. ABORTING.");
exit(-1);
}
$this->line("STAGE 3: Determine encryption algorithm, if any");
$ldap_urls = []; // [url, cert-check?, start_tls?]
$pretty_ldap_urls = [];
foreach($open_ports as $port) {
$this->line("Trying TLS first for port $port");
$ldap_url = "ldaps://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
}
if($this->test_anonymous_bind($ldap_url, false)) {
$this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
$ldap_urls[] = [$ldap_url, false, false];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
}
// now switching to ldap:// URL's from ldaps://
$ldap_url = "ldap://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
}
if ($this->test_anonymous_bind($ldap_url, false, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
$ldap_urls[] = [$ldap_url, false, true];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
}
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
}
}
$this->debugout(print_r($ldap_urls,true));
if(count($ldap_urls) > 0 ) {
$this->debugout("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->debugout("LDAP URL: " . $ldap_url[0]);
$this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
}
$this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
} else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1);
}
$this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) {
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
}
$this->line("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants();
$ldap_constants = [];
foreach($all_defined_constants AS $key => $val) {
if(starts_with($key,"LDAP_") && is_string($val)) {
$ldap_constants[$val] = $key; // INVERT the meaning here!
}
}
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
foreach($ldap_urls AS $ldap_url) {
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
$this->info("Success getting informational bind!");
} else {
$this->error("Unable to get information from bind.");
}
}
$this->line("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) {
$this->line("Starting auth to " . $ldap_url[0]);
while(true) {
$with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using";
if(!$this->confirm('Do you wish to try to authenticate to this directory: '.$ldap_url[0]." $with_tls TLS and $with_startssl STARTSSL?")) {
break;
}
$username = $this->ask("Username");
$password = $this->secret("Password");
$results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
if ($results) {
$this->info("Success authenticating with " . $username);
} else {
$this->error("Unable to authenticate with " . $username);
}
}
}
$this->info("LDAP TROUBLESHOOTING COMPLETE!");
}
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{
if ($check_cert) {
$this->line("we *ARE* checking certs");
Ldap::ignoreCertificates(false);
} else {
$this->line("we are IGNORING certs");
Ldap::ignoreCertificates(true);
}
$lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
putenv('LDAPTLS_KEY=storage/ldap_client_tls.key');
}
if($start_tls) {
if(!ldap_start_tls($lconn)) {
$this->error("WARNING: Unable to start TLS");
return false;
}
}
if(!$lconn) {
$this->error("WARNING: Failed to generate connection string - using: ".$ldap_url);
return false;
}
$net = ldap_set_option($lconn, LDAP_OPT_NETWORK_TIMEOUT, $this->option('timeout'));
$time = ldap_set_option($lconn, LDAP_OPT_TIMELIMIT, $this->option('timeout'));
if(!$net || !$time) {
$this->error("Unable to set timeouts!");
}
return $lconn;
}
public function test_anonymous_bind($ldap_url, $check_cert = true, $start_tls = false)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$this->line("Attempting to bind now, this can take a while if we mess it up");
$bind_results = ldap_bind($lconn);
$this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
ldap_close($lconn);
return (bool)$bind_results;
} catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
return false;
}
});
}
public function test_authed_bind($ldap_url, $check_cert, $start_tls, $username, $password)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password);
ldap_close($lconn);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
} else {
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
return (bool)$lconn;
}
} catch (Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
}
});
}
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password,$settings)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password, $settings) {
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);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
}
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
$cleaned_results = [];
try {
// This _may_ only work for Active Directory?
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
//$this->line(print_r($cleaned_results,true));
$default_naming_contexts = $cleaned_results[0]['namingcontexts'];
$this->info("Default Naming Contexts:");
$this->info(implode(", ", $default_naming_contexts));
//okay, great - now how do we display those results? I have no idea.
} catch (\Exception $e) {
$this->error("Unable to get base naming contexts - here's what we *did* get:");
$this->line(print_r($cleaned_results, true));
}
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->debugout("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));
$entries = ldap_get_entries($conn, $search_results);
$this->info("Printing first 10 results: ");
$pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
//print_r($data);
$headers = [];
foreach ($pretty_data as $row) {
//populate headers
foreach ($row as $key => $value) {
//skip objectsid and objectguid because it junks up output
if ($key == "objectsid" || $key == "objectguid") {
continue;
}
if (!in_array($key, $headers)) {
$headers[] = $key;
}
}
}
$table = [];
//repeat again to populate table
foreach ($pretty_data as $row) {
$newrow = [];
foreach ($headers as $header) {
if (is_array(@$row[$header])) {
$newrow[] = "[" . implode(", ", $row[$header]) . "]";
} else {
$newrow[] = @$row[$header];
}
}
$table[] = $newrow;
}
$this->table($headers, $table);
} catch (\Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
} finally {
ldap_close($conn);
}
});
}
/***********************************************
*
* This function executes $function - which is expected to be some kind of executable function -
* with a timeout set. It respects the timeout by forking execution and setting a strict timer
* for which to get back a SIGUSR1 or SIGUSR2 signal from the forked process.
*
***********************************************/
private function timed_boolean_execute($function)
{
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
$this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function();
} else {
$parent_pid = posix_getpid();
$pid = pcntl_fork();
switch($pid) {
case 0:
//we're the 'child'
if($function()) {
//SUCCESS = SIGUSR1
posix_kill($parent_pid, SIGUSR1);
} else {
//FAILURE = SIGUSR2
posix_kill($parent_pid, SIGUSR2);
}
exit();
break; //yes I know we don't need it.
case -1:
//couldn't fork
$this->error("COULD NOT FORK - assuming failure");
return false;
break; //I still know that we don't need it
default:
//we remain the 'parent', $pid is the PID of the forked process.
$siginfo = [];
$exit_status = pcntl_sigtimedwait ([SIGUSR1, SIGUSR2], $siginfo, $this->option('timeout'));
if ($exit_status == SIGUSR1) {
return true;
} else {
posix_kill($pid, SIGKILL); //make sure we don't have processes hanging around that might try and send signals during later executions, confusing us
return false;
}
break; //Yeah I get it already, shush.
}
}
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Events\UserMerged; use Illuminate\Console\Command;
use App\Models\User; use App\Models\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command;
class MergeUsersByUsername extends Command class MergeUsersByUsername extends Command
{ {
@@ -42,91 +42,71 @@ class MergeUsersByUsername extends Command
{ {
// Get the list of users who have an email address as their username // Get the list of users who have an email address as their username
$users = User::where('username', 'LIKE', '%@%')->whereNull('deleted_at')->get(); $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) { foreach ($users as $user) {
$parts = explode('@', trim($user->username)); $parts = explode("@", $user->username);
$this->info('Checking against username '.trim($parts[0]).'.'); $bad_users = User::where('username', '=', $parts[0])->whereNull('deleted_at')->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')->get();
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->get();
foreach ($bad_users as $bad_user) { foreach ($bad_users as $bad_user) {
$this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') '); $this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') ');
// Walk the list of assets // Walk the list of assets
foreach ($bad_user->assets as $asset) { foreach ($bad_user->assets as $asset) {
$this->info('Updating asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id); $this->info( 'Updating asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id);
$asset->assigned_to = $user->id; $asset->assigned_to = $user->id;
if (! $asset->save()) { if (!$asset->save()) {
$this->error('Could not update assigned_to field on asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id); $this->error( 'Could not update assigned_to field on asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id);
$this->error('Error saving: '.$asset->getErrors()); $this->error( 'Error saving: '.$asset->getErrors());
} }
} }
// Walk the list of licenses // Walk the list of licenses
foreach ($bad_user->licenses as $license) { foreach ($bad_user->licenses as $license) {
$this->info('Updating license '.$license->name.' '.$license->id.' to user '.$user->id); $this->info( 'Updating license '.$license->name.' '.$license->id.' to user '.$user->id);
$bad_user->licenses()->updateExistingPivot($license->id, ['assigned_to' => $user->id]); $bad_user->licenses()->updateExistingPivot($license->id, ['assigned_to' => $user->id]);
} }
// Walk the list of consumables // Walk the list of consumables
foreach ($bad_user->consumables as $consumable) { foreach ($bad_user->consumables as $consumable) {
$this->info('Updating consumable '.$consumable->id.' to user '.$user->id); $this->info( 'Updating consumable '.$consumable->id.' to user '.$user->id);
$bad_user->consumables()->updateExistingPivot($consumable->id, ['assigned_to' => $user->id]); $bad_user->consumables()->updateExistingPivot($consumable->id, ['assigned_to' => $user->id]);
} }
// Walk the list of accessories // Walk the list of accessories
foreach ($bad_user->accessories as $accessory) { foreach ($bad_user->accessories as $accessory) {
$this->info('Updating accessory '.$accessory->id.' to user '.$user->id); $this->info( 'Updating accessory '.$accessory->id.' to user '.$user->id);
$bad_user->accessories()->updateExistingPivot($accessory->id, ['assigned_to' => $user->id]); $bad_user->accessories()->updateExistingPivot($accessory->id, ['assigned_to' => $user->id]);
} }
// Walk the list of logs // Walk the list of logs
foreach ($bad_user->userlog as $log) { foreach ($bad_user->userlog as $log) {
$this->info('Updating action log record '.$log->id.' to user '.$user->id); $this->info( 'Updating action log record '.$log->id.' to user '.$user->id);
$log->target_id = $user->id; $log->target_id = $user->id;
$log->save(); $log->save();
} }
// Update any manager IDs // Update any manager IDs
$this->info('Updating managed user records to user '.$user->id); $this->info( 'Updating managed user records to user '.$user->id);
User::where('manager_id', '=', $bad_user->id)->update(['manager_id' => $user->id]); User::where('manager_id', '=', $bad_user->id)->update(['manager_id' => $user->id]);
// Update location manager IDs // Update location manager IDs
foreach ($bad_user->managedLocations as $managedLocation) { foreach ($bad_user->managedLocations as $managedLocation) {
$this->info('Updating managed location record '.$managedLocation->name.' to manager '.$user->id); $this->info( 'Updating managed location record '.$managedLocation->name.' to manager '.$user->id);
$managedLocation->manager_id = $user->id; $managedLocation->manager_id = $user->id;
$managedLocation->save(); $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 // Mark the user as deleted
$this->info('Marking the user as deleted'); $this->info( 'Marking the user as deleted');
$bad_user->deleted_at = Carbon::now()->timestamp; $bad_user->deleted_at = Carbon::now()->timestamp;
$bad_user->save(); $bad_user->save();
event(new UserMerged($bad_user, $user, null));
} }
} }
} }
} }

View File

@@ -2,9 +2,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class MoveUploadsToNewDisk extends Command class MoveUploadsToNewDisk extends Command
{ {
@@ -39,10 +39,10 @@ class MoveUploadsToNewDisk extends Command
*/ */
public function handle() public function handle()
{ {
if (config('filesystems.default') == 'local') {
if (config('filesystems.default')=='local') {
$this->error('Your current disk is set to local so we cannot proceed.'); $this->error('Your current disk is set to local so we cannot proceed.');
$this->warn("Please configure your .env settings for S3. \nChange your PUBLIC_FILESYSTEM_DISK value to 's3_public' and your PRIVATE_FILESYSTEM_DISK to s3_private."); $this->warn("Please configure your .env settings for S3. \nChange your PUBLIC_FILESYSTEM_DISK value to 's3_public' and your PRIVATE_FILESYSTEM_DISK to s3_private.");
return false; return false;
} }
$delete_local = $this->argument('delete_local'); $delete_local = $this->argument('delete_local');
@@ -62,9 +62,10 @@ class MoveUploadsToNewDisk extends Command
// iterate files // iterate files
foreach ($public_uploads as $public_type => $public_upload) { foreach($public_uploads as $public_type => $public_upload)
{
$type_count = 0; $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++) { for ($i = 0; $i < count($public_upload); $i++) {
$type_count++; $type_count++;
@@ -75,10 +76,12 @@ class MoveUploadsToNewDisk extends Command
$new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename); $new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename);
$this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url); $this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::debug($e); \Log::debug($e);
$this->error($e); $this->error($e);
} }
} }
} }
$logos = glob("public/uploads/setting*.*"); $logos = glob("public/uploads/setting*.*");
@@ -89,80 +92,92 @@ class MoveUploadsToNewDisk extends Command
$this->info($logo); $this->info($logo);
$type_count++; $type_count++;
$filename = basename($logo); $filename = basename($logo);
Storage::disk('public')->put('uploads/' . $filename, file_get_contents($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); $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['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*"); $private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*"); $private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*"); $private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*"); $private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*"); $private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*"); $private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
$private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*"); $private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*");
foreach ($private_uploads as $private_type => $private_upload) { 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; $type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) { for ($x = 0; $x < count($private_upload); $x++) {
$type_count++; $type_count++;
$filename = basename($private_upload[$x]); $filename = basename($private_upload[$x]);
try { try {
Storage::put($private_type . '/' . $filename, file_get_contents($private_upload[$i])); Storage::put($private_type.'/'.$filename, file_get_contents($private_upload[$i]));
$new_url = Storage::url($private_type . '/' . $filename, $filename); $new_url = Storage::url($private_type.'/'.$filename, $filename);
$this->info($type_count . '. PRIVATE: ' . $filename . ' was copied to ' . $new_url); $this->info($type_count.'. PRIVATE: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) {
Log::debug($e); } catch (\Exception $e) {
$this->error($e); \Log::debug($e);
} $this->error($e);
} }
} }
}
if ($delete_local == 'true') {
$public_delete_count = 0;
$private_delete_count = 0;
$this->info("\n\n"); if ($delete_local=='true') {
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); $public_delete_count = 0;
$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"); $private_delete_count = 0;
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
if ($this->confirm('Do you wish to continue?')) { $this->info("\n\n");
foreach ($public_uploads as $public_type => $public_upload) { $this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
for ($i = 0; $i < count($public_upload); $i++) { $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");
$filename = $public_upload[$i]; $this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
try {
unlink($filename); if ($this->confirm("Do you wish to continue?")) {
$public_delete_count++;
} catch (\Exception $e) { foreach($public_uploads as $public_type => $public_upload) {
Log::debug($e);
$this->error($e); 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

@@ -1,19 +1,19 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Helper\ProgressIndicator;
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
/** /**
* Class ObjectImportCommand * Class ObjectImportCommand
*/ */
class ObjectImportCommand extends Command class ObjectImportCommand extends Command
{ {
/** /**
* The console command name. * The console command name.
* *
@@ -28,11 +28,6 @@ class ObjectImportCommand extends Command
*/ */
protected $description = 'Import Items from CSV'; protected $description = 'Import Items from CSV';
/**
* The progress indicator instance.
*/
protected ProgressIndicator $progressIndicator;
/** /**
* Create a new command instance. * Create a new command instance.
* *
@@ -42,7 +37,7 @@ class ObjectImportCommand extends Command
{ {
parent::__construct(); parent::__construct();
} }
private $bar;
/** /**
* Execute the console command. * Execute the console command.
* *
@@ -50,42 +45,60 @@ class ObjectImportCommand extends Command
*/ */
public function handle() public function handle()
{ {
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
$this->progressIndicator = new ProgressIndicator($this->output);
$filename = $this->argument('filename'); $filename = $this->argument('filename');
$class = title_case($this->option('item-type')); $class = title_case($this->option('item-type'));
$classString = "App\\Importer\\{$class}Importer"; $classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename); $importer = new $classString($filename);
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback']) $importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
->setCreatedBy($this->option('user_id')) ->setUserId($this->option('user_id'))
->setUpdating($this->option('update')) ->setUpdating($this->option('update'))
->setShouldNotify($this->option('send-welcome')) ->setShouldNotify($this->option('send-welcome'))
->setUsernameFormat($this->option('username_format')); ->setUsernameFormat($this->option('username_format'));
// This $logFile/useFiles() bit is currently broken, so commenting it out for now // This $logFile/useFiles() bit is currently broken, so commenting it out for now
// $logFile = $this->option('logfile'); // $logFile = $this->option('logfile');
// Log::useFiles($logFile); // \Log::useFiles($logFile);
$this->progressIndicator->start('======= Importing Items from '.$filename.' ========='); $this->comment('======= Importing Items from '.$filename.' =========');
$importer->import(); $importer->import();
$this->progressIndicator->finish('Import finished.'); $this->bar = null;
if (!empty($this->errors)) {
$this->comment("The following Errors were encountered.");
foreach ($this->errors as $asset => $error) {
$this->comment('Error: Item: ' . $asset . ' failed validation: ' . json_encode($error));
}
} else {
$this->comment("All Items imported successfully!");
}
$this->comment("");
return;
} }
public function errorCallback($item, $field, $error) public function errorCallback($item, $field, $errorString)
{ {
$this->output->write("\x0D\x1B[2K"); $this->errors[$item->name][$field] = $errorString;
$this->warn('Error: Item: '.$item->name.' failed validation: '.json_encode($error));
} }
public function progress($count)
public function progress($importedItemsCount)
{ {
$this->progressIndicator->advance(); if (!$this->bar) {
$this->bar = $this->output->createProgressBar($count);
}
static $index =0;
$index++;
if ($index < $count) {
$this->bar->advance();
} else {
$this->bar->finish();
}
} }
// Tracks the current item for error messages
private $updating;
// An array of errors encountered while parsing
private $errors;
/** /**
* Log a message to file, configurable by the --log-file parameter. * Log a message to file, configurable by the --log-file parameter.
@@ -95,20 +108,19 @@ class ObjectImportCommand extends Command
* @since 3.0 * @since 3.0
* @param string $string * @param string $string
* @param string $level * @param string $level
*/ */
public function log($string, $level = 'info') public function log($string, $level = 'info')
{ {
if ($level === 'warning') { if ($level === 'warning') {
Log::warning($string); \Log::warning($string);
$this->comment($string); $this->comment($string);
} else { } else {
Log::Info($string); \Log::Info($string);
if ($this->option('verbose')) { if ($this->option('verbose')) {
$this->comment($string); $this->comment($string);
} }
} }
} }
/** /**
* Get the console command arguments. * Get the console command arguments.
* *
@@ -118,11 +130,12 @@ class ObjectImportCommand extends Command
*/ */
protected function getArguments() protected function getArguments()
{ {
return [ return array(
['filename', InputArgument::REQUIRED, 'File for the CSV import.'], array('filename', InputArgument::REQUIRED, 'File for the CSV import.'),
]; );
} }
/** /**
* Get the console command options. * Get the console command options.
* *
@@ -132,15 +145,16 @@ class ObjectImportCommand extends Command
*/ */
protected function getOptions() protected function getOptions()
{ {
return [ return array(
['email_format', null, InputOption::VALUE_REQUIRED, 'The format of the email addresses that should be generated. Options are firstname.lastname, firstname, filastname', null], array('email_format', null, InputOption::VALUE_REQUIRED, 'The format of the email addresses that should be generated. Options are firstname.lastname, firstname, filastname', null),
['username_format', null, InputOption::VALUE_REQUIRED, 'The format of the username that should be generated. Options are firstname.lastname, firstname, filastname, email', null], array('username_format', null, InputOption::VALUE_REQUIRED, 'The format of the username that should be generated. Options are firstname.lastname, firstname, filastname, email', null),
['logfile', null, InputOption::VALUE_REQUIRED, 'The path to log output to. storage/logs/importer.log by default', storage_path('logs/importer.log')], array('logfile', null, InputOption::VALUE_REQUIRED, 'The path to log output to. storage/logs/importer.log by default', storage_path('logs/importer.log') ),
['item-type', null, InputOption::VALUE_REQUIRED, 'Item Type To import. Valid Options are Asset, Consumable, Accessory, License, or User', 'Asset'], array('item-type', null, InputOption::VALUE_REQUIRED, 'Item Type To import. Valid Options are Asset, Consumable, Accessory, License, or User', 'Asset'),
['web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'], array('web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'),
['user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1], array('user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1),
['update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'], array('update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'),
['send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'], array('send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'),
]; );
} }
} }

View File

@@ -4,8 +4,8 @@ namespace App\Console\Commands;
use App\Models\Asset; use App\Models\Asset;
use App\Models\CustomField; use App\Models\CustomField;
use Illuminate\Support\Facades\Schema; use Schema;
use Illuminate\Support\Facades\DB; use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class PaveIt extends Command class PaveIt extends Command
@@ -51,7 +51,8 @@ class PaveIt extends Command
} }
// List all the tables in the database so we don't have to worry about missing some as the app grows // List all the tables in the database so we don't have to worry about missing some as the app grows
$tables = Schema::getTables(); $tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
$except_tables = [ $except_tables = [
'oauth_access_tokens', 'oauth_access_tokens',
'oauth_clients', 'oauth_clients',
@@ -59,9 +60,6 @@ class PaveIt extends Command
'migrations', 'migrations',
'settings', 'settings',
'users', 'users',
'telescope_entries',
'telescope_entries_tags',
'telescope_monitoring',
]; ];
// We only need to find out what these are so we can nuke these columns on the assets table. // We only need to find out what these are so we can nuke these columns on the assets table.
@@ -69,15 +67,14 @@ class PaveIt extends Command
foreach ($custom_fields as $custom_field) { foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.'); $this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
if (Schema::hasColumn('assets', $custom_field->db_column)) { if (\Schema::hasColumn('assets', $custom_field->db_column)) {
Schema::table('assets', function ($table) use ($custom_field) { \Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column); $table->dropColumn($custom_field->db_column);
}); });
} }
} }
foreach ($tables as $table_obj) { foreach ($tables as $table) {
$table = $table_obj['name'];
if (in_array($table, $except_tables)) { if (in_array($table, $except_tables)) {
$this->info($table. ' is SKIPPED.'); $this->info($table. ' is SKIPPED.');
} else { } else {
@@ -87,8 +84,8 @@ class PaveIt extends Command
} }
// Leave in the demo oauth keys so we don't have to reset them every day in the demos // Leave in the demo oauth keys so we don't have to reset them every day in the demos
DB::statement('delete from oauth_clients WHERE id > 2'); \DB::statement('delete from oauth_clients WHERE id > 2');
DB::statement('delete from oauth_access_tokens WHERE user_id > 2'); \DB::statement('delete from oauth_access_tokens WHERE id > 2');
} }
} }

View File

@@ -3,7 +3,6 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Category; use App\Models\Category;
@@ -16,8 +15,6 @@ use App\Models\Statuslabel;
use App\Models\Supplier; use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class Purge extends Command class Purge extends Command
{ {
@@ -53,7 +50,7 @@ class Purge extends Command
public function handle() public function handle()
{ {
$force = $this->option('force'); $force = $this->option('force');
if (($this->confirm("\n****************************************************\nTHIS WILL PURGE ALL SOFT-DELETED ITEMS IN YOUR SYSTEM. \nThere is NO undo. This WILL permanently destroy \nALL of your deleted data. \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) || $force == 'true') { if (($this->confirm("\n****************************************************\nTHIS WILL PURGE ALL SOFT-DELETED ITEMS IN YOUR SYSTEM. \nThere is NO undo. This WILL permanently destroy \nALL of your deleted data. \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) || $force == 'true') {
/** /**
* Delete assets * Delete assets
@@ -62,19 +59,19 @@ class Purge extends Command
$assetcount = $assets->count(); $assetcount = $assets->count();
$this->info($assets->count().' assets purged.'); $this->info($assets->count().' assets purged.');
$asset_assoc = 0; $asset_assoc = 0;
$maintenances = 0; $asset_maintenances = 0;
foreach ($assets as $asset) { foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->display_name.'" deleted.'); $this->info('- Asset "'.$asset->present()->name().'" deleted.');
$asset_assoc += $asset->assetlog()->count(); $asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete(); $asset->assetlog()->forceDelete();
$maintenances += $asset->maintenances()->count(); $asset_maintenances += $asset->assetmaintenances()->count();
$asset->maintenances()->forceDelete(); $asset->assetmaintenances()->forceDelete();
$asset->forceDelete(); $asset->forceDelete();
} }
$this->info($asset_assoc.' corresponding log records purged.'); $this->info($asset_assoc.' corresponding log records purged.');
$this->info($maintenances.' corresponding maintenance records purged.'); $this->info($asset_maintenances.' corresponding maintenance records purged.');
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get(); $locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($locations->count().' locations purged.'); $this->info($locations->count().' locations purged.');
@@ -83,8 +80,9 @@ class Purge extends Command
$location->forceDelete(); $location->forceDelete();
} }
$accessories = Accessory::whereNotNull('deleted_at')->withTrashed()->get(); $accessories = Accessory::whereNotNull('deleted_at')->withTrashed()->get();
$accessory_assoc = 0; $accessory_assoc=0;
$this->info($accessories->count().' accessories purged.'); $this->info($accessories->count().' accessories purged.');
foreach ($accessories as $accessory) { foreach ($accessories as $accessory) {
$this->info('- Accessory "'.$accessory->name.'" deleted.'); $this->info('- Accessory "'.$accessory->name.'" deleted.');
@@ -94,6 +92,7 @@ class Purge extends Command
} }
$this->info($accessory_assoc.' corresponding log records purged.'); $this->info($accessory_assoc.' corresponding log records purged.');
$consumables = Consumable::whereNotNull('deleted_at')->withTrashed()->get(); $consumables = Consumable::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($consumables->count().' consumables purged.'); $this->info($consumables->count().' consumables purged.');
foreach ($consumables as $consumable) { foreach ($consumables as $consumable) {
@@ -102,6 +101,7 @@ class Purge extends Command
$consumable->forceDelete(); $consumable->forceDelete();
} }
$components = Component::whereNotNull('deleted_at')->withTrashed()->get(); $components = Component::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($components->count().' components purged.'); $this->info($components->count().' components purged.');
foreach ($components as $component) { foreach ($components as $component) {
@@ -126,6 +126,7 @@ class Purge extends Command
$model->forceDelete(); $model->forceDelete();
} }
$categories = Category::whereNotNull('deleted_at')->withTrashed()->get(); $categories = Category::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($categories->count().' categories purged.'); $this->info($categories->count().' categories purged.');
foreach ($categories as $category) { foreach ($categories as $category) {
@@ -144,20 +145,6 @@ class Purge extends Command
$this->info($users->count().' users purged.'); $this->info($users->count().' users purged.');
$user_assoc = 0; $user_assoc = 0;
foreach ($users as $user) { foreach ($users as $user) {
$rel_path = 'private_uploads/users';
$filenames = Actionlog::where('action_type', 'uploaded')
->where('item_id', $user->id)
->pluck('filename');
foreach($filenames as $filename) {
try {
if (Storage::exists($rel_path . '/' . $filename)) {
Storage::delete($rel_path . '/' . $filename);
}
} catch (\Exception $e) {
Log::info('An error occurred while deleting files: ' . $e->getMessage());
}
}
$this->info('- User "'.$user->username.'" deleted.'); $this->info('- User "'.$user->username.'" deleted.');
$user_assoc += $user->userlog()->count(); $user_assoc += $user->userlog()->count();
$user->userlog()->forceDelete(); $user->userlog()->forceDelete();
@@ -178,8 +165,11 @@ class Purge extends Command
$this->info('- Status Label "'.$status_label->name.'" deleted.'); $this->info('- Status Label "'.$status_label->name.'" deleted.');
$status_label->forceDelete(); $status_label->forceDelete();
} }
} else { } else {
$this->info('Action canceled. Nothing was purged.'); $this->info('Action canceled. Nothing was purged.');
} }
} }
} }

View File

@@ -38,7 +38,7 @@ class PurgeLoginAttempts extends Command
public function handle() public function handle()
{ {
if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE YOUR LOGIN ATTEMPT RECORDS. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) { if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE YOUR LOGIN ATTEMPT RECORDS. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) {
\DB::statement('delete from login_attempts'); \DB::statement('delete from login_attempts');
} }
} }
} }

View File

@@ -48,13 +48,15 @@ class ReEncodeCustomFieldNames extends Command
*/ */
public function handle() public function handle()
{ {
if ($this->confirm('This will regenerate all of the custom field database fieldnames in your database. THIS WILL CHANGE YOUR SCHEMA AND SHOULD NOT BE DONE WITHOUT MAKING A BACKUP FIRST. Do you wish to continue?')) {
if ($this->confirm('This will regenerate all of the custom field database fieldnames in your database. THIS WILL CHANGE YOUR SCHEMA AND SHOULD NOT BE DONE WITHOUT MAKING A BACKUP FIRST. Do you wish to continue?'))
{
/** Get all of the custom fields */ /** Get all of the custom fields */
$fields = CustomField::get(); $fields = CustomField::get();
$asset_columns = \DB::getSchemaBuilder()->getColumnListing('assets'); $asset_columns = \DB::getSchemaBuilder()->getColumnListing('assets');
$custom_field_columns = []; $custom_field_columns = array();
/** Loop through the columns on the assets table */ /** Loop through the columns on the assets table */
foreach ($asset_columns as $asset_column) { foreach ($asset_columns as $asset_column) {
@@ -69,55 +71,43 @@ class ReEncodeCustomFieldNames extends Command
* Then use that ID as the array key for use comparing the actual assets field name * Then use that ID as the array key for use comparing the actual assets field name
* and the db_column value from the custom fields table. * and the db_column value from the custom fields table.
*/ */
$last_part = substr(strrchr($asset_column, '_snipeit_'), 1); $last_part = substr(strrchr($asset_column, "_snipeit_"), 1);
$custom_field_columns[$last_part] = $asset_column; $custom_field_columns[$last_part] = $asset_column;
} }
} }
foreach ($fields as $field) { 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 */ /** The assets table has the column it should have, all is well */
if ($field->db_column == $field->convertUnicodeDbSlug() && \Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) { if (\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.'); {
$this->info('-- ✓ This field exists - all good');
/** /**
* There is a mismatch between the fieldname on the assets table and * There is a mismatch between the fieldname on the assets table and
* what $field->convertUnicodeDbSlug() is *now* expecting. * what $field->convertUnicodeDbSlug() is *now* expecting.
*/ */
} else { } else {
$this->warn('-- X Field mismatch: updating... ');
if ($field->db_column != $field->convertUnicodeDbSlug()) { /** Make sure the custom_field_columns array has the ID */
$this->error('-- ✘ Field mismatch: '.$field->name.' value should be '.$field->convertUnicodeDbSlug().' but is '.$field->db_column.' in the custom_fields table'); if (array_key_exists($field->id, $custom_field_columns)) {
} else { /**
$this->error('-- ✘ Field mismatch: '.$field->name.' column should be '.$field->convertUnicodeDbSlug().' but is '.$custom_field_columns[$field->id].' on the assets table.'); * Update the asset schema to the corrected fieldname that will be recognized by the
* system elsewhere that we use $field->convertUnicodeDbSlug()
*/
\Schema::table('assets', function($table) use ($custom_field_columns, $field) {
$table->renameColumn($custom_field_columns[$field->id], $field->convertUnicodeDbSlug());
});
} $this->warn('-- ✓ Field updated from '.$custom_field_columns[$field->id].' to '.$field->convertUnicodeDbSlug());
} else {
/** Make sure the custom_field_columns array has the ID */ $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.');
if (array_key_exists($field->id, $custom_field_columns)) { }
/**
* 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();
} 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.');
}
} }
/** Update the db_column property in the custom fields table, just in case it doesn't match the other /** Update the db_column property in the custom fields table, just in case it doesn't match the other
@@ -125,7 +115,12 @@ class ReEncodeCustomFieldNames extends Command
*/ */
$field->db_column = $field->convertUnicodeDbSlug(); $field->db_column = $field->convertUnicodeDbSlug();
$field->save(); $field->save();
} }
} }
} }
} }

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Console\Commands;
use App\LegacyEncrypter\McryptEncrypter;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class RecryptFromMcrypt extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:legacy-recrypt
{--force : Force a re-crypt of encrypted data from MCRYPT.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command allows upgrading users to de-encrypt their deprecated mcrypt encrypted fields and re-encrypt them using the current OpenSSL encryption.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Check and see if they have a legacy app key listed in their .env
// If not, we can try to use the current APP_KEY if looks like it's old
$legacy_key = env('LEGACY_APP_KEY');
$key_parts = explode(':', $legacy_key);
$legacy_cipher = env('LEGACY_CIPHER', 'rijndael-256');
$errors = array();
if (!$legacy_key) {
$this->error('ERROR: You do not have a LEGACY_APP_KEY set in your .env file. Please locate your old APP_KEY and ADD a line to your .env file like: LEGACY_APP_KEY=YOUR_OLD_APP_KEY');
return false;
}
// Do some basic legacy app key length checks
if (strlen($legacy_key) == 32) {
$legacy_length_check = true;
} elseif (array_key_exists('1', $key_parts) && (strlen($key_parts[1])==44)) {
$legacy_key = base64_decode($key_parts[1],true);
$legacy_length_check = true;
} else {
$legacy_length_check = false;
}
// Check that the app key is 32 characters
if ($legacy_length_check === true) {
$this->comment('INFO: Your LEGACY_APP_KEY looks correct. Okay to continue.');
} else {
$this->error('ERROR: Your LEGACY_APP_KEY is not the correct length (32 characters or base64 followed by 44 characters for later versions). Please locate your old APP_KEY and use that as your LEGACY_APP_KEY in your .env file to continue.');
return false;
}
$this->error('================================!!!! WARNING !!!!================================');
$this->error('================================!!!! WARNING !!!!================================');
$this->comment("This tool will attempt to decrypt your old Snipe-IT (mcrypt, now deprecated) encrypted data and re-encrypt it using OpenSSL. \n\nYou should only continue if you have backed up any and all old APP_KEYs and have backed up your data.");
$force = ($this->option('force')) ? true : false;
if ($force || ($this->confirm("Are you SURE you wish to continue?"))) {
$backup_file = 'backups/env-backups/'.'app_key-'.date('Y-m-d-gis');
try {
Storage::disk('local')->put($backup_file, 'APP_KEY: '.config('app.key'));
Storage::disk('local')->append($backup_file, 'LEGACY_APP_KEY: '.$legacy_key);
} catch (\Exception $e) {
$this->info('WARNING: Could not backup app keys');
}
if ($legacy_cipher){
$mcrypter = new McryptEncrypter($legacy_key,$legacy_cipher);
}else{
$mcrypter = new McryptEncrypter($legacy_key);
}
$settings = Setting::getSettings();
if ($settings->ldap_pword=='') {
$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->save();
}
/** @var CustomField[] $custom_fields */
$custom_fields = CustomField::where('field_encrypted','=', 1)->get();
$this->comment('INFO: Retrieving encrypted custom fields...');
$query = Asset::withTrashed();
foreach ($custom_fields as $custom_field) {
$this->comment('FIELD TO RECRYPT: '.$custom_field->name .' ('.$custom_field->db_column.')');
$query->orWhereNotNull($custom_field->db_column);
}
// Get all assets with a value in any of the fields that were encrypted
/** @var Asset[] $assets */
$assets = $query->get();
$bar = $this->output->createProgressBar(count($assets));
foreach ($assets as $asset) {
foreach ($custom_fields as $encrypted_field) {
$columnName = $encrypted_field->db_column;
// Make sure the value isn't null
if ($asset->{$columnName}!='') {
// Try to decrypt the payload using the legacy app key
try {
$decrypted_field = $mcrypter->decrypt($asset->{$columnName});
$asset->{$columnName} = \Crypt::encrypt($decrypted_field);
$this->comment($decrypted_field);
} catch (\Exception $e) {
$errors[] = ' - ERROR: Could not decrypt field ['.$encrypted_field->name.']: '.$e->getMessage();
}
}
}
$asset->save();
$bar->advance();
}
$bar->finish();
if (count($errors) > 0) {
$this->comment("\n\n");
$this->error("The decrypter encountered some errors: \n");
foreach ($errors as $error) {
$this->error($error);
}
}
}
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Setting; use App\Models\Setting;
use Illuminate\Support\Facades\Artisan; use Artisan;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RegenerateAssetTags extends Command class RegenerateAssetTags extends Command
@@ -40,20 +40,23 @@ class RegenerateAssetTags extends Command
*/ */
public function handle() public function handle()
{ {
if ($this->confirm('This will regenerate all of the asset tags within your system. This action is data-destructive and should be used with caution. Do you wish to continue?')) {
if ($this->confirm('This will regenerate all of the asset tags within your system. This action is data-destructive and should be used with caution. Do you wish to continue?'))
{
$output['info'] = []; $output['info'] = [];
$output['warn'] = []; $output['warn'] = [];
$output['error'] = []; $output['error'] = [];
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$start_tag = ($this->option('start')) ? $this->option('start') : (($settings->next_auto_tag_base) ? Setting::getSettings()->next_auto_tag_base : 1); $start_tag = ($this->option('start')) ? $this->option('start') : (($settings->next_auto_tag_base) ? Setting::getSettings()->next_auto_tag_base : 1) ;
$this->info('Starting at '.$start_tag); $this->info('Starting at '.$start_tag);
$total_assets = Asset::orderBy('id', 'asc')->get(); $total_assets = Asset::orderBy('id','asc')->get();
$bar = $this->output->createProgressBar(count($total_assets)); $bar = $this->output->createProgressBar(count($total_assets));
try { try {
Artisan::call('backup:run'); Artisan::call('backup:run');
} catch (\Exception $e) { } catch (\Exception $e) {
$output['error'][] = $e; $output['error'][] = $e;
@@ -61,6 +64,7 @@ class RegenerateAssetTags extends Command
foreach ($total_assets as $asset) { foreach ($total_assets as $asset) {
$start_tag++;
$output['info'][] = 'Asset tag:'.$asset->asset_tag; $output['info'][] = 'Asset tag:'.$asset->asset_tag;
$asset->asset_tag = $settings->auto_increment_prefix.$settings->auto_increment_prefix.$start_tag; $asset->asset_tag = $settings->auto_increment_prefix.$settings->auto_increment_prefix.$start_tag;
@@ -72,33 +76,29 @@ class RegenerateAssetTags extends Command
// Use forceSave here to override model level validation // Use forceSave here to override model level validation
$asset->forceSave(); $asset->forceSave();
$start_tag++;
if ($bar) {
$bar->advance();
}
} }
$settings->next_auto_tag_base = Asset::zerofill($start_tag, $settings->zerofill_count);
$settings->save();
$bar->finish(); $bar->finish();
$this->info("\n"); $this->info("\n");
if (($this->option('output') == 'all') || ($this->option('output') == 'info')) {
if (($this->option('output')=='all') || ($this->option('output')=='info')) {
foreach ($output['info'] as $key => $output_text) { foreach ($output['info'] as $key => $output_text) {
$this->info($output_text); $this->info($output_text);
} }
} }
if (($this->option('output') == 'all') || ($this->option('output') == 'warn')) { if (($this->option('output')=='all') || ($this->option('output')=='warn')) {
foreach ($output['warn'] as $key => $output_text) { foreach ($output['warn'] as $key => $output_text) {
$this->warn($output_text); $this->warn($output_text);
} }
} }
if (($this->option('output') == 'all') || ($this->option('output') == 'error')) { if (($this->option('output')=='all') || ($this->option('output')=='error')) {
foreach ($output['error'] as $key => $output_text) { foreach ($output['error'] as $key => $output_text) {
$this->error($output_text); $this->error($output_text);
} }
} }
} }
} }
} }

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\AssetModel;
use Illuminate\Console\Command;
class RemoveExplicitEols extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:remove-explicit-eols {--model_name= : The name of the asset model to update (use "all" to update all models)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes explicit EOLs on assets with selected model so they may inherit the asset model EOL';
/**
* Execute the console command.
*/
public function handle()
{
$startTime = microtime(true);
if ($this->option('model_name') == 'all') {
$assets = Asset::all();
$this->updateAssets($assets);
} else {
$assetModel = AssetModel::where('name', '=', $this->option('model_name'))->first();
if ($assetModel) {
$assets = Asset::where('model_id', '=', $assetModel->id)->get();
$this->updateAssets($assets);
} else {
$this->error('Asset model not found');
}
}
$endTime = microtime(true);
$executionTime = ($endTime - $startTime);
$this->info('Command executed in ' . round($executionTime, 2) . ' seconds.');
}
private function updateAssets($assets)
{
foreach ($assets as $asset) {
$asset->eol_explicit = 0;
$asset->asset_eol_date = null;
$asset->save();
}
$this->info($assets->count() . ' Assets updated successfully');
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Actionlog;
use Illuminate\Console\Command;
class RemoveInvalidUploadDeleteActionLogItems extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:remove-invalid-upload-delete-action-log-items';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Permanently remove invalid "upload deleted" action log items that have a null filename. This command can potentially result in deleted files being "resurrected" in the UI.';
/**
* Execute the console command.
*/
public function handle()
{
$invalidLogs = Actionlog::query()
->where('action_type', 'upload deleted')
->whereNull('filename')
->withTrashed()
->get();
$this->info("{$invalidLogs->count()} invalid log items found.");
if ($invalidLogs->count() === 0) {
return 0;
}
$this->table(['ID', 'Action Type', 'Item Type', 'Item ID', 'Created At', 'Deleted At'], $invalidLogs->map(fn($log) => [
$log->id,
$log->action_type,
$log->item_type,
$log->item_id,
$log->created_at,
$log->deleted_at,
])->toArray());
if ($this->confirm("Do you wish to remove {$invalidLogs->count()} log items?")) {
$invalidLogs->each(fn($log) => $log->forceDelete());
}
return 0;
}
}

View File

@@ -48,14 +48,13 @@ class ResetDemoSettings extends Command
$settings->auto_increment_assets = 1; $settings->auto_increment_assets = 1;
$settings->logo = 'snipe-logo.png'; $settings->logo = 'snipe-logo.png';
$settings->alert_email = 'service@snipe-it.io'; $settings->alert_email = 'service@snipe-it.io';
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = null; $settings->header_color = null;
$settings->label2_2d_type = 'QRCODE'; $settings->barcode_type = 'QRCODE';
$settings->default_currency = 'USD'; $settings->default_currency = 'USD';
$settings->brand = 2; $settings->brand = 2;
$settings->ldap_enabled = 0; $settings->ldap_enabled = 0;
$settings->full_multiple_companies_support = 0; $settings->full_multiple_companies_support = 0;
$settings->label2_1d_type = 'C128'; $settings->alt_barcode = 'C128';
$settings->skin = ''; $settings->skin = '';
$settings->email_domain = 'snipeitapp.com'; $settings->email_domain = 'snipeitapp.com';
$settings->email_format = 'filastname'; $settings->email_format = 'filastname';
@@ -63,9 +62,9 @@ class ResetDemoSettings extends Command
$settings->date_display_format = 'D M d, Y'; $settings->date_display_format = 'D M d, Y';
$settings->time_display_format = 'g:iA'; $settings->time_display_format = 'g:iA';
$settings->thumbnail_max_h = '30'; $settings->thumbnail_max_h = '30';
$settings->locale = 'en-US'; $settings->locale = 'en';
$settings->version_footer = 'on'; $settings->version_footer = 'on';
$settings->support_footer = 'on'; $settings->support_footer = null;
$settings->saml_enabled = '0'; $settings->saml_enabled = '0';
$settings->saml_sp_x509cert = null; $settings->saml_sp_x509cert = null;
$settings->saml_idp_metadata = null; $settings->saml_idp_metadata = null;
@@ -73,13 +72,12 @@ class ResetDemoSettings extends Command
$settings->saml_forcelogin = '0'; $settings->saml_forcelogin = '0';
$settings->saml_slo = null; $settings->saml_slo = null;
$settings->saml_custom_settings = null; $settings->saml_custom_settings = null;
$settings->default_avatar = 'default.png';
$settings->save(); $settings->save();
if ($user = User::where('username', '=', 'admin')->first()) { if ($user = User::where('username', '=', 'admin')->first()) {
$user->locale = 'en-US'; $user->locale = 'en';
$user->save(); $user->save();
} }

View File

@@ -6,8 +6,8 @@ use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use App\Models\License; use App\Models\License;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Artisan; use Artisan;
use Illuminate\Support\Facades\DB; use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RestoreDeletedUsers extends Command class RestoreDeletedUsers extends Command
@@ -43,15 +43,16 @@ class RestoreDeletedUsers extends Command
*/ */
public function handle() public function handle()
{ {
$start_date = $this->option('start_date'); $start_date = $this->option('start_date');
$end_date = $this->option('end_date'); $end_date = $this->option('end_date');
$asset_totals = 0; $asset_totals = 0;
$license_totals = 0; $license_totals = 0;
$user_count = 0; $user_count = 0;
if (($start_date == '') || ($end_date == '')) {
$this->info('ERROR: All fields are required.');
if (($start_date=='') || ($end_date=='')) {
$this->info('ERROR: All fields are required.');
return false; return false;
} }
@@ -62,15 +63,15 @@ class RestoreDeletedUsers extends Command
foreach ($users as $user) { foreach ($users as $user) {
$user_count++; $user_count++;
$user_logs = Actionlog::where('target_id', $user->id)->where('target_type', User::class) $user_logs = Actionlog::where('target_id', $user->id)->where('target_type',User::class)
->where('action_type', 'checkout')->with('item')->get(); ->where('action_type','checkout')->with('item')->get();
$this->info($user_count.'. '.$user->username.' ('.$user->id.') was deleted at '.$user->deleted_at.' and has '.$user_logs->count().' checkouts associated.'); $this->info($user_count.'. '.$user->username.' ('.$user->id.') was deleted at '.$user->deleted_at. ' and has '.$user_logs->count().' checkouts associated.');
foreach ($user_logs as $user_log) { foreach ($user_logs as $user_log) {
$this->info(' * '.$user_log->item_type.': '.$user_log->item->name.' - item_id: '.$user_log->item_id); $this->info(' * '.$user_log->item_type.': '.$user_log->item->name.' - item_id: '.$user_log->item_id);
if ($user_log->item_type == Asset::class) { if ($user_log->item_type==Asset::class) {
$asset_totals++; $asset_totals++;
DB::table('assets') DB::table('assets')
@@ -78,10 +79,11 @@ class RestoreDeletedUsers extends Command
->update(['assigned_to' => $user->id, 'assigned_type'=> User::class]); ->update(['assigned_to' => $user->id, 'assigned_type'=> User::class]);
$this->info(' ** Asset '.$user_log->item->id.' ('.$user_log->item->asset_tag.') restored to user '.$user->id.''); $this->info(' ** Asset '.$user_log->item->id.' ('.$user_log->item->asset_tag.') restored to user '.$user->id.'');
} elseif ($user_log->item_type == License::class) {
} elseif ($user_log->item_type==License::class) {
$license_totals++; $license_totals++;
$avail_seat = DB::table('license_seats')->where('license_id', '=', $user_log->item->id) $avail_seat = DB::table('license_seats')->where('license_id','=',$user_log->item->id)
->whereNull('assigned_to')->whereNull('asset_id')->whereBetween('updated_at', [$start_date, $end_date])->first(); ->whereNull('assigned_to')->whereNull('asset_id')->whereBetween('updated_at', [$start_date, $end_date])->first();
if ($avail_seat) { if ($avail_seat) {
$this->info(' ** Allocating seat '.$avail_seat->id.' for this License'); $this->info(' ** Allocating seat '.$avail_seat->id.' for this License');
@@ -89,17 +91,27 @@ class RestoreDeletedUsers extends Command
DB::table('license_seats') DB::table('license_seats')
->where('id', $avail_seat->id) ->where('id', $avail_seat->id)
->update(['assigned_to' => $user->id]); ->update(['assigned_to' => $user->id]);
} else { } else {
$this->warn('ERROR: No available seats for '.$user_log->item->name); $this->warn('ERROR: No available seats for '.$user_log->item->name);
} }
} }
} }
$this->warn('Restoring user '.$user->username.'!'); $this->warn('Restoring user '.$user->username.'!');
$user->restore(); $user->restore();
} }
$this->info($asset_totals.' assets affected'); $this->info($asset_totals.' assets affected');
$this->info($license_totals.' licenses affected'); $this->info($license_totals.' licenses affected');
} }
} }

View File

@@ -3,163 +3,8 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use ZipArchive; use ZipArchive;
use Illuminate\Support\Facades\Log;
use enshrined\svgSanitize\Sanitizer;
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.
// remove *only* line-feeds & carriage-returns; helpful for regexes against lines from
// Windows dumps
$line = trim($line, "\r\n");
if($this->statement_is_permitted && $line[0] === ' ') {
return $line . "\n"; //re-add the newline
}
$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 'permitted' block
"/^\\(.*\\)[,;]$/" => false, //older MySQL dump style with one set of values per line
/* we *could* have made the ^INSERT INTO blah VALUES$ turn on the capturing state, and closed it with
a ^(blahblah);$ but it's cleaner to not have to manage the state machine. We're just going to
assume that (blahblah), or (blahblah); are values for INSERT and are always acceptable. */
"<^/\*!40101 SET NAMES '?[a-zA-Z0-9_-]+'? \*/;$>" => false, //using weird delimiters (<,>) for readability. allow quoted or unquoted charsets
"<^/\*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' \*/;$>" => false, //same, now handle zero-values
];
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 . "\n"; //re-add newline
}
}
// 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_checkout' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command class RestoreFromBackup extends Command
{ {
@@ -168,21 +13,17 @@ class RestoreFromBackup extends Command
* *
* @var string * @var string
*/ */
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"} {--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated} {filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar} {--no-progress : Don\'t show a progress bar}';
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}
{--sql-stdout-only : ONLY "Sanitize" the SQL and print it to stdout - useful for debugging - probably requires --sanitize-with-prefix= }';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Restore from a previously created Snipe-IT backup file'; protected $description = 'Restore from a previously created backup';
/** /**
* Create a new command instance. * Create a new command instance.
@@ -202,23 +43,20 @@ class RestoreFromBackup extends Command
public function handle() public function handle()
{ {
$dir = getcwd(); $dir = getcwd();
if( $dir != base_path() ) { // usually only the case when running via webserver, not via command-line print "Current working directory is: $dir\n";
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?!
}
// //
$filename = $this->argument('filename'); $filename = $this->argument('filename');
if (! $filename) { if (!$filename) {
return $this->error('Missing required filename'); 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'); return $this->error("Data loss not confirmed");
} }
if (config('database.default') != 'mysql') { if (config('database.default') != 'mysql') {
return $this->error('DB_CONNECTION must be MySQL in order to perform a restore. Detected: '.config('database.default')); return $this->error("DB_CONNECTION must be MySQL in order to perform a restore. Detected: ".config('database.default'));
} }
$za = new ZipArchive(); $za = new ZipArchive();
@@ -226,57 +64,51 @@ class RestoreFromBackup extends Command
$errcode = $za->open($filename/* , ZipArchive::RDONLY */); // that constant only exists in PHP 7.4 and higher $errcode = $za->open($filename/* , ZipArchive::RDONLY */); // that constant only exists in PHP 7.4 and higher
if ($errcode !== true) { if ($errcode !== true) {
$errors = [ $errors = [
ZipArchive::ER_EXISTS => 'File already exists.', ZipArchive::ER_EXISTS => "File already exists.",
ZipArchive::ER_INCONS => 'Zip archive inconsistent.', ZipArchive::ER_INCONS => "Zip archive inconsistent.",
ZipArchive::ER_INVAL => 'Invalid argument.', ZipArchive::ER_INVAL => "Invalid argument.",
ZipArchive::ER_MEMORY => 'Malloc failure.', ZipArchive::ER_MEMORY => "Malloc failure.",
ZipArchive::ER_NOENT => 'No such file ('.$filename.') in directory '.$dir.'.', ZipArchive::ER_NOENT => "No such file.",
ZipArchive::ER_NOZIP => 'Not a zip archive.', ZipArchive::ER_NOZIP => "Not a zip archive.",
ZipArchive::ER_OPEN => "Can't open file.", ZipArchive::ER_OPEN => "Can't open file.",
ZipArchive::ER_READ => 'Read error.', ZipArchive::ER_READ => "Read error.",
ZipArchive::ER_SEEK => 'Seek error.', ZipArchive::ER_SEEK => "Seek error."
]; ];
return $this->error('Could not access file: '.$filename.' - '.array_key_exists($errcode, $errors) ? $errors[$errcode] : " Unknown reason: $errcode"); return $this->error("Could not access file: ".$filename." - ".array_key_exists($errcode,$errors) ? $errors[$errcode] : " Unknown reason: $errcode");
} }
$private_dirs = [ $private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', //this was changed from asset_maintenances => maintenances Aug 10 2025
'storage/private_uploads/maintenances', //but let 'maintenances' take precedence
'storage/private_uploads/models', //and let 'models' take precedence
'storage/private_uploads/assets', // these are asset _files_, not the pictures. 'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits', 'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports', 'storage/private_uploads/imports',
'storage/private_uploads/locations', 'storage/private_uploads/assetmodels',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users', 'storage/private_uploads/users',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures'
]; ];
$private_files = [ $private_files = [
'storage/oauth-private.key', 'storage/oauth-private.key',
'storage/oauth-public.key', 'storage/oauth-public.key'
]; ];
$public_dirs = [ $public_dirs = [
'public/uploads/accessories',
// 'public/uploads/assetmodels' => 'public/uploads/models', //according to git, this was _never_ a thing... (see below)
'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
'public/uploads/categories',
'public/uploads/companies', 'public/uploads/companies',
'public/uploads/components', '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/consumables',
'public/uploads/departments', 'public/uploads/departments',
'public/uploads/locations', 'public/uploads/avatars',
'public/uploads/manufacturers',
'public/uploads/models', // ...it's been this way for 9 years (as of late 2025)
'public/uploads/suppliers', '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 = [ $public_files = [
@@ -285,178 +117,106 @@ class RestoreFromBackup extends Command
'public/uploads/setting-label_logo*', 'public/uploads/setting-label_logo*',
'public/uploads/setting-logo*', 'public/uploads/setting-logo*',
'public/uploads/favicon.*', 'public/uploads/favicon.*',
'public/uploads/favicon-uploaded.*', 'public/uploads/favicon-uploaded.*'
]; ];
$all_files = $private_dirs + $public_dirs;
$sqlfiles = []; $sqlfiles = [];
$sqlfile_indices = []; $sqlfile_indices = [];
$interesting_files = []; $interesting_files = [];
$boring_files = []; $boring_files = [];
$unsafe_files = [];
$good_extensions = config('filesystems.allowed_upload_extensions_array'); for ($i=0; $i<$za->numFiles;$i++) {
$private_extensions = array_merge($good_extensions, ["csv", "key"]); //add csv, and 'key'
$public_extensions = array_diff($good_extensions, ["xml"]); //remove xml
$sanitizer = new Sanitizer();
/**
* TODO: I _hate_ the "continue 3" thing we keep doing here
* I think a better approach might be to have the "each file" stuff be in a method on this class, and the
* boring_files and interesting_files be properties on it that we fill out. Then, in that method, we could
* just do a 'return' once the file is actually handled (yay or nay). We could also start to break out some of
* the _other_ things that we do into their own methods too? But I don't care about that as much.
*/
for ($i = 0; $i < $za->numFiles; $i++) {
$stat_results = $za->statIndex($i); $stat_results = $za->statIndex($i);
// echo "index: $i\n"; // echo "index: $i\n";
// print_r($stat_results); // print_r($stat_results);
$raw_path = $stat_results['name']; $raw_path = $stat_results['name'];
if (strpos($raw_path, '\\') !== false) { //found a backslash, swap it to forward-slash if(strpos($raw_path,'\\')!==false) { //found a backslash, swap it to forward-slash
$raw_path = strtr($raw_path, '\\', '/'); $raw_path = strtr($raw_path,'\\','/');
//print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n"; //print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
} }
// skip macOS resource fork files (?!?!?!) // skip macOS resource fork files (?!?!?!)
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) { if(strpos($raw_path,"__MACOSX")!==false && strpos($raw_path,"._") !== false) {
//print "SKIPPING macOS Resource fork file: $raw_path\n"; //print "SKIPPING macOS Resource fork file: $raw_path\n";
// $boring_files[] = $raw_path; //stop adding this to the boring files list; it's just confusing $boring_files[] = $raw_path;
continue; continue;
} }
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') { if(@pathinfo($raw_path)['extension'] == "sql") {
Log::debug("Found a sql file!"); print "Found a sql file!\n";
$sqlfiles[] = $raw_path; $sqlfiles[] = $raw_path;
$sqlfile_indices[] = $i; $sqlfile_indices[] = $i;
continue; continue;
} }
if ($raw_path[-1] == '/') {
//last character is '/' - this is a directory, and we don't need it, and we don't need to warn about it
continue;
}
if (in_array(basename($raw_path), [".gitkeep", ".gitignore", ".DS_Store"])) {
//skip these boring files silently without reporting on them; they're stupid
continue;
}
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
foreach (['public' => $public_dirs, 'private' => $private_dirs] as $purpose => $dirs) { foreach(array_merge($private_dirs,$public_dirs) as $dir) {
$allowed_extensions = match ($purpose) { $last_pos = strrpos($raw_path,$dir.'/');
'public' => $public_extensions, if($last_pos !== false ) {
'private' => $private_extensions, //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?
foreach ($dirs as $dir => $destdir) { $interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
if (is_int($dir)) { continue 2;
$dir = $destdir; if($last_pos + strlen($dir) +1 == strlen($raw_path)) {
} // we don't care about that; we just want files with the appropriate prefix
$last_pos = strrpos($raw_path, $dir . '/'); //print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
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?
//the CSV bit, below, is because we store CSV files as "blahcsv" - without an extension
if (!in_array($extension, $allowed_extensions) && !($dir == "storage/private_uploads/imports" && substr($raw_path, -3) == "csv" && $extension == "")) {
$unsafe_files[] = $raw_path;
Log::debug($raw_path . ' from directory ' . $dir . ' is being skipped');
} else {
if ($dir != $destdir) {
Log::debug("Getting ready to save file $raw_path to new directory $destdir");
}
$interesting_files[$raw_path] = ['dest' => $destdir, 'index' => $i];
}
continue 3;
} }
} }
} }
$good_extensions = ["png","gif","jpg","svg","jpeg","doc","docx","pdf","txt",
foreach (['public' => $public_files, 'private' => $private_files] as $purpose => $files) { "zip","rar","xls","xlsx","lic","xml","rtf", "webp","key","ico"];
$allowed_extensions = match ($purpose) { foreach(array_merge($private_files, $public_files) as $file) {
'public' => $public_extensions, $has_wildcard = (strpos($file,"*") !== false);
'private' => $private_extensions, if($has_wildcard) {
}; $file = substr($file,0,-1); //trim last character (which should be the wildcard)
foreach ($files as $file) { }
$has_wildcard = (strpos($file, '*') !== false); $last_pos = strrpos($raw_path,$file); // no trailing slash!
if ($has_wildcard) { if($last_pos !== false ) {
$file = substr($file, 0, -1); //trim last character (which should be the wildcard) $extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if(!in_array($extension, $good_extensions)) {
$this->warn("Potentially unsafe file ".$raw_path." is being skipped");
$boring_files[] = $raw_path;
continue 2;
} }
$last_pos = strrpos($raw_path, $file); // no trailing slash! //print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
if ($last_pos !== false) { //no wildcards found in $file, process 'normally'
if (!in_array($extension, $allowed_extensions)) { if($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
// gathering potentially unsafe files here to return at exit // print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
$unsafe_files[] = $raw_path; $interesting_files[$raw_path] = ['dest' => dirname($file),'index' => $i];
Log::debug('Potentially unsafe file ' . $raw_path . ' is being skipped'); continue 2;
$boring_files[] = $raw_path;
continue 3;
}
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//no wildcards found in $file, process 'normally'
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
continue 3;
}
} }
} }
} }
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file $boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop } // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1); // print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) { if( count($sqlfiles) != 1) {
return $this->error('There should be exactly *one* sql backup file found, found: '.(count($sqlfiles) == 0 ? 'None' : implode(', ', $sqlfiles))); return $this->error("There should be exactly *one* sql backup file found, found: ".( count($sqlfiles) == 0 ? "None" : implode(", ",$sqlfiles)));
} }
if (strpos($sqlfiles[0], 'db-dumps') === false) { if( strpos($sqlfiles[0], "db-dumps") === false ) {
//return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]); //return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]);
//older Snipe-IT installs don't have the db-dumps subdirectory component //older Snipe-IT installs don't have the db-dumps subdirectory component
} }
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitize your SQL.");
}
// If we're doing --sql-stdout-only, handle that now so we don't have to open pipes to mysql and all of that silliness
if ($this->option('sql-stdout-only')) {
$sql_importer = new SQLStreamer($sql_contents, STDOUT, $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
return $this->warn("$bytes_read total bytes read");
//TODO - it'd be nice to dump this message to STDERR so that STDOUT is just pure SQL,
// which would be good for redirecting to a file, and not having to trim the last line off of it
}
//how to invoke the restore? //how to invoke the restore?
$pipes = []; $pipes = [];
$env_vars = getenv(); $env_vars = getenv();
$env_vars['MYSQL_PWD'] = config('database.connections.mysql.password'); $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) $proc_results = proc_open("mysql -h ".escapeshellarg(config('database.connections.mysql.host'))." -u ".escapeshellarg(config('database.connections.mysql.username'))." ".escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
// 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? [0 => ['pipe','r'],1 => ['pipe','w'],2 => ['pipe','w']],
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? ".exe" : "");
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");
}
$proc_results = proc_open("$mysql_binary -h ".escapeshellarg(config('database.connections.mysql.host')).' -u '.escapeshellarg(config('database.connections.mysql.username')).' '.escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes, $pipes,
null, null,
$env_vars); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them. $env_vars); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them.
if ($proc_results === false) { if($proc_results === false) {
return $this->error('Unable to invoke mysql via CLI'); return $this->error("Unable to invoke mysql via CLI");
} }
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[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("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. // $this->info("Stderr says? ".fgets($pipes[2])); //FIXME: ditto, same.
// should we read stdout? // should we read stdout?
@@ -464,50 +224,28 @@ class RestoreFromBackup extends Command
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy! //$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
// FIXME - this feels like it wants to go somewhere else? $sql_stat = $za->statIndex($sqlfile_indices[0]);
// and it doesn't seem 'right' - if you can't get a stream to the .sql file, //$this->info("SQL Stat is: ".print_r($sql_stat,true));
// why do we care what's happening with pipes and stdout and stderr?! $sql_contents = $za->getStream($sql_stat['name']);
if ($sql_contents === false) { if ($sql_contents === false) {
$stdout = fgets($pipes[1]); $stdout = fgets($pipes[1]);
$this->info($stdout); $this->info($stdout);
$stderr = fgets($pipes[2]); $stderr = fgets($pipes[2]);
$this->info($stderr); $this->info($stderr);
return false; return false;
} }
try { while(($buffer = fgets($sql_contents)) !== false ) {
if ( $this->option('sanitize-with-prefix') === null) { //$this->info("Buffer is: '$buffer'");
// "Legacy" direct-piping $bytes_written = fwrite($pipes[0],$buffer);
$bytes_read = 0; if($bytes_written === false) {
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) { $stdout = fgets($pipes[1]);
$bytes_read += strlen($buffer); $this->info($stdout);
// Log::debug("Buffer is: '$buffer'"); $stderr = fgets($pipes[2]);
$bytes_written = fwrite($pipes[0], $buffer); $this->info($stderr);
return false;
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();
} }
} 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!");
}
fclose($pipes[0]); fclose($pipes[0]);
fclose($sql_contents); fclose($sql_contents);
@@ -519,55 +257,41 @@ class RestoreFromBackup extends Command
//wait, have to do fclose() on all pipes first? //wait, have to do fclose() on all pipes first?
$close_results = proc_close($proc_results); $close_results = proc_close($proc_results);
if ($close_results != 0) { if($close_results != 0) {
return $this->error('There may have been a problem with the database import: Error number '.$close_results); return $this->error("There may have been a problem with the database import: Error number ".$close_results);
} }
//and now copy the files over too (right?) //and now copy the files over too (right?)
//FIXME - we don't prune the filesystem space yet!!!! //FIXME - we don't prune the filesystem space yet!!!!
if ($this->option('no-progress')) { if($this->option('no-progress')) {
$bar = null; $bar = null;
} else { } else {
$bar = $this->output->createProgressBar(count($interesting_files)); $bar = $this->output->createProgressBar(count($interesting_files));
} }
foreach ($interesting_files as $pretty_file_name => $file_details) { foreach($interesting_files AS $pretty_file_name => $file_details) {
$ugly_file_name = $za->statIndex($file_details['index'])['name']; $ugly_file_name = $za->statIndex($file_details['index'])['name'];
$migrated_file_name = $file_details['dest'] . '/' . basename($pretty_file_name); $fp = $za->getStream($ugly_file_name);
if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) { //$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$svg_contents = $za->getFromIndex($file_details['index']); $migrated_file = fopen($file_details['dest']."/".basename($pretty_file_name),"w");
$cleaned_svg = $sanitizer->sanitize($svg_contents); while(($buffer = fgets($fp))!== false) {
file_put_contents($migrated_file_name, $cleaned_svg); fwrite($migrated_file,$buffer);
} else {
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
if (!is_dir($file_details['dest'])) {
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
}
$migrated_file = fopen($migrated_file_name, 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
} }
if ($bar) { fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
if($bar) {
$bar->advance(); $bar->advance();
} }
} }
if ($bar) { if($bar) {
$bar->finish(); $bar->finish();
$this->line(''); $this->line("");
} else { } else {
$this->info(count($interesting_files).' files were succesfully transferred'); $this->info(count($interesting_files)." files were succesfully transferred");
} }
if (count($unsafe_files) > 0) { foreach($boring_files as $boring_file) {
foreach ($unsafe_files as $unsafe_file) { $this->warn($boring_file." was skipped.");
$this->warn('Potentially unsafe file '.$unsafe_file.' was skipped');
}
}
foreach ($boring_files as $boring_file) {
$this->warn($boring_file.' was skipped.');
} }
} }
} }

View File

@@ -2,13 +2,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException; use Artisan;
use Illuminate\Encryption\Encrypter; use App\Models\CustomField;
use App\Models\Asset;
use App\Models\Setting;
use \Illuminate\Encryption\Encrypter;
class RotateAppKey extends Command class RotateAppKey extends Command
{ {
@@ -17,17 +16,14 @@ class RotateAppKey extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'snipeit:rotate-key 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}';
/** /**
* The console command description. * The console command description.
* *
* @var string * @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. * Create a new command instance.
@@ -46,42 +42,28 @@ class RotateAppKey extends Command
*/ */
public function handle() public function handle()
{ {
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both. 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? ")) {
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)) {
// Get the existing app_key and ciphers // Get the existing app_key and ciphers
// We put them in a variable since we clear the cache partway through here. // We put them in a variable since we clear the cache partway through here.
if ($this->option('emergency')) { $old_app_key = config('app.key');
$old_app_key = config('app.key'); $cipher = config('app.cipher');
$cipher = config('app.cipher');
// Generate a new one // Generate a new one
Artisan::call('key:generate', ['--show' => true]); Artisan::call('key:generate', ['--show' => true]);
$new_app_key = trim(Artisan::output()); $new_app_key = Artisan::output();
// Clear the config cache // Clear the config cache
Artisan::call('config:clear'); Artisan::call('config:clear');
// Write the new app key to the .env file $this->warn('Your app cipher is: '.$cipher);
$this->writeNewEnvironmentFileWith($new_app_key); $this->warn('Your old APP_KEY is: '.$old_app_key);
} elseif ($this->argument('previous_key')) { $this->warn('Your new APP_KEY is: '.$new_app_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); // Write the new app key to the .env file
$this->warn('Your old APP_KEY is: ' . $old_app_key); $this->writeNewEnvironmentFileWith($new_app_key);
$this->warn('Your new APP_KEY is: ' . $new_app_key);
// Manually create an old encrypter instance using the old 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 // and also create a new encrypter instance so we can re-crypt the field
@@ -91,38 +73,33 @@ class RotateAppKey extends Command
$fields = CustomField::where('field_encrypted', '1')->get(); $fields = CustomField::where('field_encrypted', '1')->get();
foreach ($fields as $field) { foreach ($fields as $field) {
$assets = Asset::whereNotNull($field->db_column)->get(); $assets = Asset::whereNotNull($field->db_column)->get();
foreach ($assets as $asset) { foreach ($assets as $asset) {
try {
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column}); $asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
$this->line('DECRYPTED: ' . $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} = $newEncrypter->encrypt($asset->{$field->db_column}); $asset->{$field->db_column} = $newEncrypter->encrypt($asset->{$field->db_column});
$this->line('ENCRYPTED: '.$field->db_column); $this->line('ENCRYPTED: '.$field->db_column);
$asset->save(); $asset->save();
} }
} }
// Handle the LDAP password if one is provided // Handle the LDAP password if one is provided
$setting = Setting::first(); $setting = Setting::first();
if ($setting->ldap_pword != '') { if ($setting->ldap_pword!='') {
try { $setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword); $setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword); $setting->save();
$setting->save(); $this->warn('LDAP password has been re-encrypted.');
$this->warn('LDAP password has been re-encrypted.');
} catch(DecryptException $e) {
$this->warn("Unable to decrypt old LDAP password; skipping");
}
} }
} else { } else {
$this->info('This operation has been canceled. No changes have been made.'); $this->info('This operation has been canceled. No changes have been made.');
} }
@@ -136,9 +113,10 @@ class RotateAppKey extends Command
*/ */
protected function writeNewEnvironmentFileWith($key) protected function writeNewEnvironmentFileWith($key)
{ {
file_put_contents($this->laravel->environmentFilePath(), preg_replace( file_put_contents($this->laravel->environmentFilePath(), preg_replace(
$this->keyReplacementPattern(), $this->keyReplacementPattern(),
'APP_KEY="'.$key.'"', 'APP_KEY='.$key,
file_get_contents($this->laravel->environmentFilePath()) file_get_contents($this->laravel->environmentFilePath())
)); ));
} }
@@ -150,8 +128,8 @@ class RotateAppKey extends Command
*/ */
protected function keyReplacementPattern() protected function keyReplacementPattern()
{ {
$escaped = '="?'.preg_quote($this->laravel['config']['app.key'], '/').'"?'; $escaped = preg_quote('='.$this->laravel['config']['app.key'], '/');
return "/^APP_KEY{$escaped}/m"; 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

@@ -1,127 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Mail\UnacceptedAssetReminderMail;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CurrentInventory;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\Mail;
class SendAcceptanceReminder extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:acceptance-reminder';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This will resend users with unaccepted items a reminder to accept or decline them.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$pending = CheckoutAcceptance::query()
->with([
'checkoutable' => function (MorphTo $morph) {
$morph->morphWith([
Asset::class => ['model.category', 'assignedTo', 'adminuser', 'company', 'checkouts'],
Accessory::class => ['category', 'company', 'checkouts'],
LicenseSeat::class => ['user', 'license', 'checkouts'],
Component::class => ['assignedTo', 'company', 'checkouts'],
Consumable::class => ['company', 'checkouts'],
]);
},
'assignedTo',
])
->whereHasMorph(
'checkoutable',
[Asset::class, Accessory::class, LicenseSeat::class, Component::class, Consumable::class],
fn ($q) => $q->whereNull('accepted_at')
->whereNull('declined_at')
)
->pending()
->get();
$count = 0;
$unacceptedAssetGroups = $pending
->map(function($acceptance) {
return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance];
})
->groupBy(function($item) {
return $item['acceptance']->assignedTo ? $item['acceptance']->assignedTo->id : '';
});
$no_email_list= [];
foreach($unacceptedAssetGroups as $unacceptedAssetGroup) {
// The [0] is weird, but it allows for the item_count to work and grabs the appropriate info for each user.
// Collapsing and flattening the collection doesn't work above.
$acceptance = $unacceptedAssetGroup[0]['acceptance'];
$locale = $acceptance->assignedTo?->locale;
$email = $acceptance->assignedTo?->email;
if(!$email){
$no_email_list[] = [
'id' => $acceptance->assignedTo?->id,
'name' => $acceptance->assignedTo?->display_name,
];
} else {
$count++;
}
$item_count = $unacceptedAssetGroup->count();
if ($locale && $email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count))->locale($locale));
} elseif ($email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count)));
}
}
$this->info($count.' users notified.');
$headers = ['ID', 'Name'];
$rows = [];
foreach ($no_email_list as $user) {
$rows[] = [$user['id'], $user['name']];
}
if (!empty($rows)) {
$this->info("The following users do not have an email address:");
$this->table($headers, $rows);
}
return 0;
}
}

View File

@@ -39,16 +39,22 @@ class SendCurrentInventoryToUsers extends Command
*/ */
public function handle() public function handle()
{ {
$users = User::whereNull('deleted_at')->whereNotNull('email')->with('assets', 'accessories', 'licenses')->get(); $users = User::whereNull('deleted_at')->whereNotNull('email')->with('assets', 'accessories', 'licenses')->get();
$count = 0; $count = 0;
foreach ($users as $user) { 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++; $count++;
$user->notify((new CurrentInventory($user))); $user->notify((new CurrentInventory($user)));
} }
} }
$this->info($count.' users notified.'); $this->info($count.' users notified.');
} }
} }

View File

@@ -3,12 +3,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting; use App\Models\Setting;
use App\Notifications\ExpectedCheckinAdminNotification; use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification; use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Recipients\AlertRecipient;
class SendExpectedCheckinAlerts extends Command class SendExpectedCheckinAlerts extends Command
{ {
@@ -41,32 +41,25 @@ class SendExpectedCheckinAlerts extends Command
*/ */
public function handle() public function handle()
{ {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0; $whenNotify = Carbon::now()->addDays(7);
$today = Carbon::now(); $assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get();
$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');
$this->info($whenNotify . ' is deadline');
$this->info($assets->count() . ' assets');
foreach ($assets as $asset) { foreach ($assets as $asset) {
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) { if ($asset->assigned && $asset->checkedOutToUser()) {
$this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email); $asset->assigned->notify((new ExpectedCheckinNotification($asset)));
$asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
} }
} }
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) { if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate // 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); return new AlertRecipient($item);
}); });
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets)); \Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
} }
} }
} }

View File

@@ -2,14 +2,13 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Mail\ExpiringAssetsMail;
use App\Mail\ExpiringLicenseMail;
use App\Models\Asset; use App\Models\Asset;
use App\Models\License; use App\Models\License;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting; use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\ExpiringLicenseNotification;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendExpirationAlerts extends Command class SendExpirationAlerts extends Command
{ {
@@ -42,84 +41,29 @@ class SendExpirationAlerts extends Command
*/ */
public function handle() public function handle()
{ {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$alert_interval = $settings->alert_interval; $threshold = $settings->alert_interval;
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) { if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
// Send a rollup to the admin, if settings dictate // Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email)) $recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
->map(fn($item) => trim($item)) // Trim each email return new AlertRecipient($item);
->filter(fn($item) => !empty($item)) });
->all();
// Expiring Assets // Expiring Assets
$assets = Asset::getExpiringWarrantyOrEol($alert_interval); $assets = Asset::getExpiringWarrantee($threshold);
if ($assets->count() > 0) { if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $threshold]));
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval)); \Notification::send($recipients, new ExpiringAssetsNotification($assets, $threshold));
$this->table(
[
trans('general.id'),
trans('admin/hardware/form.tag'),
trans('admin/hardware/form.model'),
trans('general.model_no'),
trans('general.purchase_date'),
trans('admin/hardware/form.eol_rate'),
trans('admin/hardware/form.eol_date'),
trans('admin/hardware/form.warranty_expires'),
],
$assets->map(fn($item) =>
[
trans('general.id') => $item->id,
trans('admin/hardware/form.tag') => $item->asset_tag,
trans('admin/hardware/form.model') => $item->model->name,
trans('general.model_no') => $item->model->model_number,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/hardware/form.eol_rate') => $item->model->eol,
trans('admin/hardware/form.eol_date') => $item->eol_date ? $item->eol_formatted_date .' ('.$item->eol_diff_for_humans.')' : '',
trans('admin/hardware/form.warranty_expires') => $item->warranty_expires ? $item->warranty_expires_formatted_date .' ('.$item->warranty_expires_diff_for_humans.')' : '',
])
);
} }
// Expiring licenses // Expiring licenses
$licenses = License::query()->ExpiringLicenses($alert_interval) $licenses = License::getExpiringLicenses($threshold);
->with('manufacturer','category')
->orderBy('expiration_date', 'ASC')
->orderBy('termination_date', 'ASC')
->get();
if ($licenses->count() > 0) { if ($licenses->count() > 0) {
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval)); $this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $threshold]));
\Notification::send($recipients, new ExpiringLicenseNotification($licenses, $threshold));
$this->table(
[
trans('general.id'),
trans('general.name'),
trans('general.purchase_date'),
trans('admin/licenses/form.expiration'),
trans('mail.expires'),
trans('admin/licenses/form.termination_date'),
trans('mail.terminates')],
$licenses->map(fn($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->name,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/licenses/form.expiration') => $item->expires_formatted_date,
trans('mail.expires') => $item->expires_formatted_date ? $item->expires_diff_for_humans : '',
trans('admin/licenses/form.termination_date') => $item->terminates_formatted_date,
trans('mail.terminates') => $item->terminates_diff_for_humans
])
);
} }
// Send a message even if the count is 0
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
} else { } else {
if ($settings->alert_email == '') { if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings'); $this->error('Could not send email. No alert email configured in settings');

View File

@@ -52,9 +52,7 @@ class SendInventoryAlerts extends Command
return new AlertRecipient($item); return new AlertRecipient($item);
}); });
Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold)); \Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
} else {
$this->info('No low inventory items found. No mail sent.');
} }
} else { } else {
if ($settings->alert_email == '') { if ($settings->alert_email == '') {

View File

@@ -2,12 +2,15 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Mail\SendUpcomingAuditMail;
use App\Models\Asset; use App\Models\Asset;
use App\Models\License;
use App\Models\Setting; use App\Models\Setting;
use Carbon\Carbon; use App\Notifications\ExpiringAssetsNotification;
use App\Models\Recipients;
use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail; use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
class SendUpcomingAuditReport extends Command class SendUpcomingAuditReport extends Command
{ {
@@ -16,7 +19,7 @@ class SendUpcomingAuditReport extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'snipeit:upcoming-audits {--with-output : Display the results in a table in your console in addition to sending the email}'; protected $signature = 'snipeit:upcoming-audits';
/** /**
* The console command description. * The console command description.
@@ -43,73 +46,46 @@ class SendUpcomingAuditReport extends Command
public function handle() public function handle()
{ {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier'); if (($settings->alert_email != '') && ($settings->audit_warning_days) && ($settings->alerts_enabled == 1)) {
$asset_count = $assets_query->count();
$this->info(number_format($asset_count) . ' assets must be audited on or before ' . $interval_date);
if (!$this->option('with-output')) {
$this->info('Run this command with the --with-output option to see the full list in the console.');
}
if ($asset_count > 0) {
$assets_for_email = $assets_query->limit(30)->get();
// Send a rollup to the admin, if settings dictate // Send a rollup to the admin, if settings dictate
if ($settings->alert_email != '') { $recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new \App\Models\Recipients\AlertRecipient($item);
});
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item))
->filter(fn($item) => !empty($item))
->all();
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets_for_email, $settings->audit_warning_days, $asset_count)); // Assets due for auditing
$this->info('Audit notification sent to: ' . $settings->alert_email);
$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 { } else {
$this->info('There is no admin alert email set so no email will be sent.'); $this->info('No assets to be audited. No report sent.');
} }
if ($this->option('with-output')) { } elseif ($settings->alert_email=='') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (!$settings->audit_warning_days) {
// Get the full list if the user wants output in the console $this->error('No audit warning days set in Admin Notifications. No mail will be sent.');
$assets_for_output = $assets_query->limit(null)->get(); } elseif ($settings->alerts_enabled!=1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
$this->table(
[
trans('general.id'),
trans('general.name'),
trans('general.last_audit'),
trans('general.next_audit_date'),
trans('mail.Days'),
trans('mail.supplier'),
trans('mail.assigned_to'),
],
$assets_for_output->map(fn($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->display_name,
trans('general.last_audit') => $item->last_audit_formatted_date,
trans('general.next_audit_date') => $item->next_audit_formatted_date,
trans('mail.Days') => round($item->next_audit_diff_in_days),
trans('mail.supplier') => $item->supplier ? $item->supplier->name : '',
trans('mail.assigned_to') => $item->assignedTo ? $item->assignedTo->display_name : '',
])
);
}
} else { } else {
$this->info('There are no assets due for audit in the next ' . $interval . ' days.'); $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 App\Models\Asset;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SyncAssetCounters extends Command class SyncAssetCounters extends Command
{ {
@@ -40,38 +39,37 @@ class SyncAssetCounters extends Command
public function handle() public function handle()
{ {
$start = microtime(true); $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') $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; foreach ($output['info'] as $key => $output_text) {
$asset->checkout_counter = (int) $asset->checkouts_count; $this->info($output_text);
$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'); $time_elapsed_secs = microtime(true) - $start;
$this->info('Sync executed in ' . $time_elapsed_secs . ' seconds');
}
} else { } else {
$this->info('No assets to sync'); $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

@@ -38,6 +38,7 @@ class SyncAssetLocations extends Command
*/ */
public function handle() public function handle()
{ {
$output['info'] = []; $output['info'] = [];
$output['warn'] = []; $output['warn'] = [];
$output['error'] = []; $output['error'] = [];
@@ -50,89 +51,96 @@ class SyncAssetLocations extends Command
$output['info'][] = 'There are '.$rtd_assets->count().' unassigned assets.'; $output['info'][] = 'There are '.$rtd_assets->count().' unassigned assets.';
foreach ($rtd_assets as $rtd_asset) { foreach ($rtd_assets as $rtd_asset) {
$output['info'][] = 'Setting Unassigned Asset '.$rtd_asset->id.' ('.$rtd_asset->asset_tag.') to location: '.$rtd_asset->rtd_location_id.' because their default location is: '.$rtd_asset->rtd_location_id; $output['info'][] = 'Setting Unassigned Asset ' . $rtd_asset->id . ' ('.$rtd_asset->asset_tag.') to location: ' . $rtd_asset->rtd_location_id . " because their default location is: " . $rtd_asset->rtd_location_id;
$rtd_asset->location_id = $rtd_asset->rtd_location_id; $rtd_asset->location_id=$rtd_asset->rtd_location_id;
$rtd_asset->unsetEventDispatcher(); $rtd_asset->unsetEventDispatcher();
$rtd_asset->save(); $rtd_asset->save();
$bar->advance(); $bar->advance();
} }
$assigned_user_assets = Asset::where('assigned_type', \App\Models\User::class)->whereNotNull('assigned_to')->whereNull('deleted_at')->get(); $assigned_user_assets = Asset::where('assigned_type','App\Models\User')->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_user_assets->count().' assets checked out to users.'; $output['info'][] = 'There are '.$assigned_user_assets->count().' assets checked out to users.';
foreach ($assigned_user_assets as $assigned_user_asset) { foreach ($assigned_user_assets as $assigned_user_asset) {
if (($assigned_user_asset->assignedTo) && ($assigned_user_asset->assignedTo->userLoc)) { if (($assigned_user_asset->assignedTo) && ($assigned_user_asset->assignedTo->userLoc)) {
$new_location = $assigned_user_asset->assignedTo->userLoc->id; $new_location = $assigned_user_asset->assignedTo->userLoc->id;
$output['info'][] = 'Setting User Asset '.$assigned_user_asset->id.' ('.$assigned_user_asset->asset_tag.') to '.$assigned_user_asset->assignedTo->userLoc->name.' which is id: '.$new_location; $output['info'][] ='Setting User Asset ' . $assigned_user_asset->id . ' ('.$assigned_user_asset->asset_tag.') to ' . $assigned_user_asset->assignedTo->userLoc->name . ' which is id: ' . $new_location;
} else { } else {
$output['warn'][] = 'Asset '.$assigned_user_asset->id.' ('.$assigned_user_asset->asset_tag.') still has no location! '; $output['warn'][] ='Asset ' . $assigned_user_asset->id . ' ('.$assigned_user_asset->asset_tag.') still has no location! ';
$new_location = $assigned_user_asset->rtd_location_id; $new_location = $assigned_user_asset->rtd_location_id;
} }
$assigned_user_asset->location_id = $new_location; $assigned_user_asset->location_id=$new_location;
$assigned_user_asset->unsetEventDispatcher(); $assigned_user_asset->unsetEventDispatcher();
$assigned_user_asset->save(); $assigned_user_asset->save();
$bar->advance(); $bar->advance();
} }
$assigned_location_assets = Asset::where('assigned_type', \App\Models\Location::class) $assigned_location_assets = Asset::where('assigned_type','App\Models\Location')
->whereNotNull('assigned_to')->whereNull('deleted_at')->get(); ->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_location_assets->count().' assets checked out to locations.'; $output['info'][] = 'There are '.$assigned_location_assets->count().' assets checked out to locations.';
foreach ($assigned_location_assets as $assigned_location_asset) { foreach ($assigned_location_assets as $assigned_location_asset) {
if ($assigned_location_asset->assignedTo) { if ($assigned_location_asset->assignedTo) {
$assigned_location_asset->location_id = $assigned_location_asset->assignedTo->id; $assigned_location_asset->location_id = $assigned_location_asset->assignedTo->id;
$output['info'][] = 'Setting Location Assigned asset '.$assigned_location_asset->id.' ('.$assigned_location_asset->asset_tag.') that is checked out to '.$assigned_location_asset->assignedTo->name.' (#'.$assigned_location_asset->assignedTo->id.') to location: '.$assigned_location_asset->assetLoc()->id; $output['info'][] ='Setting Location Assigned asset ' . $assigned_location_asset->id . ' ('.$assigned_location_asset->asset_tag.') that is checked out to '.$assigned_location_asset->assignedTo->name.' (#'.$assigned_location_asset->assignedTo->id.') to location: ' . $assigned_location_asset->assetLoc()->id;
$assigned_location_asset->unsetEventDispatcher(); $assigned_location_asset->unsetEventDispatcher();
$assigned_location_asset->save(); $assigned_location_asset->save();
} else { } else {
$output['warn'][] = 'Asset '.$assigned_location_asset->id.' ('.$assigned_location_asset->asset_tag.') did not return a valid associated location - perhaps it was deleted?'; $output['warn'][] ='Asset ' . $assigned_location_asset->id . ' ('.$assigned_location_asset->asset_tag.') did not return a valid associated location - perhaps it was deleted?';
} }
$bar->advance(); $bar->advance();
} }
// Assigned to assets // Assigned to assets
$assigned_asset_assets = Asset::where('assigned_type', \App\Models\Asset::class) $assigned_asset_assets = Asset::where('assigned_type','App\Models\Asset')
->whereNotNull('assigned_to')->whereNull('deleted_at')->get(); ->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'Asset-assigned assets: '.$assigned_asset_assets->count(); $output['info'][] ='Asset-assigned assets: '.$assigned_asset_assets->count();
foreach ($assigned_asset_assets as $assigned_asset_asset) { foreach ($assigned_asset_assets as $assigned_asset_asset) {
// Check to make sure there aren't any invalid relationships // Check to make sure there aren't any invalid relationships
if ($assigned_asset_asset->assetLoc()) { if ($assigned_asset_asset->assetLoc()) {
$assigned_asset_asset->location_id = $assigned_asset_asset->assetLoc()->id; $assigned_asset_asset->location_id = $assigned_asset_asset->assetLoc()->id;
$output['info'][] = 'Setting Asset Assigned asset '.$assigned_asset_asset->assetLoc()->id.' ('.$assigned_asset_asset->asset_tag.') location to: '.$assigned_asset_asset->assetLoc()->id; $output['info'][] ='Setting Asset Assigned asset ' . $assigned_asset_asset->assetLoc()->id. ' ('.$assigned_asset_asset->asset_tag.') location to: ' . $assigned_asset_asset->assetLoc()->id;
$assigned_asset_asset->unsetEventDispatcher(); $assigned_asset_asset->unsetEventDispatcher();
$assigned_asset_asset->save(); $assigned_asset_asset->save();
} else { } else {
$output['warn'][] = 'Asset Assigned asset '.$assigned_asset_asset->id.' ('.$assigned_asset_asset->asset_tag.') does not seem to have a valid location'; $output['warn'][] ='Asset Assigned asset ' . $assigned_asset_asset->id. ' ('.$assigned_asset_asset->asset_tag.') does not seem to have a valid location';
}
$bar->advance();
} }
$bar->advance(); $unlocated_assets = Asset::whereNull("location_id")->whereNull('deleted_at')->get();
} $output['info'][] ='Assets still without a location: '.$unlocated_assets->count();
foreach($unlocated_assets as $unlocated_asset) {
$unlocated_assets = Asset::whereNull('location_id')->whereNull('deleted_at')->get(); $output['warn'][] ='Asset: '.$unlocated_asset->id.' still has no location. ';
$output['info'][] = 'Assets still without a location: '.$unlocated_assets->count();
foreach ($unlocated_assets as $unlocated_asset) {
$output['warn'][] = 'Asset: '.$unlocated_asset->id.' still has no location. ';
$bar->advance(); $bar->advance();
} }
$bar->finish(); $bar->finish();
$this->info("\n"); $this->info("\n");
if (($this->option('output') == 'all') || ($this->option('output') == 'info')) {
if (($this->option('output')=='all') || ($this->option('output')=='info')) {
foreach ($output['info'] as $key => $output_text) { foreach ($output['info'] as $key => $output_text) {
$this->info($output_text); $this->info($output_text);
} }
} }
if (($this->option('output') == 'all') || ($this->option('output') == 'warn')) { if (($this->option('output')=='all') || ($this->option('output')=='warn')) {
foreach ($output['warn'] as $key => $output_text) { foreach ($output['warn'] as $key => $output_text) {
$this->warn($output_text); $this->warn($output_text);
} }
} }
if (($this->option('output') == 'all') || ($this->option('output') == 'error')) { if (($this->option('output')=='all') || ($this->option('output')=='error')) {
foreach ($output['error'] as $key => $output_text) { foreach ($output['error'] as $key => $output_text) {
$this->error($output_text); $this->error($output_text);
} }
} }
} }
} }

View File

@@ -6,12 +6,13 @@ use Illuminate\Console\Command;
class SystemBackup extends Command class SystemBackup extends Command
{ {
/** /**
* The console command name. * The console command name.
* *
* @var string * @var string
*/ */
protected $signature = 'snipeit:backup {--filename=}'; protected $name = 'snipeit:backup';
/** /**
* The console command description. * The console command description.
@@ -37,20 +38,8 @@ class SystemBackup extends Command
*/ */
public function handle() public function handle()
{ {
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes //
$this->call('backup:run');
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');
}
} }
} }

View File

@@ -1,51 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Helper;
use Illuminate\Console\Command;
class TestLocationsFMCS extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:test-locations-fmcs {--location_id=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test for company ID inconsistencies if FullMultipleCompanySupport with scoped locations will be used.';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('This script checks for company ID inconsistencies if Full Multiple Company Support with scoped locations will be used.');
$this->info('This could take a few moments if have a very large dataset.');
$this->newLine();
// if parameter location_id is set, only test this location
$location_id = null;
if ($this->option('location_id')) {
$location_id = $this->option('location_id');
}
$mismatched = Helper::test_locations_fmcs(true, $location_id);
$this->warn(trans_choice('admin/settings/message.location_scoping.mismatch', count($mismatched)));
$this->newLine();
$this->info('Edit your locations to associate them with the correct company.');
$header = ['Type', 'ID', 'Name', 'Checkout Type', 'Company ID', 'Item Company', 'Item Location', 'Location Company', 'Location Company ID'];
sort($mismatched);
$this->table($header, $mismatched);
}
}

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

@@ -37,6 +37,7 @@ class Version extends Command
*/ */
public function handle() public function handle()
{ {
$use_branch = $this->option('branch'); $use_branch = $this->option('branch');
$use_type = $this->option('type'); $use_type = $this->option('type');
$git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD')); $git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
@@ -53,67 +54,73 @@ class Version extends Command
$this->line('Type is: '.$use_type); $this->line('Type is: '.$use_type);
$this->line('Current version is: '.$full_hash_version); $this->line('Current version is: '.$full_hash_version);
if (count($version) == 3) { if (count($version)==3) {
$this->line('This does not look like an alpha/beta release.'); $this->line('This does not look like an alpha/beta release.');
} else { } else {
if (array_key_exists('3', $version)) { if (array_key_exists('3',$version)) {
$this->line('The current version looks like a beta release.'); $this->line('The current version looks like a beta release.');
$prerelease_version = $version[1]; $prerelease_version = $version[1];
$hash_version = $version[3]; $hash_version = $version[3];
} }
} }
$app_version_raw = explode('.', $app_version); $app_version_raw = explode('.', $app_version);
$maj = str_replace('v', '', $app_version_raw[0]); $maj = str_replace('v', '', $app_version_raw[0]);
$min = $app_version_raw[1]; $min = $app_version_raw[1];
$patch = ''; $patch = '';
// This is a major release that might not have a third .0 // This is a major release that might not have a third .0
if (array_key_exists(2, $app_version_raw)) { if (array_key_exists(2, $app_version_raw)) {
$patch = $app_version_raw[2]; $patch = $app_version_raw[2];
} }
if ($use_type == 'major') { if ($use_type=='major') {
$app_version = 'v'.($maj + 1).".$min.$patch"; $app_version = "v".($maj + 1).".$min.$patch";
} elseif ($use_type == 'minor') { } elseif ($use_type=='minor') {
$app_version = 'v'."$maj.".($min + 1).".$patch"; $app_version = "v"."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'pre') { } elseif ($use_type=='pre') {
$pre_raw = str_replace('beta', '', $prerelease_version); $pre_raw = str_replace('beta','', $prerelease_version);
$pre_raw = str_replace('alpha', '', $pre_raw); $pre_raw = str_replace('alpha','', $pre_raw);
$pre_raw = str_ireplace('rc', '', $pre_raw); $pre_raw = str_ireplace('rc','', $pre_raw);
$pre_raw = $pre_raw++; $pre_raw = $pre_raw++;
$this->line('Setting the pre-release to '.$prerelease_version.'-'.$pre_raw); $this->line('Setting the pre-release to '. $prerelease_version.'-'.$pre_raw);
$app_version = 'v'."$maj.".($min + 1).".$patch"; $app_version = "v"."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'patch') { } elseif ($use_type=='patch') {
$app_version = 'v'."$maj.$min.".($patch + 1); $app_version = "v" . "$maj.$min." . ($patch + 1);
// If nothing is passed, leave the version as it is, just increment the build // If nothing is passed, leave the version as it is, just increment the build
} else { } else {
$app_version = 'v'."$maj.$min.".$patch; $app_version = "v" . "$maj.$min." . $patch;
} }
// Determine if this tag already exists, or if this prior to a release // Determine if this tag already exists, or if this prior to a release
$this->line('Running: git rev-parse master '.$current_app_version); $this->line('Running: git rev-parse master '.$current_app_version);
// $pre_release = trim(shell_exec('git rev-parse '.$use_branch.' '.$current_app_version.' 2>&1 1> /dev/null')); // $pre_release = trim(shell_exec('git rev-parse '.$use_branch.' '.$current_app_version.' 2>&1 1> /dev/null'));
if ($use_branch == 'develop') { if ($use_branch=='develop') {
$app_version = $app_version.'-pre'; $app_version = $app_version.'-pre';
} }
$full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version; $full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version;
$array = var_export( $array = var_export(
[ array(
'app_version' => $app_version, 'app_version' => $app_version,
'full_app_version' => $full_app_version, 'full_app_version' => $full_app_version,
'build_version' => $build_version, 'build_version' => $build_version,
'prerelease_version' => $prerelease_version, 'prerelease_version' => $prerelease_version,
'hash_version' => $hash_version, 'hash_version' => $hash_version,
'full_hash' => $full_hash_version, 'full_hash' => $full_hash_version,
'branch' => $git_branch, ], 'branch' => $git_branch),
true true
); );
// Construct our file content // Construct our file content
$content = <<<CON $content = <<<CON
<?php <?php
@@ -122,6 +129,7 @@ CON;
// And finally write the file and output the current version // And finally write the file and output the current version
\File::put($versionFile, $content); \File::put($versionFile, $content);
$this->info('Setting NEW version: '.$full_app_version.' ('.$git_branch.')'); $this->info('Setting NEW version: '. $full_app_version.' ('.$git_branch.')');
} }
} }

View File

@@ -5,12 +5,12 @@ namespace App\Console;
use App\Console\Commands\ImportLocations; use App\Console\Commands\ImportLocations;
use App\Console\Commands\ReEncodeCustomFieldNames; use App\Console\Commands\ReEncodeCustomFieldNames;
use App\Console\Commands\RestoreDeletedUsers; use App\Console\Commands\RestoreDeletedUsers;
use App\Models\Setting;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
* Define the application's command schedule. * Define the application's command schedule.
* *
@@ -19,16 +19,12 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
if(Setting::getSettings()?->alerts_enabled === 1) { $schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:inventory-alerts')->daily(); $schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily(); $schedule->command('snipeit:expected-checkin')->daily();
$schedule->command('snipeit:expected-checkin')->daily();
$schedule->command('snipeit:upcoming-audits')->daily();
}
$schedule->command('snipeit:backup')->weekly(); $schedule->command('snipeit:backup')->weekly();
$schedule->command('backup:clean')->daily(); $schedule->command('backup:clean')->daily();
$schedule->command('auth:clear-resets')->everyFifteenMinutes(); $schedule->command('snipeit:upcoming-audits')->daily();
$schedule->command('saml:clear_expired_nonces')->weekly();
} }
/** /**

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Enums;
enum ActionType: string
{
// General
case Create = 'create';
case Update = 'update';
case Delete = 'delete';
case Restore = 'restore';
// Assets/Accessories/Components/Licenses/Consumables
case Checkout = 'checkout';
case CheckinFrom = 'checkin from';
case Requested = 'requested';
case RequestCanceled = 'request canceled';
case Accepted = 'accepted';
case Declined = 'declined';
case Audit = 'audit';
case NoteAdded = 'note added';
// Users
case TwoFactorReset = '2FA reset';
case Merged = 'merged';
// Licenses
case DeleteSeats = 'delete seats';
case AddSeats = 'add seats';
// File Uploads
case Uploaded = 'uploaded';
case UploadDeleted = 'upload deleted';
}

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