develop reheading

This commit is contained in:
akemidx 2024-04-23 17:05:50 -04:00
commit ac4aa97103
5889 changed files with 132231 additions and 120037 deletions

View file

@ -1,11 +1,14 @@
{
"projectName": "snipe-it",
"projectOwner": "snipe",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
"CONTRIBUTORS.md"
],
"imageSize": 110,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "snipe",
@ -2997,6 +3000,24 @@
"contributions": [
"code"
]
},
{
"login": "coach1988",
"name": "coach1988",
"avatar_url": "https://avatars.githubusercontent.com/u/2565989?v=4",
"profile": "https://github.com/coach1988",
"contributions": [
"code"
]
},
{
"login": "mauro-miatello",
"name": "MrM",
"avatar_url": "https://avatars.githubusercontent.com/u/11910225?v=4",
"profile": "https://github.com/mauro-miatello",
"contributions": [
"code"
]
}
]
}

View file

@ -45,6 +45,7 @@ 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

View file

@ -36,6 +36,7 @@ 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

View file

@ -6,7 +6,7 @@ APP_DEBUG=false
APP_KEY=ChangeMe
APP_URL=null
APP_TIMEZONE='UTC'
APP_LOCALE=en
APP_LOCALE='en-US'
MAX_RESULTS=500
# --------------------------------------------
@ -42,6 +42,7 @@ 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
@ -86,6 +87,7 @@ COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=cookieStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS

View file

@ -6,7 +6,7 @@ APP_DEBUG=false
APP_KEY='base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU='
APP_URL='http://localhost:8000'
APP_TIMEZONE='US/Pacific'
APP_LOCALE=en
APP_LOCALE='en-US'
FILESYSTEM_DISK=local
# --------------------------------------------

View file

@ -6,7 +6,7 @@ APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
APP_LOCALE='en-US'
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS

View file

@ -30,10 +30,10 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View file

@ -36,7 +36,7 @@ jobs:
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.3.0
uses: codacy/codacy-analysis-cli-action@v4.4.0
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
@ -52,6 +52,6 @@ jobs:
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif

View file

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

73
.github/workflows/tests-mysql.yml vendored Normal file
View file

@ -0,0 +1,73 @@
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:
- "7.4"
- "8.0"
- "8.1.1"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: php artisan test --parallel

58
.github/workflows/tests-sqlite.yml vendored Normal file
View file

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

View file

@ -1,73 +0,0 @@
name: Tests
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:
- "7.4"
- "8.0"
- "8.1.1"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: php artisan test --parallel

View file

@ -0,0 +1,10 @@
{
"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": "7.4.0",
"php_max_major_minor": "8.1",
"php_max_wontwork": "8.2.0",
"current_snipeit_version": "6.3"
}

444
CONTRIBUTORS.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -66,6 +66,7 @@ class LdapSync extends Command
$ldap_result_dept = Setting::getSettings()->ldap_dept;
$ldap_result_manager = Setting::getSettings()->ldap_manager;
$ldap_default_group = Setting::getSettings()->ldap_default_group;
$search_base = Setting::getSettings()->ldap_base_dn;
try {
$ldapconn = Ldap::connectToLdap();
@ -83,26 +84,37 @@ class LdapSync extends Command
$summary = [];
try {
if ( $this->option('location_id') != '') {
/**
* 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');
$location_ou = Location::where('id', '=', $location_id)->value('ldap_ou');
$search_base = $location_ou;
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
}
}
else if ($this->option('base_dn') != '') {
/**
* if a manual base DN has been specified, use that. Allow the Base DN to override
* even if there's a location-based DN - if you picked it, you must have picked it for a reason.
*/
if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn');
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".');
} else {
$search_base = null;
}
/**
* If a filter has been specified, use that
*/
if ($this->option('filter') != '') {
$results = Ldap::findLdapUsers($search_base, -1, $this->option('filter'));
} else {
$results = Ldap::findLdapUsers($search_base);
}
} catch (\Exception $e) {
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
@ -115,14 +127,14 @@ class LdapSync extends Command
/* Determine which location to assign users to by default. */
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
if ($this->option('location') != '') {
if ($location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
if ($this->option('location') != '') {
if ($location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
} elseif ($this->option('location_id') != '') {
foreach($this->option('location_id') as $location_id) {
} elseif ($this->option('location_id')) {
foreach($this->option('location_id') as $location_id) {
if ($location = Location::where('id', '=', $location_id)->first()) {
Log::debug('Location ID ' . $location_id . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
@ -378,7 +390,7 @@ class LdapSync extends Command
$user->location_id = $location->id;
}
}
$location = null;
$user->ldap_import = 1;
$errors = '';

View file

@ -63,7 +63,7 @@ class ResetDemoSettings extends Command
$settings->date_display_format = 'D M d, Y';
$settings->time_display_format = 'g:iA';
$settings->thumbnail_max_h = '30';
$settings->locale = 'en';
$settings->locale = 'en-US';
$settings->version_footer = 'on';
$settings->support_footer = null;
$settings->saml_enabled = '0';
@ -78,7 +78,7 @@ class ResetDemoSettings extends Command
$settings->save();
if ($user = User::where('username', '=', 'admin')->first()) {
$user->locale = 'en';
$user->locale = 'en-US';
$user->save();
}

View file

@ -5,6 +5,151 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use ZipArchive;
class SQLStreamer {
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
private bool $reading_beginning_of_line = true;
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
if($this->statement_is_permitted && $line[0] === ' ') {
return $line;
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
// ^^^^^^ that bit should *exit* the 'perimitted' black
];
foreach($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line;
}
}
// all that is not allowed is denied.
return "";
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// \Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command
{
/**
@ -12,10 +157,13 @@ class RestoreFromBackup extends Command
*
* @var string
*/
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar}';
{--no-progress : Don\'t show a progress bar}
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
/**
* The console command description.
@ -34,8 +182,6 @@ class RestoreFromBackup extends Command
parent::__construct();
}
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
/**
* Execute the console command.
*
@ -55,7 +201,7 @@ class RestoreFromBackup extends Command
return $this->error('Missing required filename');
}
if (! $this->option('force') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
return $this->error('Data loss not confirmed');
}
@ -84,35 +230,36 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/assetmodels',
'storage/private_uploads/users',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
];
$private_files = [
'storage/oauth-private.key',
'storage/oauth-public.key',
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
'public/uploads/categories',
'public/uploads/manufacturers',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/consumables',
'public/uploads/departments',
'public/uploads/avatars',
'public/uploads/suppliers',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/locations',
'public/uploads/accessories',
'public/uploads/models',
'public/uploads/categories',
'public/uploads/avatars',
'public/uploads/manufacturers',
'public/uploads/models',
'public/uploads/suppliers',
];
$public_files = [
@ -157,11 +304,11 @@ class RestoreFromBackup extends Command
}
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
$last_pos = strrpos($raw_path, $dir.'/');
$last_pos = strrpos($raw_path, $dir . '/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
continue 2;
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
// we don't care about that; we just want files with the appropriate prefix
@ -170,7 +317,7 @@ class RestoreFromBackup extends Command
}
}
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', ];
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
@ -179,8 +326,8 @@ class RestoreFromBackup extends Command
$last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (! in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file '.$raw_path.' is being skipped');
if (!in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
@ -195,7 +342,6 @@ class RestoreFromBackup extends Command
}
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) {
@ -207,6 +353,17 @@ class RestoreFromBackup extends Command
//older Snipe-IT installs don't have the db-dumps subdirectory component
}
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
//how to invoke the restore?
$pipes = [];
@ -227,6 +384,7 @@ class RestoreFromBackup extends Command
return $this->error('Unable to invoke mysql via CLI');
}
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
@ -237,9 +395,9 @@ class RestoreFromBackup extends Command
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']);
// FIXME - this feels like it wants to go somewhere else?
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
// why do we care what's happening with pipes and stdout and stderr?!
if ($sql_contents === false) {
$stdout = fgets($pipes[1]);
$this->info($stdout);
@ -248,20 +406,27 @@ class RestoreFromBackup extends Command
return false;
}
$bytes_read = 0;
try {
while (($buffer = fgets($sql_contents, self::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
if ( $this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
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);
@ -270,7 +435,6 @@ class RestoreFromBackup extends Command
$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!");
}
@ -302,7 +466,7 @@ class RestoreFromBackup extends Command
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, self::$buffer_size)) !== false) {
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);

View file

@ -0,0 +1,44 @@
<?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

@ -9,6 +9,7 @@ use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SendExpectedCheckinAlerts extends Command
{
@ -42,7 +43,7 @@ class SendExpectedCheckinAlerts extends Command
public function handle()
{
$settings = Setting::getSettings();
$whenNotify = Carbon::now()->addDays(7);
$whenNotify = Carbon::now();
$assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get();
$this->info($whenNotify.' is deadline');
@ -50,6 +51,7 @@ class SendExpectedCheckinAlerts extends Command
foreach ($assets as $asset) {
if ($asset->assigned && $asset->checkedOutToUser()) {
Log::info('Sending ExpectedCheckinNotification to ' . $asset->assigned->email);
$asset->assigned->notify((new ExpectedCheckinNotification($asset)));
}
}

View file

@ -0,0 +1,76 @@
<?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

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

View file

@ -11,6 +11,7 @@ use App\Models\CustomFieldset;
use App\Models\Depreciation;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\License;
use Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Image;
@ -18,6 +19,63 @@ use Carbon\Carbon;
class Helper
{
/**
* This is only used for reversing the migration that updates the locale to the 5-6 letter codes from two
* letter codes. The normal dropdowns use the autoglossonyms in the language files located
* in resources/en-US/localizations.php.
*/
public static $language_map = [
'af' => 'af-ZA', // Afrikaans
'am' => 'am-ET', // Amharic
'ar' => 'ar-SA', // Arabic
'bg' => 'bg-BG', // Bulgarian
'ca' => 'ca-ES', // Catalan
'cs' => 'cs-CZ', // Czech
'cy' => 'cy-GB', // Welsh
'da' => 'da-DK', // Danish
'de-i' => 'de-if', // German informal
'de' => 'de-DE', // German
'el' => 'el-GR', // Greek
'en' => 'en-US', // English
'et' => 'et-EE', // Estonian
'fa' => 'fa-IR', // Persian
'fi' => 'fi-FI', // Finnish
'fil' => 'fil-PH', // Filipino
'fr' => 'fr-FR', // French
'he' => 'he-IL', // Hebrew
'hr' => 'hr-HR', // Croatian
'hu' => 'hu-HU', // Hungarian
'id' => 'id-ID', // Indonesian
'is' => 'is-IS', // Icelandic
'it' => 'it-IT', // Italian
'iu' => 'iu-NU', // Inuktitut
'ja' => 'ja-JP', // Japanese
'ko' => 'ko-KR', // Korean
'lt' => 'lt-LT', // Lithuanian
'lv' => 'lv-LV', // Latvian
'mi' => 'mi-NZ', // Maori
'mk' => 'mk-MK', // Macedonian
'mn' => 'mn-MN', // Mongolian
'ms' => 'ms-MY', // Malay
'nl' => 'nl-NL', // Dutch
'no' => 'no-NO', // Norwegian
'pl' => 'pl-PL', // Polish
'ro' => 'ro-RO', // Romanian
'ru' => 'ru-RU', // Russian
'sk' => 'sk-SK', // Slovak
'sl' => 'sl-SI', // Slovenian
'so' => 'so-SO', // Somali
'ta' => 'ta-IN', // Tamil
'th' => 'th-TH', // Thai
'tl' => 'tl-PH', // Tagalog
'tr' => 'tr-TR', // Turkish
'uk' => 'uk-UA', // Ukrainian
'vi' => 'vi-VN', // Vietnamese
'zu' => 'zu-ZA', // Zulu
];
/**
* Simple helper to invoke the markdown parser
*
@ -354,7 +412,7 @@ class Helper
if ($index >= $total_colors) {
\Log::error('Status label count is '.$index.' and exceeds the allowed count of 266.');
\Log::info('Status label count is '.$index.' and exceeds the allowed count of 266.');
//patch fix for array key overflow (color count starts at 1, array starts at 0)
$index = $index - $total_colors - 1;
@ -658,18 +716,19 @@ class Helper
*/
public static function checkLowInventory()
{
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get();
$components = Component::whereNotNull('min_amt')->get();
$asset_models = AssetModel::where('min_amt', '>', 0)->get();
$licenses = License::where('min_amt', '>', 0)->get();
$avail_consumables = 0;
$items_array = [];
$all_count = 0;
foreach ($consumables as $consumable) {
$avail = $consumable->numRemaining();
if ($avail < ($consumable->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($avail < ($consumable->min_amt) + $alert_threshold) {
if ($consumable->qty > 0) {
$percent = number_format((($avail / $consumable->qty) * 100), 0);
} else {
@ -688,7 +747,7 @@ class Helper
foreach ($accessories as $accessory) {
$avail = $accessory->qty - $accessory->users_count;
if ($avail < ($accessory->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($avail < ($accessory->min_amt) + $alert_threshold) {
if ($accessory->qty > 0) {
$percent = number_format((($avail / $accessory->qty) * 100), 0);
} else {
@ -707,7 +766,7 @@ class Helper
foreach ($components as $component) {
$avail = $component->numRemaining();
if ($avail < ($component->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($avail < ($component->min_amt) + $alert_threshold) {
if ($component->qty > 0) {
$percent = number_format((($avail / $component->qty) * 100), 0);
} else {
@ -730,7 +789,7 @@ class Helper
$total_owned = $asset->where('model_id', '=', $asset_model->id)->count();
$avail = $asset->where('model_id', '=', $asset_model->id)->whereNull('assigned_to')->count();
if ($avail < ($asset_model->min_amt)+ \App\Models\Setting::getSettings()->alert_threshold) {
if ($avail < ($asset_model->min_amt) + $alert_threshold) {
if ($avail > 0) {
$percent = number_format((($avail / $total_owned) * 100), 0);
} else {
@ -746,6 +805,26 @@ class Helper
}
}
foreach ($licenses as $license){
$avail = $license->remaincount();
if ($avail < ($license->min_amt) + $alert_threshold) {
if ($avail > 0) {
$percent = number_format((($avail / $license->min_amt) * 100), 0);
} else {
$percent = 100;
}
$items_array[$all_count]['id'] = $license->id;
$items_array[$all_count]['name'] = $license->name;
$items_array[$all_count]['type'] = 'licenses';
$items_array[$all_count]['percent'] = $percent;
$items_array[$all_count]['remaining'] = $avail;
$items_array[$all_count]['min_amt'] = $license->min_amt;
$all_count++;
}
}
return $items_array;
}
@ -763,7 +842,7 @@ class Helper
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif')) {
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
return $filetype;
}
@ -1027,6 +1106,8 @@ class Helper
'jpeg' => 'far fa-image',
'gif' => 'far fa-image',
'png' => 'far fa-image',
'webp' => 'far fa-image',
'avif' => 'far fa-image',
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
@ -1062,6 +1143,8 @@ class Helper
case 'jpeg':
case 'gif':
case 'png':
case 'webp':
case 'avif':
return true;
break;
default:
@ -1317,7 +1400,7 @@ class Helper
/*
* I know it's gauche to return a shitty HTML string, but this is just a helper and since it will be the same every single time,
* I know it's gauche to return a shitty HTML string, but this is just a helper and since it will be the same every single time,
* it seemed pretty safe to do here. Don't you judge me.
*/
public static function showDemoModeFieldWarning() {
@ -1325,4 +1408,55 @@ class Helper
return "<p class=\"text-warning\"><i class=\"fas fa-lock\"></i>" . trans('general.feature_disabled') . "</p>";
}
}
/**
* Ah, legacy code.
*
* This corrects the original mistakes from 2013 where we used the wrong locale codes. Hopefully we
* can get rid of this in a future version, but this should at least give us the belt and suspenders we need
* to be sure this change is not too disruptive.
*
* In this array, we ONLY include the older languages where we weren't using the correct locale codes.
*
* @see public static $language_map in this file
* @author A. Gianotto <snipe@snipe.net>
* @since 6.3.0
*
* @param $language_code
* @return string []
*/
public static function mapLegacyLocale($language_code = null)
{
if (strlen($language_code) > 4) {
return $language_code;
}
foreach (self::$language_map as $legacy => $new) {
if ($language_code == $legacy) {
\Log::debug('Current language is '.$legacy.', using '.$new.' instead');
return $new;
}
}
// Return US english if we don't have a match
return 'en-US';
}
public static function mapBackToLegacyLocale($new_locale = null)
{
if (strlen($new_locale) <= 4) {
return $new_locale; //"new locale" apparently wasn't quite so new
}
// This does a *reverse* search against our new language map array - given the value, find the *key* for it
$legacy_locale = array_search($new_locale, self::$language_map);
if($legacy_locale !== false) {
return $legacy_locale;
}
return $new_locale; // better that you have some weird locale that doesn't fit into our mappings anywhere than 'void'
}
}

View file

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

View file

@ -60,9 +60,10 @@ class AccessoryCheckinController extends Controller
$this->authorize('checkin', $accessory);
$checkin_at = date('Y-m-d');
$checkin_hours = date('H:i:s');
$checkin_at = date('Y-m-d H:i:s');
if ($request->filled('checkin_at')) {
$checkin_at = $request->input('checkin_at');
$checkin_at = $request->input('checkin_at').' '.$checkin_hours;
}
// Was the accessory updated?

View file

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

View file

@ -36,7 +36,8 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('view', Asset::class);
$maintenances = AssetMaintenance::select('asset_maintenances.*')->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'admin');
$maintenances = AssetMaintenance::select('asset_maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'admin');
if ($request->filled('search')) {
$maintenances = $maintenances->TextSearch($request->input('search'));
@ -47,7 +48,7 @@ class AssetMaintenancesController extends Controller
}
if ($request->filled('supplier_id')) {
$maintenances->where('supplier_id', '=', $request->input('supplier_id'));
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('asset_maintenance_type')) {
@ -70,10 +71,13 @@ class AssetMaintenancesController extends Controller
'notes',
'asset_tag',
'asset_name',
'serial',
'user_id',
'supplier',
'is_warranty',
'status_label',
];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
@ -90,6 +94,12 @@ class AssetMaintenancesController extends Controller
case 'asset_name':
$maintenances = $maintenances->OrderByAssetName($order);
break;
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
default:
$maintenances = $maintenances->orderBy($sort, $order);
break;
@ -116,41 +126,17 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
// create a new model instance
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(e($request->input('asset_id')));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot add a maintenance for that asset'));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->user_id = Auth::id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
$maintenance = new AssetMaintenance();
$maintenance->fill($request->all());
$maintenance->user_id = Auth::id();
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.create.success')));
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
@ -158,65 +144,39 @@ class AssetMaintenancesController extends Controller
* Validates and stores an update to an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $assetMaintenanceId
* @param int $id
* @param int $request
* @version v1.0
* @since [v4.0]
* @return string JSON
*/
public function update(Request $request, $assetMaintenanceId = null)
public function update(Request $request, $id)
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
}
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
$assetMaintenance->supplier_id = e($request->input('supplier_id'));
$assetMaintenance->is_warranty = e($request->input('is_warranty'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
if (($assetMaintenance->completion_date == null)
) {
if (($assetMaintenance->asset_maintenance_time !== 0)
|| (! is_null($assetMaintenance->asset_maintenance_time))
) {
$assetMaintenance->asset_maintenance_time = null;
// Can this user manage this asset?
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
}
// The asset this miantenance is attached to is not valid or has been deleted
if (!$maintenance->asset) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
}
$maintenance->fill($request->all());
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
}
/**

View file

@ -3,15 +3,20 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\DepreciationReportTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
@ -20,11 +25,12 @@ use App\Models\License;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Auth;
use \Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use DB;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Log;
use Input;
use Paginator;
use Slack;
@ -43,6 +49,8 @@ use Route;
*/
class AssetsController extends Controller
{
use MigratesLegacyAssetLocations;
/**
* Returns JSON listing of all assets
*
@ -86,6 +94,7 @@ class AssetsController extends Controller
'serial',
'model_number',
'last_checkout',
'last_checkin',
'notes',
'expected_checkin',
'order_number',
@ -103,6 +112,7 @@ class AssetsController extends Controller
'requests_counter',
'byod',
'asset_eol_date',
'requestable',
];
$filter = [];
@ -134,7 +144,7 @@ class AssetsController extends Controller
// Search custom fields by column name
foreach ($all_custom_fields as $field) {
if ($request->filled($field->db_column_name())) {
if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
}
}
@ -531,37 +541,14 @@ class AssetsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param \App\Http\Requests\ImageUploadRequest $request
* @since [v4.0]
* @return \Illuminate\Http\JsonResponse
*/
public function store(ImageUploadRequest $request)
public function store(StoreAssetRequest $request): JsonResponse
{
$this->authorize('create', Asset::class);
$asset = new Asset();
$asset->model()->associate(AssetModel::find((int) $request->get('model_id')));
$asset->name = $request->get('name');
$asset->serial = $request->get('serial');
$asset->company_id = Company::getIdForCurrentUser($request->get('company_id'));
$asset->model_id = $request->get('model_id');
$asset->order_number = $request->get('order_number');
$asset->notes = $request->get('notes');
$asset->asset_tag = $request->get('asset_tag', Asset::autoincrement_asset());
$asset->user_id = Auth::id();
$asset->archived = '0';
$asset->physical = '1';
$asset->depreciate = '0';
$asset->status_id = $request->get('status_id', 0);
$asset->warranty_months = $request->get('warranty_months', null);
$asset->purchase_cost = $request->get('purchase_cost');
$asset->asset_eol_date = $request->get('asset_eol_date', $asset->present()->eol_date());
$asset->purchase_date = $request->get('purchase_date', null);
$asset->assigned_to = $request->get('assigned_to', null);
$asset->supplier_id = $request->get('supplier_id');
$asset->requestable = $request->get('requestable', 0);
$asset->rtd_location_id = $request->get('rtd_location_id', null);
$asset->location_id = $request->get('rtd_location_id', null);
$asset->fill($request->validated());
$asset->user_id = Auth::id();
/**
* this is here just legacy reasons. Api\AssetController
@ -574,10 +561,11 @@ class AssetsController extends Controller
$asset = $request->handleImages($asset);
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
$model = AssetModel::find($request->input('model_id'));
if (($model) && ($model->fieldset)) {
// Check that it's an object and not a collection
// (Sometimes people send arrays here and they shouldn't
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
// Set the field value based on what was sent in the request
@ -585,25 +573,30 @@ class AssetsController extends Controller
// If input value is null, use custom field's default value
if ($field_val == null) {
\Log::debug('Field value for '.$field->db_column.' is null');
Log::debug('Field value for '.$field->db_column.' is null');
$field_val = $field->defaultValue($request->get('model_id'));
\Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id')));
Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id')));
}
// if the field is set to encrypted, make sure we encrypt the value
if ($field->field_encrypted == '1') {
\Log::debug('This model field is encrypted in this fieldset.');
Log::debug('This model field is encrypted in this fieldset.');
if (Gate::allows('admin')) {
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
$field_val = \Crypt::encrypt($field->defaultValue($request->get('model_id')));
$field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
} else {
$field_val = \Crypt::encrypt($request->input($field->db_column));
$field_val = Crypt::encrypt($request->input($field->db_column));
}
}
}
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
$asset->{$field->db_column} = $field_val;
@ -627,6 +620,8 @@ class AssetsController extends Controller
}
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@ -670,16 +665,26 @@ class AssetsController extends Controller
$model = AssetModel::find($asset->model_id);
// Update custom fields
$problems_updating_encrypted_custom_fields = false;
if (($model) && (isset($model->fieldset))) {
foreach ($model->fieldset->fields as $field) {
$field_val = $request->input($field->db_column, null);
if ($request->has($field->db_column)) {
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
$asset->{$field->db_column} = \Crypt::encrypt($request->input($field->db_column));
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
continue;
}
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
$asset->{$field->db_column} = $field_val;
}
}
}
@ -705,7 +710,11 @@ class AssetsController extends Controller
$asset->image = $asset->getImageUrl();
}
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@ -884,20 +893,21 @@ class AssetsController extends Controller
*/
public function checkin(Request $request, $asset_id)
{
$this->authorize('checkin', Asset::class);
$asset = Asset::findOrFail($asset_id);
$asset = Asset::with('model')->findOrFail($asset_id);
$this->authorize('checkin', $asset);
$target = $asset->assignedTo;
if (is_null($target)) {
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.already_checked_in')));
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag'=> e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.already_checked_in')));
}
$asset->expected_checkin = null;
$asset->last_checkout = null;
//$asset->last_checkout = null;
$asset->last_checkin = now();
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
@ -905,10 +915,16 @@ class AssetsController extends Controller
$asset->name = $request->input('name');
}
$this->migrateLegacyLocations($asset);
$asset->location_id = $asset->rtd_location_id;
if ($request->filled('location_id')) {
$asset->location_id = $request->input('location_id');
if ($request->input('update_default_location')){
$asset->rtd_location_id = $request->input('location_id');
}
}
if ($request->has('status_id')) {
@ -922,10 +938,31 @@ class AssetsController extends Controller
$originalValues['action_date'] = $checkin_at;
}
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
});
// Get all pending Acceptances for this asset and delete them
CheckoutAcceptance::pending()
->whereHasMorph(
'checkoutable',
[Asset::class],
function (Builder $query) use ($asset) {
$query->where('id', $asset->id);
})
->get()
->map(function ($acceptance) {
$acceptance->delete();
});
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.success')));
return response()->json(Helper::formatStandardApiResponse('success', [
'asset_tag'=> e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error')));
@ -1047,8 +1084,7 @@ class AssetsController extends Controller
$assets = Asset::select('assets.*')
->with('location', 'assetstatus', 'assetlog', 'company','assignedTo',
'model.category', 'model.manufacturer', 'model.fieldset', 'supplier', 'requests')
->requestableAssets();
'model.category', 'model.manufacturer', 'model.fieldset', 'supplier', 'requests');
@ -1056,7 +1092,7 @@ class AssetsController extends Controller
if ($request->filled('search')) {
$assets->TextSearch($request->input('search'));
}
// Search custom fields by column name
foreach ($all_custom_fields as $field) {
if ($request->filled($field->db_column_name())) {
@ -1086,6 +1122,7 @@ class AssetsController extends Controller
break;
}
$assets->requestableAssets();
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');

View file

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

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ConsumablesTransformer;
@ -11,6 +12,7 @@ use App\Models\Consumable;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Auth;
class ConsumablesController extends Controller
{
@ -290,17 +292,9 @@ class ConsumablesController extends Controller
]
);
// Log checkout event
$logaction = $consumable->logCheckout($request->input('note'), $user);
$data['log_id'] = $logaction->id;
$data['eula'] = $consumable->getEula();
$data['first_name'] = $user->first_name;
$data['item_name'] = $consumable->name;
$data['checkout_date'] = $logaction->created_at;
$data['note'] = $logaction->note;
$data['require_acceptance'] = $consumable->requireAcceptance();
event(new CheckoutableCheckedOut($consumable, $user, Auth::user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
}

View file

@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Transformers\GroupsTransformer;
use App\Models\Group;
use Illuminate\Http\Request;
use Auth;
class GroupsController extends Controller
@ -25,7 +26,7 @@ class GroupsController extends Controller
$this->authorize('view', Group::class);
$allowed_columns = ['id', 'name', 'created_at', 'users_count'];
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at')->withCount('users as users_count');
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at', 'created_by')->with('admin')->withCount('users as users_count');
if ($request->filled('search')) {
$groups = $groups->TextSearch($request->input('search'));
@ -63,6 +64,7 @@ class GroupsController extends Controller
$group = new Group;
$group->name = $request->input('name');
$group->created_by = Auth::user()->id;
$group->permissions = json_encode($request->input('permissions')); // Todo - some JSON validation stuff here
if ($group->save()) {

View file

@ -136,6 +136,7 @@ class LicensesController extends Controller
'seats',
'termination_date',
'depreciation_id',
'min_amt',
];
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$licenses = $licenses->orderBy($sort, $order);

View file

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

View file

@ -32,14 +32,34 @@ class ReportsController extends Controller
}
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
$actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
$actionlogs = $actionlogs->where(function($query) use ($request)
{
$query->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')))
->orWhere(function($query) use ($request)
{
$query->where('target_id', '=', $request->input('item_id'))
->where('target_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
});
});
}
if ($request->filled('action_type')) {
$actionlogs = $actionlogs->where('action_type', '=', $request->input('action_type'))->orderBy('created_at', 'desc');
}
if ($request->filled('user_id')) {
$actionlogs = $actionlogs->where('user_id', '=', $request->input('user_id'));
}
if ($request->filled('action_source')) {
$actionlogs = $actionlogs->where('action_source', '=', $request->input('action_source'))->orderBy('created_at', 'desc');
}
if ($request->filled('remote_ip')) {
$actionlogs = $actionlogs->where('remote_ip', '=', $request->input('remote_ip'))->orderBy('created_at', 'desc');
}
if ($request->filled('uploads')) {
$actionlogs = $actionlogs->whereNotNull('filename')->orderBy('created_at', 'desc');
}
@ -52,6 +72,9 @@ class ReportsController extends Controller
'accept_signature',
'action_type',
'note',
'remote_ip',
'user_agent',
'action_source',
];

View file

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

View file

@ -73,6 +73,7 @@ class UsersController extends Controller
'users.end_date',
'users.vip',
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',)
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count');
@ -122,6 +123,10 @@ class UsersController extends Controller
$users = $users->where('users.country', '=', $request->input('country'));
}
if ($request->filled('website')) {
$users = $users->where('users.website', '=', $request->input('website'));
}
if ($request->filled('zip')) {
$users = $users->where('users.zip', '=', $request->input('zip'));
}
@ -192,11 +197,6 @@ class UsersController extends Controller
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
$limit = app('api_limit_value');
switch ($request->input('sort')) {
case 'manager':
$users = $users->OrderManager($order);
@ -259,6 +259,7 @@ class UsersController extends Controller
'start_date',
'end_date',
'autoassign_licenses',
'website',
];
$sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name';
@ -272,8 +273,14 @@ class UsersController extends Controller
$users = $users->withTrashed();
}
// Apply companyable scope
$users = Company::scopeCompanyables($users);
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
$limit = app('api_limit_value');
$total = $users->count();
$users = $users->skip($offset)->take($limit)->get();
@ -353,6 +360,7 @@ class UsersController extends Controller
$user = new User;
$user->fill($request->all());
$user->created_by = Auth::user()->id;
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
@ -396,7 +404,10 @@ class UsersController extends Controller
public function show($id)
{
$this->authorize('view', User::class);
$user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count')->findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
return (new UsersTransformer)->transformUser($user);
}
@ -416,6 +427,8 @@ class UsersController extends Controller
$this->authorize('update', User::class);
$user = User::findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
/**
* This is a janky hack to prevent people from changing admin demo user data on the public demo.
@ -452,6 +465,7 @@ class UsersController extends Controller
if (! Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
}
$user->permissions = $permissions_array;
}
@ -474,6 +488,7 @@ class UsersController extends Controller
// Check if the request has groups passed and has a value
if ($request->filled('groups')) {
$validator = Validator::make($request->all(), [
'groups.*' => 'integer|exists:permission_groups,id',
]);
@ -481,10 +496,19 @@ class UsersController extends Controller
if ($validator->fails()){
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
$user->groups()->sync($request->input('groups'));
// Only save groups if the user is a superuser
if (Auth::user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// The groups field has been passed but it is null, so we should blank it out
} elseif ($request->has('groups')) {
$user->groups()->sync([]);
// Only save groups if the user is a superuser
if (Auth::user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
}
@ -505,37 +529,43 @@ class UsersController extends Controller
public function destroy($id)
{
$this->authorize('delete', User::class);
$user = User::findOrFail($id);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('delete', $user);
if (($user->assets) && ($user->assets->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete_has_assets')));
}
if ($user) {
if (($user->licenses) && ($user->licenses->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->licenses->count().' license(s) associated with them and cannot be deleted.'));
}
if (($user->accessories) && ($user->accessories->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->accessories->count().' accessories associated with them.'));
}
if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->managedLocations()->count().' locations that they manage.'));
}
if ($user->delete()) {
// Remove the user's avatar if they have one
if (Storage::disk('public')->exists('avatars/'.$user->avatar)) {
try {
Storage::disk('public')->delete('avatars/'.$user->avatar);
} catch (\Exception $e) {
\Log::debug($e);
}
$this->authorize('delete', $user);
if (($user->assets) && ($user->assets->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete_has_assets')));
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
if (($user->licenses) && ($user->licenses->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->licenses->count() . ' license(s) associated with them and cannot be deleted.'));
}
if (($user->accessories) && ($user->accessories->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->accessories->count() . ' accessories associated with them.'));
}
if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->managedLocations()->count() . ' locations that they manage.'));
}
if ($user->delete()) {
// Remove the user's avatar if they have one
if (Storage::disk('public')->exists('avatars/' . $user->avatar)) {
try {
Storage::disk('public')->delete('avatars/' . $user->avatar);
} catch (\Exception $e) {
\Log::debug($e);
}
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
@ -553,7 +583,31 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
$this->authorize('view', Asset::class);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model')->get();
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('view', $user);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');
// Filter on category ID
if ($request->filled('category_id')) {
$assets = $assets->InCategory($request->input('category_id'));
}
// Filter on model ID
if ($request->filled('model_id')) {
$model_ids = $request->input('model_id');
if (!is_array($model_ids)) {
$model_ids = array($model_ids);
}
$assets = $assets->InModelList($model_ids);
}
$assets = $assets->get();
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
@ -569,7 +623,10 @@ class UsersController extends Controller
*/
public function emailAssetList(Request $request, $id)
{
$this->authorize('update', User::class);
$user = User::findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
if (empty($user->email)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
@ -593,6 +650,7 @@ class UsersController extends Controller
$this->authorize('view', User::class);
$this->authorize('view', Consumable::class);
$user = User::findOrFail($id);
$this->authorize('update', $user);
$consumables = $user->consumables;
return (new ConsumablesTransformer)->transformConsumables($consumables, $consumables->count(), $request);
}
@ -609,6 +667,7 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
$user = User::findOrFail($id);
$this->authorize('view', $user);
$this->authorize('view', Accessory::class);
$accessories = $user->accessories;
@ -629,6 +688,7 @@ class UsersController extends Controller
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
@ -652,9 +712,20 @@ class UsersController extends Controller
if ($request->filled('id')) {
try {
$user = User::find($request->get('id'));
$this->authorize('update', $user);
$user->two_factor_secret = null;
$user->two_factor_enrolled = 0;
$user->save();
$user->saveQuietly();
// Log the reset
$logaction = new Actionlog();
$logaction->target_type = User::class;
$logaction->target_id = $user->id;
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('2FA reset');
return response()->json(['message' => trans('admin/settings/general.two_factor_reset_success')], 200);
} catch (\Exception $e) {
@ -706,11 +777,11 @@ class UsersController extends Controller
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/users/message.restore.success')), 200);
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')), 200);
}
// Check validation to make sure we're not restoring a user with the same username as an existing user
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.user'), 'error' => $user->getErrors()->first()])), 200);
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);

View file

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

View file

@ -7,6 +7,7 @@ use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -88,7 +89,7 @@ class AssetModelsController extends Controller
$model->requestable = Request::has('requestable');
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = e($request->input('fieldset_id'));
$model->fieldset_id = $request->input('fieldset_id');
}
$model = $request->handleImages($model);
@ -101,7 +102,6 @@ class AssetModelsController extends Controller
}
}
// Redirect to the new model page
return redirect()->route('models.index')->with('success', trans('admin/models/message.create.success'));
}
@ -166,17 +166,14 @@ class AssetModelsController extends Controller
$this->removeCustomFieldsDefaultValues($model);
if ($request->input('fieldset_id') == '') {
$model->fieldset_id = null;
} else {
$model->fieldset_id = $request->input('fieldset_id');
$model->fieldset_id = $request->input('fieldset_id');
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
}
@ -292,7 +289,7 @@ class AssetModelsController extends Controller
public function show($modelId = null)
{
$this->authorize('view', AssetModel::class);
$model = AssetModel::withTrashed()->find($modelId);
$model = AssetModel::withTrashed()->withCount('assets')->find($modelId);
if (isset($model->id)) {
return view('models/view', compact('model'));
@ -446,7 +443,6 @@ class AssetModelsController extends Controller
$del_count = 0;
foreach ($models as $model) {
\Log::debug($model->id);
if ($model->assets_count > 0) {
$del_error_count++;
@ -456,8 +452,6 @@ class AssetModelsController extends Controller
}
}
\Log::debug($del_count);
\Log::debug($del_error_count);
if ($del_error_count == 0) {
return redirect()->route('models.index')
@ -493,11 +487,11 @@ class AssetModelsController extends Controller
* @param array $defaultValues
* @return void
*/
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues)
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues): bool
{
$data = array();
foreach ($defaultValues as $customFieldId => $defaultValue) {
$customField = \App\Models\CustomField::find($customFieldId);
$customField = CustomField::find($customFieldId);
$data[$customField->db_column] = $defaultValue;
}

View file

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

View file

@ -6,8 +6,10 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckinRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
@ -15,6 +17,8 @@ use Illuminate\Support\Facades\View;
class AssetCheckinController extends Controller
{
use MigratesLegacyAssetLocations;
/**
* Returns a view that presents a form to check an asset back into inventory.
*
@ -35,6 +39,12 @@ class AssetCheckinController extends Controller
$this->authorize('checkin', $asset);
// This asset is already checked in, redirect
if (is_null($asset->assignedTo)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in'));
}
return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto);
}
@ -67,11 +77,9 @@ class AssetCheckinController extends Controller
}
$asset->expected_checkin = null;
$asset->last_checkout = null;
//$asset->last_checkout = null;
$asset->last_checkin = now();
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->assigned_type = null;
$asset->accepted = null;
$asset->name = $request->get('name');
@ -79,24 +87,7 @@ class AssetCheckinController extends Controller
$asset->status_id = e($request->get('status_id'));
}
// This is just meant to correct legacy issues where some user data would have 0
// as a location ID, which isn't valid. Later versions of Snipe-IT have stricter validation
// rules, so it's necessary to fix this for long-time users. It's kinda gross, but will help
// people (and their data) in the long run
if ($asset->rtd_location_id == '0') {
\Log::debug('Manually override the RTD location IDs');
\Log::debug('Original RTD Location ID: '.$asset->rtd_location_id);
$asset->rtd_location_id = '';
\Log::debug('New RTD Location ID: '.$asset->rtd_location_id);
}
if ($asset->location_id == '0') {
\Log::debug('Manually override the location IDs');
\Log::debug('Original Location ID: '.$asset->location_id);
$asset->location_id = '';
\Log::debug('New Location ID: '.$asset->location_id);
}
$this->migrateLegacyLocations($asset);
$asset->location_id = $asset->rtd_location_id;
@ -117,12 +108,9 @@ class AssetCheckinController extends Controller
$checkin_at = $request->get('checkin_at');
}
if(!empty($asset->licenseseats->all())){
foreach ($asset->licenseseats as $seat){
$seat->assigned_to = null;
$seat->save();
}
}
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
});
// Get all pending Acceptances for this asset and delete them
$acceptances = CheckoutAcceptance::pending()->whereHasMorph('checkoutable',

View file

@ -98,7 +98,7 @@ class AssetCheckoutController extends Controller
}
}
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $request->get('name'))) {
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.checkout.success'));
}

View file

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

View file

@ -102,6 +102,10 @@ class AssetsController extends Controller
{
$this->authorize(Asset::class);
// There are a lot more rules to add here but prevents
// errors around `asset_tags` not being present below.
$this->validate($request, ['asset_tags' => ['required', 'array']]);
// Handle asset tags - there could be one, or potentially many.
// This is only necessary on create, not update, since bulk editing is handled
// differently
@ -131,9 +135,6 @@ class AssetsController extends Controller
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
$asset->user_id = Auth::id();
$asset->archived = '0';
$asset->physical = '1';
$asset->depreciate = '0';
$asset->status_id = request('status_id');
$asset->warranty_months = request('warranty_months', null);
$asset->purchase_cost = request('purchase_cost');
@ -149,7 +150,8 @@ class AssetsController extends Controller
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
}
if ($asset->assigned_to == '') {
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
if (!request('assigned_user') && !request('assigned_asset') && !request('assigned_location')) {
$asset->location_id = $request->input('rtd_location_id', null);
}
@ -365,7 +367,6 @@ class AssetsController extends Controller
$asset->order_number = $request->input('order_number');
$asset->asset_tag = $asset_tag[1];
$asset->notes = $request->input('notes');
$asset->physical = '1';
$asset = $request->handleImages($asset);
@ -525,31 +526,33 @@ class AssetsController extends Controller
public function getBarCode($assetId = null)
{
$settings = Setting::getSettings();
$asset = Asset::find($assetId);
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png';
if ($asset = Asset::withTrashed()->find($assetId)) {
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png';
if (isset($asset->id, $asset->asset_tag)) {
if (file_exists($barcode_file)) {
$header = ['Content-type' => 'image/png'];
if (isset($asset->id, $asset->asset_tag)) {
if (file_exists($barcode_file)) {
$header = ['Content-type' => 'image/png'];
return response()->file($barcode_file, $header);
} else {
// Calculate barcode width in pixel based on label width (inch)
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
return response()->file($barcode_file, $header);
} else {
// Calculate barcode width in pixel based on label width (inch)
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
$barcode = new \Com\Tecnick\Barcode\Barcode();
try {
$barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
file_put_contents($barcode_file, $barcode_obj->getPngData());
$barcode = new \Com\Tecnick\Barcode\Barcode();
try {
$barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
file_put_contents($barcode_file, $barcode_obj->getPngData());
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
} catch (\Exception $e) {
Log::debug('The barcode format is invalid.');
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
} catch (\Exception $e) {
Log::debug('The barcode format is invalid.');
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
}
}
}
}
return null;
}
/**
@ -738,11 +741,11 @@ class AssetsController extends Controller
if ($isCheckinHeaderExplicit) {
//if checkin date header exists, assume that empty or future date is still checked out
//if checkin is before todays date, assume it's checked in and do not assign user ID, if checkin date is in the future or blank, this is the expected checkin date, items is checked out
// if checkin date header exists, assume that empty or future date is still checked out
// if checkin is before today's date, assume it's checked in and do not assign user ID, if checkin date is in the future or blank, this is the expected checkin date, items are checked out
if ((strtotime($checkin_date) > strtotime(Carbon::now())) || (empty($checkin_date))
) {
if ((strtotime($checkin_date) > strtotime(Carbon::now())) || (empty($checkin_date)))
{
//only do this if item is checked out
$asset->assigned_to = $user->id;
$asset->assigned_type = User::class;

View file

@ -7,11 +7,14 @@ use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Statuslabel;
use App\Models\Setting;
use App\View\Label;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use App\Http\Requests\AssetCheckoutRequest;
use App\Models\CustomField;
@ -23,6 +26,13 @@ class BulkAssetsController extends Controller
/**
* Display the bulk edit page.
*
* This method is super weird because it's kinda of like a controller within a controller.
* It's main function is to determine what the bulk action in, and then return a view with
* the information that view needs, be it bulk delete, bulk edit, restore, etc.
*
* This is something that made sense at the time, but sort of doesn't make sense now. A JS front-end to determine form
* action would make a lot more sense here and make things a lot more clear.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param int $assetId
@ -32,72 +42,146 @@ class BulkAssetsController extends Controller
public function edit(Request $request)
{
$this->authorize('view', Asset::class);
/**
* No asset IDs were passed
*/
if (! $request->filled('ids')) {
return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$asset_ids = $request->input('ids');
// Figure out where we need to send the user after the update is complete, and store that in the session
$bulk_back_url = request()->headers->get('referer');
session(['bulk_back_url' => $bulk_back_url]);
$asset_ids = array_values(array_unique($request->input('ids')));
//custom fields logic
$asset_custom_field = Asset::with(['model.fieldset.fields', 'model'])->whereIn('id', $asset_ids)->whereHas('model', function ($query) {
return $query->where('fieldset_id', '!=', null);
})->get();
$allowed_columns = [
'id',
'name',
'asset_tag',
'serial',
'model_number',
'last_checkout',
'notes',
'expected_checkin',
'order_number',
'image',
'assigned_to',
'created_at',
'updated_at',
'purchase_date',
'purchase_cost',
'last_audit_date',
'next_audit_date',
'warranty_months',
'checkout_counter',
'checkin_counter',
'requests_counter',
'byod',
'asset_eol_date',
];
$models = $asset_custom_field->unique('model_id');
/**
* Make sure the column is allowed, and if it's a custom field, make sure we strip the custom_fields. prefix
*/
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = str_replace('custom_fields.', '', $request->input('sort'));
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
$assets = Asset::with('assignedTo', 'location', 'model')->whereIn('assets.id', $asset_ids);
$assets = $assets->get();
if ($assets->isEmpty()) {
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
return redirect()->back()->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
}
$models = $assets->unique('model_id');
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
}
if ($request->filled('bulk_actions')) {
switch ($request->input('bulk_actions')) {
case 'labels':
$this->authorize('view', Asset::class);
$assets_found = Asset::find($asset_ids);
if ($assets_found->isEmpty()){
return redirect()->back();
}
return (new Label)
->with('assets', $assets_found)
->with('assets', $assets)
->with('settings', Setting::getSettings())
->with('bulkedit', true)
->with('count', 0);
case 'delete':
$this->authorize('delete', Asset::class);
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
$assets->each(function ($assets) {
$this->authorize('delete', $assets);
});
return view('hardware/bulk-delete')->with('assets', $assets);
case 'restore':
$this->authorize('update', Asset::class);
$assets = Asset::withTrashed()->find($asset_ids);
$assets = Asset::withTrashed()->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
});
return view('hardware/bulk-restore')->with('assets', $assets);
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
->with('models', $models->pluck(['model']))
->with('models', $models->pluck(['model']))
->with('modelNames', $modelNames);
}
}
switch ($sort_override) {
case 'model':
$assets->OrderModels($order);
break;
case 'model_number':
$assets->OrderModelNumber($order);
break;
case 'category':
$assets->OrderCategory($order);
break;
case 'manufacturer':
$assets->OrderManufacturer($order);
break;
case 'company':
$assets->OrderCompany($order);
break;
case 'location':
$assets->OrderLocation($order);
case 'rtd_location':
$assets->OrderRtdLocation($order);
break;
case 'status_label':
$assets->OrderStatus($order);
break;
case 'supplier':
$assets->OrderSupplier($order);
break;
case 'assigned_to':
$assets->OrderAssigned($order);
break;
default:
$assets->orderBy($column_sort, $order);
break;
}
return redirect()->back()->with('error', 'No action selected');
}
@ -117,25 +201,31 @@ class BulkAssetsController extends Controller
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
if (Session::exists('ids')) {
$assets = Session::get('ids');
} elseif (! $request->filled('ids') || count($request->input('ids')) <= 0) {
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$assets = array_keys($request->input('ids'));
if ($request->anyFilled($custom_field_columns)) {
$custom_fields_present = true;
} else {
$custom_fields_present = false;
}
$assets = Asset::whereIn('id', array_keys($request->input('ids')))->get();
/**
* If ANY of these are filled, prepare to update the values on the assets.
*
* Additional checks will be needed for some of them to make sure the values
* make sense (for example, changing the status ID to something incompatible with
* its checkout status.
*/
if (($request->filled('purchase_date'))
|| ($request->filled('expected_checkin'))
|| ($request->filled('purchase_cost'))
@ -154,22 +244,32 @@ class BulkAssetsController extends Controller
|| ($request->anyFilled($custom_field_columns))
) {
foreach ($assets as $assetId) {
// Let's loop through those assets and build an update array
foreach ($assets as $asset) {
$this->update_array = [];
/**
* Leave out model_id and status here because we do math on that later. We have to do some extra
* validation and checks on those two.
*
* It's tempting to make these match the request check above, but some of these values require
* extra work to make sure the data makes sense.
*/
$this->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
->conditionallyAddItem('status_id')
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date');
foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column);
}
}
/**
* Blank out fields that were requested to be blanked out via checkbox
*/
if ($request->input('null_purchase_date')=='1') {
$this->update_array['purchase_date'] = null;
}
@ -186,7 +286,6 @@ class BulkAssetsController extends Controller
$this->update_array['purchase_cost'] = $request->input('purchase_cost');
}
if ($request->filled('company_id')) {
$this->update_array['company_id'] = $request->input('company_id');
if ($request->input('company_id') == 'clear') {
@ -194,88 +293,129 @@ class BulkAssetsController extends Controller
}
}
/**
* We're trying to change the model ID - we need to do some extra checks here to make sure
* the custom field values work for the custom fieldset rules around this asset. Uniqueness
* and requiredness across the fieldset is particularly important, since those are
* fieldset-specific attributes.
*/
if ($request->filled('model_id')) {
$this->update_array['model_id'] = AssetModel::find($request->input('model_id'))->id;
}
/**
* We're trying to change the status ID - we need to do some extra checks here to
* make sure the status label type is one that makes sense for the state of the asset,
* for example, we shouldn't be able to make an asset archived if it's currently assigned
* to someone/something.
*/
if ($request->filled('status_id')) {
$updated_status = Statuslabel::find($request->input('status_id'));
// We cannot assign a non-deployable status type if the asset is already assigned.
// This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one.
if (
($asset->assigned_to == '')
|| ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1')
) {
$this->update_array['status_id'] = $updated_status->id;
}
}
/**
* We're changing the location ID - figure out which location we should apply
* this change to:
*
* 0 - RTD location only
* 1 - location ID and RTD location ID
* 2 - location ID only
*
* Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe
*/
if ($request->filled('rtd_location_id')) {
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) {
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
}
}
/**
* ------------------------------------------------------------------------------
* ANYTHING that happens past this foreach
* WILL NOT BE logged in the edit log_meta data
* ------------------------------------------------------------------------------
*/
$changed = [];
$asset = Asset::find($assetId);
foreach ($this->update_array as $key => $value) {
if ($this->update_array[$key] != $asset->{$key}) {
$changed[$key]['old'] = $asset->{$key};
$changed[$key]['new'] = $this->update_array[$key];
}
}
if ($custom_fields_present) {
$model = $asset->model()->first();
/**
* Start all the custom fields shenanigans
*/
// Use the rules of the new model fieldsets if the model changed
if ($request->filled('model_id')) {
$this->update_array['model_id'] = $request->input('model_id');
$model = \App\Models\AssetModel::find($request->input('model_id'));
}
// Does the model have a fieldset?
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
// Make sure this model is valid
$assetCustomFields = ($model) ? $model->fieldset : null;
if ($assetCustomFields && $assetCustomFields->fields) {
foreach ($assetCustomFields->fields as $field) {
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted=='1')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
/*
* Check if the decrypted existing value is different from one we just submitted
* and if not, pull it out of the object since it shouldn't really be updating at all.
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
* different times will have different values, so it will *look* like it was updated
* but it wasn't.
*/
if ($decrypted_old != $this->update_array[$field->db_column]) {
$asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]);
} else {
/*
* Remove the encrypted custom field from the update_array, since nothing changed
*/
unset($this->update_array[$field->db_column]);
unset($asset->{$field->db_column});
}
/*
* Check if the decrypted existing value is different from one we just submitted
* and if not, pull it out of the object since it shouldn't really be updating at all.
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
* different times will have different values, so it will *look* like it was updated
* but it wasn't.
*/
if ($decrypted_old != $this->update_array[$field->db_column]) {
$asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]);
} else {
/*
* These custom fields aren't encrypted, just carry on as usual
* Remove the encrypted custom field from the update_array, since nothing changed
*/
unset($this->update_array[$field->db_column]);
unset($asset->{$field->db_column});
}
/*
* These custom fields aren't encrypted, just carry on as usual
*/
} else {
if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) {
// Check if this is an array, and if so, flatten it
if (is_array($this->update_array[$field->db_column])) {
$asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]);
} else {
if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) {
// Check if this is an array, and if so, flatten it
if (is_array($this->update_array[$field->db_column])) {
$asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]);
} else {
$asset->{$field->db_column} = $this->update_array[$field->db_column];
}
}
$asset->{$field->db_column} = $this->update_array[$field->db_column];
}
}
}
} // endforeach
} // end custom field check
} // end custom fields handler
} // endforeach
}
// Check if it passes validation, and then try to save

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\SamlNonce;
use App\Models\Setting;
use App\Models\User;
use App\Models\Ldap;
@ -109,7 +110,14 @@ class LoginController extends Controller
try {
$user = $saml->samlLogin($samlData);
$notValidAfter = new \Carbon\Carbon(@$samlData['assertionNotOnOrAfter']);
if(\Carbon::now()->greaterThanOrEqualTo($notValidAfter)) {
abort(400,"Expired SAML Assertion");
}
if(SamlNonce::where('nonce', @$samlData['nonce'])->count() > 0) {
abort(400,"Assertion has already been used");
}
Log::debug("okay, fine, this is a new nonce then. Good for you.");
if (!is_null($user)) {
Auth::login($user);
} else {
@ -123,10 +131,14 @@ class LoginController extends Controller
$user->last_login = \Carbon::now();
$user->saveQuietly();
}
$s = new SamlNonce();
$s->nonce = @$samlData['nonce'];
$s->not_valid_after = $notValidAfter;
$s->save();
} catch (\Exception $e) {
\Log::debug('There was an error authenticating the SAML user: '.$e->getMessage());
throw new \Exception($e->getMessage());
throw $e;
}
// Fallthrough with better logging

View file

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

View file

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

View file

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

View file

@ -260,7 +260,7 @@ class CustomFieldsController extends Controller
$field->name = trim(e($request->get("name")));
$field->element = e($request->get("element"));
$field->field_values = e($request->get("field_values"));
$field->field_values = $request->get("field_values");
$field->user_id = Auth::id();
$field->help_text = $request->get("help_text");
$field->show_in_email = $show_in_email;

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\Group;
use Illuminate\Http\Request;
use Auth;
/**
* This controller handles all actions related to User Groups for
@ -63,6 +64,7 @@ class GroupsController extends Controller
$group = new Group();
$group->name = $request->input('name');
$group->permissions = json_encode($request->input('permission'));
$group->created_by = Auth::user()->id;
if ($group->save()) {
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create'));

View file

@ -6,6 +6,7 @@ use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\Labels\Label;
use App\Models\Location;
use App\Models\Manufacturer;
@ -13,6 +14,7 @@ use App\Models\Setting;
use App\Models\Supplier;
use App\Models\User;
use App\View\Label as LabelView;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class LabelsController extends Controller
@ -20,9 +22,9 @@ class LabelsController extends Controller
/**
* Returns the Label view with test data
*
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
* @param string $labelName
* @param string $labelName
* @return \Illuminate\Contracts\View\View
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
*/
public function show(string $labelName)
{
@ -41,29 +43,43 @@ class LabelsController extends Controller
$exampleAsset->status_id = 1;
$exampleAsset->company = new Company([
'name' => 'Test Company Limited',
'name' => trans('admin/labels/table.example_company'),
'phone' => '1-555-555-5555',
'email' => 'company@example.com',
]);
$exampleAsset->setRelation('assignedTo', new User(['first_name' => 'Luke', 'last_name' => 'Skywalker']));
$exampleAsset->defaultLoc = new Location(['name' => 'Building 1', 'phone' => '1-555-555-5555']);
$exampleAsset->location = new Location(['name' => 'Building 2', 'phone' => '1-555-555-5555']);
$exampleAsset->defaultLoc = new Location(['name' => trans('admin/labels/table.example_defaultloc'), 'phone' => '1-555-555-5555']);
$exampleAsset->location = new Location(['name' => trans('admin/labels/table.example_location'), 'phone' => '1-555-555-5555']);
$exampleAsset->model = new AssetModel();
$exampleAsset->model->id = 999999;
$exampleAsset->model->name = 'Test Model';
$exampleAsset->model->name = trans('admin/labels/table.example_model');
$exampleAsset->model->model_number = 'MDL5678';
$exampleAsset->model->manufacturer = new Manufacturer();
$exampleAsset->model->manufacturer->id = 999999;
$exampleAsset->model->manufacturer->name = 'Test Manufacturing Inc.';
$exampleAsset->model->manufacturer->name = trans('admin/labels/table.example_manufacturer');
$exampleAsset->model->manufacturer->support_email = 'support@test.com';
$exampleAsset->model->manufacturer->support_phone = '1-555-555-5555';
$exampleAsset->model->manufacturer->support_url = 'https://example.com';
$exampleAsset->supplier = new Supplier(['name' => 'Test Company Limited']);
$exampleAsset->supplier = new Supplier(['name' => trans('admin/labels/table.example_company')]);
$exampleAsset->model->category = new Category();
$exampleAsset->model->category->id = 999999;
$exampleAsset->model->category->name = 'Test Category';
$exampleAsset->model->category->name = trans('admin/labels/table.example_category');
$customFieldColumns = CustomField::where('field_encrypted', '=', 0)->pluck('db_column');
collect(explode(';', Setting::getSettings()->label2_fields))
->filter()
->each(function ($item) use ($customFieldColumns, $exampleAsset) {
$pair = explode('=', $item);
if (array_key_exists(1, $pair)) {
if ($customFieldColumns->contains($pair[1])) {
$exampleAsset->{$pair[1]} = "{{$pair[0]}}";
}
}
});
$settings = Setting::getSettings();
if (request()->has('settings')) {

View file

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

View file

@ -99,6 +99,7 @@ class LicensesController extends Controller
$license->category_id = $request->input('category_id');
$license->termination_date = $request->input('termination_date');
$license->user_id = Auth::id();
$license->min_amt = $request->input('min_amt');
if ($license->save()) {
return redirect()->route('licenses.index')->with('success', trans('admin/licenses/message.create.success'));
@ -176,6 +177,7 @@ class LicensesController extends Controller
$license->manufacturer_id = $request->input('manufacturer_id');
$license->supplier_id = $request->input('supplier_id');
$license->category_id = $request->input('category_id');
$license->min_amt = $request->input('min_amt');
if ($license->save()) {
return redirect()->route('licenses.show', ['license' => $licenseId])->with('success', trans('admin/licenses/message.update.success'));
@ -245,12 +247,6 @@ class LicensesController extends Controller
$available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
\Log::debug('Total: '.$total_seats_count);
\Log::debug('Users: '.$users_count);
\Log::debug('Available: '.$available_seats_count);
\Log::debug('Checkedout: '.$checkedout_seats_count);
$this->authorize('view', $license);
return view('licenses.view', compact('license'))
->with('users_count', $users_count)

View file

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

View file

@ -56,7 +56,7 @@ class ProfileController extends Controller
$user->phone = $request->input('phone');
if (! config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en');
$user->locale = $request->input('locale', 'en-US');
}
if ((Gate::allows('self.two_factor')) && ((Setting::getSettings()->two_factor_enabled == '1') && (! config('app.lock_passwords')))) {

View file

@ -252,6 +252,9 @@ class ReportsController extends Controller
trans('general.model_no'),
'To',
trans('general.notes'),
trans('admin/settings/general.login_ip'),
trans('admin/settings/general.login_user_agent'),
trans('general.action_source'),
'Changed',
];
@ -292,12 +295,15 @@ class ReportsController extends Controller
$actionlog->present()->actionType(),
e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
($actionlog->item->serial) ? $actionlog->item->serial : null,
($actionlog->item->model) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null,
($actionlog->item->model) ? $actionlog->item->model->model_number : null,
($actionlog->item) ? $actionlog->item->serial : null,
(($actionlog->item) && ($actionlog->item->model)) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null,
(($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null,
$target_name,
($actionlog->note) ? e($actionlog->note) : '',
$actionlog->log_meta,
$actionlog->remote_ip,
$actionlog->user_agent,
$actionlog->action_source,
];
fputcsv($handle, $row);
}
@ -610,7 +616,7 @@ class ReportsController extends Controller
}
if ($request->filled('url')) {
$header[] = trans('admin/manufacturers/table.url');
$header[] = trans('general.url');
}
@ -680,20 +686,27 @@ class ReportsController extends Controller
$assets->whereBetween('assets.created_at', [$created_start, $created_end]);
}
if (($request->filled('checkout_date_start')) && ($request->filled('checkout_date_end'))) {
$checkout_start = \Carbon::parse($request->input('checkout_date_start'))->startOfDay();
$checkout_end = \Carbon::parse($request->input('checkout_date_end'))->endOfDay();
$checkout_end = \Carbon::parse($request->input('checkout_date_end',now()))->endOfDay();
$assets->whereBetween('assets.last_checkout', [$checkout_start, $checkout_end]);
$actionlogassets = Actionlog::where('action_type','=', 'checkout')
->where('item_type', 'LIKE', '%Asset%',)
->whereBetween('action_date',[$checkout_start, $checkout_end])
->pluck('item_id');
$assets->whereIn('assets.id',$actionlogassets);
}
if (($request->filled('checkin_date_start'))) {
$assets->whereBetween('last_checkin', [
Carbon::parse($request->input('checkin_date_start'))->startOfDay(),
$checkin_start = \Carbon::parse($request->input('checkin_date_start'))->startOfDay();
// use today's date is `checkin_date_end` is not provided
Carbon::parse($request->input('checkin_date_end', now()))->endOfDay(),
]);
$checkin_end = \Carbon::parse($request->input('checkin_date_end', now()))->endOfDay();
$assets->whereBetween('assets.last_checkin', [$checkin_start, $checkin_end ]);
}
//last checkin is exporting, but currently is a date and not a datetime in the custom report ONLY.
if (($request->filled('expected_checkin_start')) && ($request->filled('expected_checkin_end'))) {
$assets->whereBetween('assets.expected_checkin', [$request->input('expected_checkin_start'), $request->input('expected_checkin_end')]);
@ -712,10 +725,10 @@ class ReportsController extends Controller
if ($request->filled('exclude_archived')) {
$assets->notArchived();
}
if ($request->input('deleted_assets') == '1') {
if ($request->input('deleted_assets') == 'include_deleted') {
$assets->withTrashed();
}
if ($request->input('deleted_assets') == '0') {
if ($request->input('deleted_assets') == 'only_deleted') {
$assets->onlyTrashed();
}
@ -1150,16 +1163,24 @@ class ReportsController extends Controller
$logItem = $logItem_res[0];
}
if (!$assetItem->assignedTo->locale){
Notification::locale(Setting::getSettings()->locale)->send(
$assetItem->assignedTo,
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
);
} else {
Notification::send(
$assetItem->assignedTo,
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
);
// Only send notification if assigned
if ($assetItem->assignedTo) {
if (!$assetItem->assignedTo->locale) {
Notification::locale(Setting::getSettings()->locale)->send(
$assetItem->assignedTo,
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
);
} else {
Notification::send(
$assetItem->assignedTo,
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
);
}
}
if ($assetItem->assignedTo->email == ''){
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email'));
}
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent'));

View file

@ -20,6 +20,7 @@ use DB;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Image;
use Input;
use Redirect;
@ -28,6 +29,7 @@ use App\Http\Requests\SlackSettingsRequest;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
/**
* This controller handles all actions related to Settings for
@ -186,7 +188,7 @@ class SettingsController extends Controller
$settings->alerts_enabled = 1;
$settings->pwd_secure_min = 10;
$settings->brand = 1;
$settings->locale = $request->input('locale', 'en');
$settings->locale = $request->input('locale', 'en-US');
$settings->default_currency = $request->input('default_currency', 'USD');
$settings->user_id = 1;
$settings->email_domain = $request->input('email_domain');
@ -356,6 +358,7 @@ class SettingsController extends Controller
}
$setting->default_eula_text = $request->input('default_eula_text');
$setting->load_remote = $request->input('load_remote', 0);
$setting->thumbnail_max_h = $request->input('thumbnail_max_h');
$setting->privacy_policy_link = $request->input('privacy_policy_link');
@ -420,68 +423,46 @@ class SettingsController extends Controller
// Only allow the site name and CSS to be changed if lock_passwords is false
// Because public demos make people act like dicks
if (! config('app.lock_passwords')) {
$setting->site_name = $request->input('site_name');
$setting->custom_css = $request->input('custom_css');
}
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ('1' == $request->input('clear_logo')) {
if ('1' == $request->input('clear_logo')) {
Storage::disk('public')->delete($setting->logo);
$setting->logo = null;
$setting->logo = null;
$setting->brand = 1;
}
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// If the user wants to clear the favicon...
if ($request->hasFile('favicon')) {
$favicon_image = $favicon_upload = $request->file('favicon');
$favicon_ext = $favicon_image->getClientOriginalExtension();
$setting->favicon = $favicon_file_name = 'favicon-uploaded.'.$favicon_ext;
if (($favicon_image->getClientOriginalExtension() != 'ico') && ($favicon_image->getClientOriginalExtension() != 'svg')) {
$favicon_upload = Image::make($favicon_image->getRealPath())->resize(null, 36, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($favicon_file_name, (string) $favicon_upload->encode());
} else {
Storage::disk('public')->put($favicon_file_name, file_get_contents($request->file('favicon')));
}
// Remove Current image if exists
if (($setting->favicon) && (file_exists($favicon_file_name))) {
Storage::disk('public')->delete($favicon_file_name);
}
} elseif ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->clear_favicon);
$setting->favicon = null;
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
// If they are uploading an image, validate it and upload it
}
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
$setting = $request->handleImages($setting, 600, 'favicon', '', 'favicon');
// If the user wants to clear the favicon...
if ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->favicon);
$setting->favicon = null;
}
}
if ($setting->save()) {
return redirect()->route('settings.index')
@ -519,6 +500,19 @@ class SettingsController extends Controller
*/
public function postSecurity(Request $request)
{
$this->validate($request, [
'pwd_secure_complexity' => 'array',
'pwd_secure_complexity.*' => [
Rule::in([
'disallow_same_pwd_as_user_fields',
'letters',
'numbers',
'symbols',
'case_diff',
])
]
]);
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}
@ -585,7 +579,7 @@ class SettingsController extends Controller
}
if (! config('app.lock_passwords')) {
$setting->locale = $request->input('locale', 'en');
$setting->locale = $request->input('locale', 'en-US');
}
$setting->default_currency = $request->input('default_currency', '$');
$setting->date_display_format = $request->input('date_display_format');
@ -635,21 +629,21 @@ class SettingsController extends Controller
// Check if the audit interval has changed - if it has, we want to update ALL of the assets audit dates
if ($request->input('audit_interval') != $setting->audit_interval) {
// Be careful - this could be a negative number
// This could be a negative number if the user is trying to set the audit interval to a lower number than it was before
$audit_diff_months = ((int)$request->input('audit_interval') - (int)($setting->audit_interval));
// Batch update the dates. We have to use this method to avoid time limit exceeded errors on very large datasets,
// but it DOES mean this change doesn't get logged in the action logs, since it skips the observer.
// @see https://stackoverflow.com/questions/54879160/laravel-observer-not-working-on-bulk-insert
$affected = Asset::whereNotNull('next_audit_date')
->whereNull('deleted_at')
->update(
['next_audit_date' => DB::raw('DATE_ADD(next_audit_date, INTERVAL '.$audit_diff_months.' MONTH)')]
);
\Log::debug($affected .' assets affected by audit interval update');
// Grab all of the assets that have an existing next_audit_date
$assets = Asset::whereNotNull('next_audit_date')->get();
// Update all of the assets' next_audit_date values
foreach ($assets as $asset) {
if ($asset->next_audit_date != '') {
$old_next_audit = new \DateTime($asset->next_audit_date);
$asset->next_audit_date = $old_next_audit->modify($audit_diff_months.' month')->format('Y-m-d');
$asset->forceSave();
}
}
}
$alert_email = rtrim($request->input('alert_email'), ',');
@ -810,10 +804,9 @@ class SettingsController extends Controller
*/
public function getLabels()
{
return view('settings.labels', [
'setting' => Setting::getSettings(),
'customFields' => CustomField::all(),
]);
return view('settings.labels')
->with('setting', Setting::getSettings())
->with('customFields', CustomField::where('field_encrypted', '=', 0)->get());
}
/**

View file

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

View file

@ -182,8 +182,13 @@ class UsersController extends Controller
*/
public function edit($id)
{
if ($user = User::find($id)) {
$this->authorize('update', $user);
$this->authorize('update', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
if ($user) {
$permissions = config('permissions');
$groups = Group::pluck('name', 'id');
@ -210,106 +215,109 @@ class UsersController extends Controller
*/
public function update(SaveUserRequest $request, $id = null)
{
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$this->authorize('update', User::class);
// This is a janky hack to prevent people from changing admin demo user data on the public demo.
// The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
// Thanks, jerks. You are why we can't have nice things. - snipe
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
return redirect()->route('users.index')->with('error', 'Permission denied. You cannot update user information for superadmins on the demo.');
return redirect()->route('users.index')->with('error', trans('general.permission_denied_superuser_demo'));
}
try {
$user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
$this->authorize('update', $user);
// Figure out of this user was an admin before this edit
$orig_permissions_array = $user->decodePermissions();
$orig_superuser = '0';
if (is_array($orig_permissions_array)) {
if (array_key_exists('superuser', $orig_permissions_array)) {
$orig_superuser = $orig_permissions_array['superuser'];
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
// User is valid - continue...
if ($user) {
$this->authorize('update', $user);
// Figure out of this user was an admin before this edit
$orig_permissions_array = $user->decodePermissions();
$orig_superuser = '0';
if (is_array($orig_permissions_array)) {
if (array_key_exists('superuser', $orig_permissions_array)) {
$orig_superuser = $orig_permissions_array['superuser'];
}
}
}
// Only save groups if the user is a super user
if (Auth::user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Only save groups if the user is a superuser
if (Auth::user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Update the user
if ($request->filled('username')) {
// Update the user fields
$user->username = trim($request->input('username'));
}
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->department_id = $request->input('department_id', null);
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->department_id = $request->input('department_id', null);
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
// Redirect to the user page
return redirect()->route('users.index')
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
//\Log::debug(print_r($user, true));
// Was the user updated?
if ($user->save()) {
// Redirect to the user page
return redirect()->route('users.index')
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@ -323,12 +331,13 @@ class UsersController extends Controller
*/
public function destroy($id = null)
{
try {
// Get user information
$user = User::findOrFail($id);
// Authorize takes care of many of our logic checks now.
$this->authorize('delete', User::class);
$this->authorize('delete', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
if ($user) {
// Check if we are not trying to delete ourselves
if ($user->id === Auth::id()) {
// Redirect to the user management page
@ -362,16 +371,12 @@ class UsersController extends Controller
// Delete the user
$user->delete();
// Prepare the success message
// Redirect to the user management page
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.delete'));
} catch (ModelNotFoundException $e) {
// Prepare the error message
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@ -427,59 +432,25 @@ class UsersController extends Controller
*/
public function show($userId = null)
{
if (! $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($userId)) {
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
}
// Make sure the user can view users at all
$this->authorize('view', User::class);
$userlog = $user->userlog->load('item');
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($userId);
// Make sure they can view this particular user
$this->authorize('view', $user);
return view('users/view', compact('user', 'userlog'))
->with('settings', Setting::getSettings());
}
/**
* Unsuspend a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $id
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getUnsuspend($id = null)
{
try {
// Get user information
$user = User::findOrFail($id);
$this->authorize('update', $user);
// Check if we are not trying to unsuspend ourselves
if ($user->id === Auth::id()) {
// Prepare the error message
$error = trans('admin/users/message.error.unsuspend');
// Redirect to the user management page
return redirect()->route('users.index')->with('error', $error);
}
// Do we have permission to unsuspend this user?
if ($user->isSuperUser() && ! Auth::user()->isSuperUser()) {
// Redirect to the user management page
return redirect()->route('users.index')->with('error', 'Insufficient permissions!');
}
// Redirect to the user management page
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.unsuspend'));
} catch (ModelNotFoundException $e) {
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
if ($user) {
$userlog = $user->userlog->load('item');
return view('users/view', compact('user', 'userlog'))->with('settings', Setting::getSettings());
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
}
/**
* Return a view containing a pre-populated new user form,
* populated with some fields from an existing user.
@ -493,22 +464,34 @@ class UsersController extends Controller
public function getClone(Request $request, $id = null)
{
$this->authorize('create', User::class);
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
try {
// Get the user information
$user_to_clone = User::withTrashed()->find($id);
$user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user_to_clone = Company::scopeCompanyables($user_to_clone)->find($id);
// Make sure they can view this particular user
$this->authorize('view', $user_to_clone);
if ($user_to_clone) {
$user = clone $user_to_clone;
// Blank out some fields
$user->first_name = '';
$user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null;
// Get this user groups
// Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id');
// Get all the available permissions
$permissions = config('permissions');
$clonedPermissions = $user_to_clone->decodePermissions();
@ -517,16 +500,14 @@ class UsersController extends Controller
// Show the page
return view('users/edit', compact('permissions', 'userPermissions'))
->with('user', $user)
->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups)
->with('clone_user', $user_to_clone);
} catch (ModelNotFoundException $e) {
// Prepare the error message
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
->with('user', $user)
->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups)
->with('clone_user', $user_to_clone);
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@ -546,8 +527,20 @@ class UsersController extends Controller
// Open output stream
$handle = fopen('php://output', 'w');
User::with('assets', 'accessories', 'consumables', 'department', 'licenses', 'manager', 'groups', 'userloc', 'company')
->orderBy('created_at', 'DESC')
$users = User::with(
'assets',
'accessories',
'consumables',
'department',
'licenses',
'manager',
'groups',
'userloc',
'company'
)->orderBy('created_at', 'DESC');
// FMCS scoping
Company::scopeCompanyables($users)
->chunk(500, function ($users) use ($handle) {
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
@ -565,7 +558,7 @@ class UsersController extends Controller
trans('general.licenses'),
trans('general.accessories'),
trans('general.consumables'),
trans('admin/users/table.groups'),
trans('general.groups'),
trans('general.permission_level'),
trans('general.notes'),
trans('admin/users/table.activated'),
@ -640,7 +633,11 @@ class UsersController extends Controller
public function printInventory($id)
{
$this->authorize('view', User::class);
$show_user = User::where('id', $id)->withTrashed()->first();
$show_user = Company::scopeCompanyables(User::where('id', $id)->withTrashed()->first());
// Make sure they can view this particular user
$this->authorize('view', $show_user);
$assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get();
$accessories = $show_user->accessories()->get();
$consumables = $show_user->consumables()->get();
@ -665,16 +662,23 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
if (!$user = User::find($id)) {
return redirect()->back()
->with('error', trans('admin/users/message.user_not_found', ['id' => $id]));
}
if (empty($user->email)) {
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
$user = Company::scopeCompanyables(User::find($id));
// Make sure they can view this particular user
$this->authorize('view', $user);
if ($user) {
if (empty($user->email)) {
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
}
$user->notify((new CurrentInventory($user)));
return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
}
$user->notify((new CurrentInventory($user)));
return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
return redirect()->back()->with('error', trans('admin/users/message.user_not_found', ['id' => $id]));
}
/**
@ -686,19 +690,19 @@ class UsersController extends Controller
*/
public function sendPasswordReset($id)
{
if (($user = User::find($id)) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) {
if (($user = Company::scopeCompanyables(User::find($id))) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) {
$credentials = ['email' => trim($user->email)];
try {
Password::sendResetLink($credentials);
return redirect()->back()->with('success', trans('admin/users/message.password_reset_sent', ['email' => $user->email]));
} catch (\Exception $e) {
return redirect()->back()->with('error', ' Error sending email. :( ');
return redirect()->back()->with('error', trans('general.error_sending_email'));
}
}
return redirect()->back()->with('error', 'User is not activated, is LDAP synced, or does not have an email address ');
return redirect()->back()->with('error', trans('general.pwd_reset_not_sent'));
}
}

View file

@ -59,12 +59,21 @@ class Importer extends Component
'field_map' => 'array'
];
/**
* This is used in resources/views/livewire/importer.blade.php, and we kinda shouldn't need to check for
* activeFile here, but there's some UI goofiness that allows this to crash out on some imports.
*
* @return string
*/
public function generate_field_map()
{
\Log::debug("header row is: ".print_r($this->activeFile->header_row,true));
\Log::debug("Field map is: ".print_r($this->field_map,true));
$tmp = array_combine($this->activeFile->header_row, $this->field_map);
return json_encode(array_filter($tmp));
$tmp = array();
if ($this->activeFile) {
$tmp = array_combine($this->activeFile->header_row, $this->field_map);
$tmp = array_filter($tmp);
}
return json_encode($tmp);
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Livewire;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use App\Models\Setting;
use App\Helpers\Helper;
@ -30,36 +31,55 @@ class SlackSettingsForm extends Component
'webhook_channel' => 'required_with:webhook_endpoint|starts_with:#|nullable',
'webhook_botname' => 'string|nullable',
];
public $messages = [
'webhook_endpoint.starts_with' => 'your webhook endpoint should begin with http://, https:// or other protocol.',
];
public function mount() {
$this->webhook_text= [
"slack" => array(
"slack" => array(
"name" => trans('admin/settings/general.slack') ,
"icon" => 'fab fa-slack',
"placeholder" => "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXX",
"link" => 'https://api.slack.com/messaging/webhooks',
"icon" => 'fab fa-slack',
"placeholder" => "https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXX",
"link" => 'https://api.slack.com/messaging/webhooks',
"test" => "testWebhook"
),
"general"=> array(
"name" => trans('admin/settings/general.general_webhook'),
"icon" => "fab fa-hashtag",
"placeholder" => "",
"placeholder" => trans('general.url'),
"link" => "",
"test" => "testWebhook"
),
"google" => array(
"name" => trans('admin/settings/general.google_workspaces'),
"icon" => "fa-brands fa-google",
"placeholder" => "https://chat.googleapis.com/v1/spaces/xxxxxxxx/messages?key=xxxxxx",
"link" => "https://developers.google.com/chat/how-tos/webhooks#register_the_incoming_webhook",
"test" => "googleWebhookTest"
),
"microsoft" => array(
"name" => trans('admin/settings/general.ms_teams'),
"icon" => "fa-brands fa-microsoft",
"placeholder" => "https://abcd.webhook.office.com/webhookb2/XXXXXXX",
"link" => "https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1",
"test" => "msTeamTestWebhook"
),
];
$this->setting = Setting::getSettings();
$this->save_button = trans('general.save');
$this->webhook_selected = $this->setting->webhook_selected;
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname;
$this->webhook_options = $this->setting->webhook_selected;
if($this->webhook_selected == 'microsoft' || $this->webhook_selected == 'google'){
$this->webhook_channel = '#NA';
}
if($this->setting->webhook_endpoint != null && $this->setting->webhook_channel != null){
@ -77,11 +97,17 @@ class SlackSettingsForm extends Component
$this->webhook_name = $this->webhook_text[$this->webhook_selected]['name'];
$this->webhook_icon = $this->webhook_text[$this->webhook_selected]["icon"]; ;
$this->webhook_placeholder = $this->webhook_text[$this->webhook_selected]["placeholder"];
$this->webhook_endpoint = null;
$this->webhook_link = $this->webhook_text[$this->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->webhook_selected]["test"];
if($this->webhook_selected != 'slack'){
$this->isDisabled= '';
$this->save_button = trans('general.save');
}
if($this->webhook_selected == 'microsoft' || $this->webhook_selected == 'google'){
$this->webhook_channel = '#NA';
}
}
private function isButtonDisabled() {
@ -141,6 +167,7 @@ class SlackSettingsForm extends Component
}
public function clearSettings(){
if (Helper::isDemoMode()) {
@ -174,8 +201,68 @@ class SlackSettingsForm extends Component
$this->setting->save();
session()->flash('success',trans('admin/settings/message.update.success'));
}
}
public function googleWebhookTest(){
$payload = [
"text" => trans('general.webhook_test_msg', ['app' => $this->webhook_name]),
];
try {
$response = Http::withHeaders([
'content-type' => 'applications/json',
])->post($this->webhook_endpoint,
$payload)->throw();
if (($response->getStatusCode() == 302) || ($response->getStatusCode() == 301)) {
return session()->flash('error', trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint]));
}
$this->isDisabled='';
$this->save_button = trans('general.save');
return session()->flash('success' , trans('admin/settings/message.webhook.success', ['webhook_name' => $this->webhook_name]));
} catch (\Exception $e) {
$this->isDisabled='disabled';
$this->save_button = trans('admin/settings/general.webhook_presave');
return session()->flash('error' , trans('admin/settings/message.webhook.error', ['error_message' => $e->getMessage(), 'app' => $this->webhook_name]));
}
}
public function msTeamTestWebhook(){
$payload =
[
"@type" => "MessageCard",
"@context" => "http://schema.org/extensions",
"summary" => trans('mail.snipe_webhook_summary'),
"title" => trans('mail.snipe_webhook_test'),
"text" => trans('general.webhook_test_msg', ['app' => $this->webhook_name]),
];
try {
$response = Http::withHeaders([
'content-type' => 'applications/json',
])->post($this->webhook_endpoint,
$payload)->throw();
if(($response->getStatusCode() == 302)||($response->getStatusCode() == 301)){
return session()->flash('error' , trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint]));
}
$this->isDisabled='';
$this->save_button = trans('general.save');
return session()->flash('success' , trans('admin/settings/message.webhook.success', ['webhook_name' => $this->webhook_name]));
} catch (\Exception $e) {
$this->isDisabled='disabled';
$this->save_button = trans('admin/settings/general.webhook_presave');
return session()->flash('error' , trans('admin/settings/message.webhook.error', ['error_message' => $e->getMessage(), 'app' => $this->webhook_name]));
}
return session()->flash('error' , trans('admin/settings/message.webhook.error_misc'));
}
}

View file

@ -4,9 +4,16 @@ namespace App\Http\Middleware;
use App\Models\Setting;
use Closure;
use \App\Helpers\Helper;
class CheckLocale
{
private function warn_legacy_locale($language, $source)
{
if ($language != Helper::mapLegacyLocale($language)) {
\Log::warning("$source $language and should be updated to be ".Helper::mapLegacyLocale($language));
}
}
/**
* Handle the locale for the user, default to settings otherwise.
*
@ -18,22 +25,27 @@ class CheckLocale
*/
public function handle($request, Closure $next, $guard = null)
{
// Default app settings from config
$language = config('app.locale');
$this->warn_legacy_locale($language, "APP_LOCALE in .env is set to");
if ($settings = Setting::getSettings()) {
// User's preference
if (($request->user()) && ($request->user()->locale)) {
\App::setLocale($request->user()->locale);
$language = $request->user()->locale;
$this->warn_legacy_locale($language, "username ".$request->user()->username." (".$request->user()->id.") has a language");
// App setting preference
} elseif ($settings->locale != '') {
\App::setLocale($settings->locale);
// Default app setting
} else {
\App::setLocale(config('app.locale'));
$language = $settings->locale;
$this->warn_legacy_locale($language, "App Settings is set to");
}
}
\App::setLocale(config('app.locale'));
}
\App::setLocale(Helper::mapLegacyLocale($language));
return $next($request);
}
}

View file

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

View file

@ -34,8 +34,9 @@ class ImageUploadRequest extends Request
{
return [
'image' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp',
'avatar' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp',
'image' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif',
'avatar' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif',
'favicon' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,image/x-icon,image/vnd.microsoft.icon,ico',
];
}
@ -97,22 +98,36 @@ class ImageUploadRequest extends Request
if (!config('app.lock_passwords')) {
$ext = $image->getClientOriginalExtension();
$ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
\Log::info('File name will be: '.$file_name);
\Log::debug('File extension is: '.$ext);
if (($image->getClientOriginalExtension() !== 'webp') && ($image->getClientOriginalExtension() !== 'svg')) {
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
// icons or avif, and webp support and needs to be compiled into gd for resizing to be available
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
\Log::debug('Not an SVG or webp - resize');
\Log::debug('Trying to upload to: '.$path.'/'.$file_name);
} elseif($image->getMimeType() == 'image/svg+xml') {
// If the file is an SVG, we need to clean it and NOT encode it
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
$upload = Image::make($image->getRealPath())->resize(null, $w, function ($constraint) {
Storage::disk('public')->put($path . '/' . $file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug($e);
}
} else {
try {
$upload = Image::make($image->getRealPath())->setFileInfoFromPath($image->getRealPath())->resize(null, $w, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
})->orientate();
} catch(NotReadableException $e) {
\Log::debug($e);
$validator = \Validator::make([], []);
@ -124,35 +139,12 @@ class ImageUploadRequest extends Request
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($path.'/'.$file_name, (string) $upload->encode());
} else {
// If the file is a webp, we need to just move it since webp support
// needs to be compiled into gd for resizing to be available
if ($image->getClientOriginalExtension() == 'webp') {
\Log::debug('This is a webp, just move it');
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
// If the file is an SVG, we need to clean it and NOT encode it
} else {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
\Log::debug('Trying to upload to: '.$path.'/'.$file_name);
Storage::disk('public')->put($path.'/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
}
}
// Remove Current image if exists
if (($item->{$form_fieldname}!='') && (Storage::disk('public')->exists($path.'/'.$item->{$db_fieldname}))) {
\Log::debug('A file already exists that we are replacing - we should delete the old one.');
try {
Storage::disk('public')->delete($path.'/'.$item->{$form_fieldname});
\Log::debug('Old file '.$path.'/'.$file_name.' has been deleted.');
} catch (\Exception $e) {
\Log::debug('Could not delete old file. '.$path.'/'.$file_name.' does not exist?');
}

View file

@ -3,7 +3,9 @@
namespace App\Http\Requests;
use App\Models\Asset;
use Illuminate\Foundation\Http\FormRequest;
use App\Models\Company;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Facades\Gate;
class StoreAssetRequest extends ImageUploadRequest
@ -20,7 +22,20 @@ class StoreAssetRequest extends ImageUploadRequest
public function prepareForValidation(): void
{
//
// Guard against users passing in an array for company_id instead of an integer.
// If the company_id is not an integer then we simply use what was
// provided to be caught by model level validation later.
$idForCurrentUser = is_int($this->company_id)
? Company::getIdForCurrentUser($this->company_id)
: $this->company_id;
$this->parseLastAuditDate();
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
'assigned_to' => $assigned_to ?? null,
]);
}
/**
@ -37,4 +52,21 @@ class StoreAssetRequest extends ImageUploadRequest
return $rules;
}
private function parseLastAuditDate(): void
{
if ($this->input('last_audit_date')) {
try {
$lastAuditDate = Carbon::parse($this->input('last_audit_date'));
$this->merge([
'last_audit_date' => $lastAuditDate->startOfDay()->format('Y-m-d H:i:s'),
]);
} catch (InvalidFormatException $e) {
// we don't need to do anything here...
// we'll keep the provided date in an
// invalid format so validation picks it up later
}
}
}
}

View file

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

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Traits;
use App\Models\Asset;
trait MigratesLegacyAssetLocations
{
/**
* This is just meant to correct legacy issues where some user data would have 0
* as a location ID, which isn't valid. Later versions of Snipe-IT have stricter validation
* rules, so it's necessary to fix this for long-time users. It's kinda gross, but will help
* people (and their data) in the long run
* @param Asset $asset
* @return void
*/
private function migrateLegacyLocations(Asset $asset): void
{
if ($asset->rtd_location_id == '0') {
\Log::debug('Manually override the RTD location IDs');
\Log::debug('Original RTD Location ID: ' . $asset->rtd_location_id);
$asset->rtd_location_id = '';
\Log::debug('New RTD Location ID: ' . $asset->rtd_location_id);
}
if ($asset->location_id == '0') {
\Log::debug('Manually override the location IDs');
\Log::debug('Original Location ID: ' . $asset->location_id);
$asset->location_id = '';
\Log::debug('New Location ID: ' . $asset->location_id);
}
}
}

View file

@ -6,6 +6,7 @@ use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\Company;
use App\Models\Supplier;
use App\Models\Location;
@ -181,6 +182,9 @@ class ActionlogsTransformer
'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null,
'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null,
'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null,
'remote_ip' => ($actionlog->remote_ip) ?? null,
'user_agent' => ($actionlog->user_agent) ?? null,
'action_source' => ($actionlog->action_source) ?? null,
'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'),
];
@ -212,6 +216,7 @@ class ActionlogsTransformer
{ $location = Location::withTrashed()->get();
$supplier = Supplier::withTrashed()->get();
$model = AssetModel::withTrashed()->get();
$status = Statuslabel::withTrashed()->get();
$company = Company::get();
@ -285,6 +290,19 @@ class ActionlogsTransformer
$clean_meta['Supplier'] = $clean_meta['supplier_id'];
unset($clean_meta['supplier_id']);
}
if(array_key_exists('status_id', $clean_meta)) {
$oldStatus = $status->find($clean_meta['status_id']['old']);
$oldStatusName = $oldStatus ? e($oldStatus->name) : trans('admin/statuslabels/message.deleted_label');
$newStatus = $status->find($clean_meta['status_id']['new']);
$newStatusName = $newStatus ? e($newStatus->name) : trans('admin/statuslabels/message.deleted_label');
$clean_meta['status_id']['old'] = $clean_meta['status_id']['old'] ? "[id: ".$clean_meta['status_id']['old']."] ". $oldStatusName : trans('general.unassigned');
$clean_meta['status_id']['new'] = $clean_meta['status_id']['new'] ? "[id: ".$clean_meta['status_id']['new']."] ". $newStatusName : trans('general.unassigned');
$clean_meta['Status'] = $clean_meta['status_id'];
unset($clean_meta['status_id']);
}
if(array_key_exists('asset_eol_date', $clean_meta)) {
$clean_meta['EOL date'] = $clean_meta['asset_eol_date'];
unset($clean_meta['asset_eol_date']);

View file

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

View file

@ -37,6 +37,7 @@ class AssetsTransformer
'name'=> e($asset->model->name),
] : null,
'byod' => ($asset->byod ? true : false),
'requestable' => ($asset->requestable ? true : false),
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'eol' => (($asset->asset_eol_date != '') && ($asset->purchase_date != '')) ? Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date).' months' : null,
@ -87,6 +88,7 @@ class AssetsTransformer
'purchase_date' => Helper::getFormattedDateObject($asset->purchase_date, 'date'),
'age' => $asset->purchase_date ? $asset->purchase_date->diffForHumans() : '',
'last_checkout' => Helper::getFormattedDateObject($asset->last_checkout, 'datetime'),
'last_checkin' => Helper::getFormattedDateObject($asset->last_checkin, 'datetime'),
'expected_checkin' => Helper::getFormattedDateObject($asset->expected_checkin, 'date'),
'purchase_cost' => Helper::formatCurrencyOutput($asset->purchase_cost),
'checkin_counter' => (int) $asset->checkin_counter,

View file

@ -26,6 +26,7 @@ class GroupsTransformer
'name' => e($group->name),
'permissions' => json_decode($group->permissions),
'users_count' => (int) $group->users_count,
'created_by' => ($group->admin) ? e($group->admin->present()->fullName) : null,
'created_at' => Helper::getFormattedDateObject($group->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($group->updated_at, 'datetime'),
];

View file

@ -29,6 +29,7 @@ class LicenseSeatsTransformer
'assigned_user' => ($seat->user) ? [
'id' => (int) $seat->user->id,
'name'=> e($seat->user->present()->fullName),
'email' => e($seat->user->email),
'department'=> ($seat->user->department) ?
[
'id' => (int) $seat->user->department->id,

View file

@ -27,8 +27,8 @@ class LicensesTransformer
'company' => ($license->company) ? ['id' => (int) $license->company->id, 'name'=> e($license->company->name)] : null,
'manufacturer' => ($license->manufacturer) ? ['id' => (int) $license->manufacturer->id, 'name'=> e($license->manufacturer->name)] : null,
'product_key' => (Gate::allows('viewKeys', License::class)) ? e($license->serial) : '------------',
'order_number' => e($license->order_number),
'purchase_order' => e($license->purchase_order),
'order_number' => ($license->order_number) ? e($license->order_number) : null,
'purchase_order' => ($license->purchase_order) ? e($license->purchase_order) : null,
'purchase_date' => Helper::getFormattedDateObject($license->purchase_date, 'date'),
'termination_date' => Helper::getFormattedDateObject($license->termination_date, 'date'),
'depreciation' => ($license->depreciation) ? ['id' => (int) $license->depreciation->id,'name'=> e($license->depreciation->name)] : null,
@ -38,8 +38,9 @@ class LicensesTransformer
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count,
'license_name' => e($license->license_name),
'license_email' => e($license->license_email),
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null,
'license_email' => ($license->license_email) ? e($license->license_email) : null,
'reassignable' => ($license->reassignable == 1) ? true : false,
'maintained' => ($license->maintained == 1) ? true : false,
'supplier' => ($license->supplier) ? ['id' => (int) $license->supplier->id, 'name'=> e($license->supplier->name)] : null,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,13 +58,20 @@ class CheckoutableListener
}
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event));
//slack doesn't include the url in its messaging format so this is needed to hit the endpoint
if(Setting::getSettings()->webhook_selected =='slack' || Setting::getSettings()->webhook_selected =='general') {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event));
}
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) {
Log::error("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
}
}
@ -107,15 +114,19 @@ class CheckoutableListener
$this->getCheckinNotification($event)
);
}
//slack doesn't include the url in its messaging format so this is needed to hit the endpoint
if(Setting::getSettings()->webhook_selected =='slack' || Setting::getSettings()->webhook_selected =='general') {
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event));
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event));
}
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) {
Log::error("Exception caught during checkin notification: " . $e->getMessage());
Log::warning("Exception caught during checkin notification: " . $e->getMessage());
}
}
@ -216,9 +227,10 @@ class CheckoutableListener
break;
case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class;
break;
break;
}
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
}

View file

@ -305,7 +305,7 @@ class Accessory extends SnipeModel
*/
public function requireAcceptance()
{
return $this->category->require_acceptance;
return $this->category->require_acceptance ?? false;
}
/**
@ -365,6 +365,35 @@ class Accessory extends SnipeModel
$accessory_user->limit(1)->delete();
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets a value for qty if no value is given. The database does not allow this
* field to be null, and in the other areas of the code, we set a default, but the importer
* does not.
*
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
{
$this->attributes['qty'] = (!$value) ? 0 : intval($value);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
/**
* Query builder scope to order on company
*

View file

@ -19,13 +19,26 @@ class Actionlog extends SnipeModel
{
use HasFactory;
// This is to manually set the source (via setActionSource()) for determineActionSource()
protected ?string $source = null;
protected $presenter = \App\Presenters\ActionlogPresenter::class;
use SoftDeletes;
use Presentable;
protected $table = 'action_logs';
public $timestamps = true;
protected $fillable = ['created_at', 'item_type', 'user_id', 'item_id', 'action_type', 'note', 'target_id', 'target_type', 'stored_eula'];
protected $fillable = [
'created_at',
'item_type',
'user_id',
'item_id',
'action_type',
'note',
'target_id',
'target_type',
'stored_eula'
];
use Searchable;
@ -34,7 +47,15 @@ class Actionlog extends SnipeModel
*
* @var array
*/
protected $searchableAttributes = ['action_type', 'note', 'log_meta','user_id'];
protected $searchableAttributes = [
'action_type',
'note',
'log_meta',
'user_id',
'remote_ip',
'user_agent',
'action_source'
];
/**
* The relations and their attributes that should be included when searching the model.
@ -248,6 +269,9 @@ class Actionlog extends SnipeModel
public function logaction($actiontype)
{
$this->action_type = $actiontype;
$this->remote_ip = request()->ip();
$this->user_agent = request()->header('User-Agent');
$this->action_source = $this->determineActionSource();
if ($this->save()) {
return true;
@ -312,4 +336,40 @@ class Actionlog extends SnipeModel
->orderBy('created_at', 'asc')
->get();
}
/**
* Determines what the type of request is so we can log it to the action_log
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.0
* @return string
*/
public function determineActionSource(): string
{
// This is a manually set source
if($this->source) {
return $this->source;
}
// This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
&& (starts_with(request()->header('authorization'), 'Bearer '))) {
return 'api';
}
// This is probably NOT an API call
if (request()->filled('_token')) {
return 'gui';
}
// We're not sure, probably cli
return 'cli/unknown';
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{
$this->source = $source;
}
}

View file

@ -89,24 +89,30 @@ class Asset extends Depreciable
];
protected $rules = [
'name' => 'max:255|nullable',
'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array',
'status_id' => 'required|integer|exists:status_labels,id',
'company_id' => 'integer|nullable',
'warranty_months' => 'numeric|nullable|digits_between:0,240',
'physical' => 'numeric|max:1|nullable',
'last_checkout' => 'date_format:Y-m-d H:i:s|nullable',
'expected_checkin' => 'date|nullable',
'location_id' => 'exists:locations,id|nullable',
'rtd_location_id' => 'exists:locations,id|nullable',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'purchase_date' => 'date|date_format:Y-m-d|nullable',
'serial' => 'unique_undeleted:assets,serial|nullable',
'purchase_cost' => 'numeric|nullable|gte:0',
'supplier_id' => 'exists:suppliers,id|nullable',
'asset_eol_date' => 'date|nullable',
'eol_explicit' => 'boolean|nullable',
'byod' => 'boolean',
'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array',
'status_id' => 'required|integer|exists:status_labels,id',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'name' => 'nullable|max:255',
'company_id' => 'nullable|integer|exists:companies,id',
'warranty_months' => 'nullable|numeric|digits_between:0,240',
'last_checkout' => 'nullable|date_format:Y-m-d H:i:s',
'last_checkin' => 'nullable|date_format:Y-m-d H:i:s',
'expected_checkin' => 'nullable|date',
'last_audit_date' => 'nullable|date_format:Y-m-d H:i:s',
'next_audit_date' => 'nullable|date|after:last_audit_date',
'location_id' => 'nullable|exists:locations,id',
'rtd_location_id' => 'nullable|exists:locations,id',
'purchase_date' => 'nullable|date|date_format:Y-m-d',
'serial' => 'nullable|unique_undeleted:assets,serial',
'purchase_cost' => 'nullable|numeric|gte:0',
'supplier_id' => 'nullable|exists:suppliers,id',
'asset_eol_date' => 'nullable|date',
'eol_explicit' => 'nullable|boolean',
'byod' => 'nullable|boolean',
'order_number' => 'nullable|string|max:191',
'notes' => 'nullable|string|max:65535',
'assigned_to' => 'nullable|integer',
'requestable' => 'nullable|boolean',
];
@ -164,6 +170,8 @@ class Asset extends Depreciable
'expected_checkin',
'next_audit_date',
'last_audit_date',
'last_checkin',
'last_checkout',
'asset_eol_date',
];
@ -266,7 +274,7 @@ class Asset extends Depreciable
/**
* Determines if an asset is available for checkout.
* This checks to see if the it's checked out to an invalid (deleted) user
* This checks to see if it's checked out to an invalid (deleted) user
* OR if the assigned_to and deleted_at fields on the asset are empty AND
* that the status is deployable
*
@ -460,13 +468,22 @@ class Asset extends Depreciable
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return bool
*/
public function checkedOutToUser()
public function checkedOutToUser(): bool
{
return $this->assignedType() === self::USER;
}
public function checkedOutToLocation(): bool
{
return $this->assignedType() === self::LOCATION;
}
public function checkedOutToAsset(): bool
{
return $this->assignedType() === self::ASSET;
}
/**
* Get the target this asset is checked out to
*
@ -728,7 +745,7 @@ class Asset extends Depreciable
{
$days = (is_null($days)) ? 30 : $days;
return self::where('archived', '=', '0')
return self::where('archived', '=', '0') // this can stay for right now, as `archived` defaults to 0 at the db level, but should probably be replaced with assetstatus->archived?
->whereNotNull('warranty_months')
->whereNotNull('purchase_date')
->whereNull('deleted_at')
@ -753,7 +770,7 @@ class Asset extends Depreciable
}
/**
* Establishes the asset -> status relationship
* Establishes the asset -> license seats relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
@ -921,6 +938,27 @@ class Asset extends Depreciable
return $cost;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets the requestable to a boolean 0 or 1. This accounts for forms or API calls that
* explicitly pass the requestable field but it has a null or empty value.
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
* @param $value
* @return void
*/
public function setRequestableAttribute($value)
{
$this->attributes['requestable'] = (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES
@ -1527,7 +1565,7 @@ class Asset extends Depreciable
*
* In short, this set of statements tells the query builder to ONLY query against an
* actual field that's being passed if it doesn't meet known relational fields. This
* allows us to query custom fields directly in the assetsv table
* allows us to query custom fields directly in the assets table
* (regardless of their name) and *skip* any fields that we already know can only be
* searched through relational searches that we do earlier in this method.
*

View file

@ -20,10 +20,9 @@ class AssetMaintenance extends Model implements ICompanyableChild
use SoftDeletes;
use CompanyableChildTrait;
use ValidatingTrait;
protected $casts = [
'start_date' => 'datetime',
'completion_date' => 'datetime',
];
protected $table = 'asset_maintenances';
protected $rules = [
'asset_id' => 'required|integer',
@ -31,12 +30,31 @@ class AssetMaintenance extends Model implements ICompanyableChild
'asset_maintenance_type' => 'required',
'title' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date',
'completion_date' => 'nullable|date',
'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable',
'notes' => 'string|nullable',
'cost' => 'numeric|nullable',
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'asset_id',
'supplier_id',
'asset_maintenance_type',
'is_warranty',
'start_date',
'completion_date',
'asset_maintenance_time',
'notes',
'cost',
];
use Searchable;
/**
@ -44,7 +62,15 @@ class AssetMaintenance extends Model implements ICompanyableChild
*
* @var array
*/
protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date'];
protected $searchableAttributes =
[
'title',
'notes',
'asset_maintenance_type',
'cost',
'start_date',
'completion_date'
];
/**
* The relations and their attributes that should be included when searching the model.
@ -52,8 +78,11 @@ class AssetMaintenance extends Model implements ICompanyableChild
* @var array
*/
protected $searchableRelations = [
'asset' => ['name', 'asset_tag'],
'asset' => ['name', 'asset_tag', 'serial'],
'asset.model' => ['name', 'model_number'],
'asset.supplier' => ['name'],
'asset.assetstatus' => ['name'],
'supplier' => ['name'],
];
public function getCompanyableParents()
@ -177,6 +206,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
->orderBy('suppliers_maintenances.name', $order);
}
/**
* Query builder scope to order on admin user
*
@ -219,4 +249,33 @@ class AssetMaintenance extends Model implements ICompanyableChild
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order);
}
/**
* Query builder scope to order on serial
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderByAssetSerial($query, $order)
{
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order);
}
/**
* Query builder scope to order on status label name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderStatusName($query, $order)
{
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order);
}
}

View file

@ -46,15 +46,6 @@ class AssetModel extends SnipeModel
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
public function setEolAttribute($value)
{
if ($value == '') {
$value = 0;
}
$this->attributes['eol'] = $value;
}
/**
* The attributes that are mass assignable.
*

View file

@ -81,26 +81,6 @@ final class Company extends SnipeModel
}
}
/**
* Scoping table queries, determining if a logged in user is part of a company, and only allows
* that user to see items associated with that company
*/
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
if (Auth::user()) {
$company_id = Auth::user()->company_id;
} else {
$company_id = null;
}
$table = ($table_name) ? $table_name."." : $query->getModel()->getTable().".";
if (\Schema::hasColumn($query->getModel()->getTable(), $column)) {
return $query->where($table.$column, '=', $company_id);
} else {
return $query->join('users as users_comp', 'users_comp.id', 'user_id')->where('users_comp.company_id', '=', $company_id);
}
}
public static function getIdFromInput($unescaped_input)
{
@ -113,6 +93,14 @@ final class Company extends SnipeModel
}
}
/**
* Get the company id for the current user taking into
* account the full multiple company support setting
* and if the current user is a super user.
*
* @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForCurrentUser($unescaped_input)
{
if (! static::isFullMultipleCompanySupportEnabled()) {
@ -133,25 +121,49 @@ final class Company extends SnipeModel
}
}
/**
* Check to see if the current user should have access to the model.
* I hate this method and I think it should be refactored.
*
* @param $companyable
* @return bool|void
*/
public static function isCurrentUserHasAccess($companyable)
{
// When would this even happen tho??
if (is_null($companyable)) {
return false;
} elseif (! static::isFullMultipleCompanySupportEnabled()) {
return true;
} elseif (!$companyable instanceof Company && !\Schema::hasColumn($companyable->getModel()->getTable(), 'company_id')) {
// This is primary for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before()
return true;
} else {
if (Auth::user()) {
$current_user_company_id = Auth::user()->company_id;
$companyable_company_id = $companyable->company_id;
}
return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser();
// If FMCS is not enabled, everyone has access, return true
if (! static::isFullMultipleCompanySupportEnabled()) {
return true;
}
// Again, where would this happen? But check that $companyable is not a string
if (!is_string($companyable)) {
$company_table = $companyable->getModel()->getTable();
try {
// This is primary for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before()
if (!$companyable instanceof Company && !\Schema::hasColumn($company_table, 'company_id')) {
return true;
}
} catch (\Exception $e) {
\Log::warning($e);
}
}
if (Auth::user()) {
\Log::warning('Companyable is '.$companyable);
$current_user_company_id = Auth::user()->company_id;
$companyable_company_id = $companyable->company_id;
return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser();
}
}
public static function isCurrentUserAuthorized()
@ -182,6 +194,10 @@ final class Company extends SnipeModel
&& ($this->users()->count() === 0);
}
/**
* @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForUser($unescaped_input)
{
if (! static::isFullMultipleCompanySupportEnabled() || Auth::user()->isSuperUser()) {
@ -191,38 +207,6 @@ final class Company extends SnipeModel
}
}
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
{
// If not logged in and hitting this, assume we are on the command line and don't scope?'
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser()) || (! Auth::check())) {
return $query;
} else {
return static::scopeCompanyablesDirectly($query, $column, $table_name);
}
}
public static function scopeCompanyableChildren(array $companyable_names, $query)
{
if (count($companyable_names) == 0) {
throw new Exception('No Companyable Children to scope');
} elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser())) {
return $query;
} else {
$f = function ($q) {
static::scopeCompanyablesDirectly($q);
};
$q = $query->where(function ($q) use ($companyable_names, $f) {
$q2 = $q->whereHas($companyable_names[0], $f);
for ($i = 1; $i < count($companyable_names); $i++) {
$q2 = $q2->orWhereHas($companyable_names[$i], $f);
}
});
return $q;
}
}
public function users()
{
@ -253,4 +237,98 @@ final class Company extends SnipeModel
{
return $this->hasMany(Component::class, 'company_id');
}
/**
* START COMPANY SCOPING FOR FMCS
*/
/**
* Scoping table queries, determining if a logged in user is part of a company, and only allows the user to access items associated with that company if FMCS is enabled.
*
* This method is the one that the CompanyableTrait uses to contrain queries automatically, however that trait CANNOT be
* applied to the user's model, since it causes an infinite loop against the authenticated user.
*
* @todo - refactor that trait to handle the user's model as well.
*
* @author [A. Gianotto] <snipe@snipe.net>
* @param $query
* @param $column
* @param $table_name
* @return mixed
*/
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
{
// If not logged in and hitting this, assume we are on the command line and don't scope?'
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser()) || (! Auth::check())) {
return $query;
} else {
\Log::debug('Fire scopeCompanyablesDirectly.');
return static::scopeCompanyablesDirectly($query, $column, $table_name);
}
}
/**
* Scoping table queries, determining if a logged in user is part of a company, and only allows
* that user to see items associated with that company
*/
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
// Get the company ID of the logged in user, or set it to null if there is no company assicoated with the user
if (Auth::user()) {
\Log::debug('Admin company is: '.Auth::user()->company_id);
$company_id = Auth::user()->company_id;
} else {
$company_id = null;
}
// Dynamically get the table name if it's not passed in, based on the model we're querying against
$table = ($table_name) ? $table_name."." : $query->getModel()->getTable().".";
\Log::debug('Model is: '.$query->getModel());
\Log::debug('Table is: '.$table);
// If the column exists in the table, use it to scope the query
if (\Schema::hasColumn($query->getModel()->getTable(), $column)) {
return $query->where($table.$column, '=', $company_id);
} else {
return $query->join('users as users_comp', 'users_comp.id', 'user_id')->where('users_comp.company_id', '=', $company_id);
}
}
/**
* I legit do not know what this method does, but we can't remove it (yet).
*
* This gets invoked by CompanyableChildScope, but I'm not sure what it does.
*
* @author [A. Gianotto] <snipe@snipe.net>
* @param array $companyable_names
* @param $query
* @return mixed
*/
public static function scopeCompanyableChildren(array $companyable_names, $query)
{
\Log::debug('Company Names in scopeCompanyableChildren: '.print_r($companyable_names, true));
if (count($companyable_names) == 0) {
throw new Exception('No Companyable Children to scope');
} elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser())) {
return $query;
} else {
$f = function ($q) {
\Log::debug('scopeCompanyablesDirectly firing ');
static::scopeCompanyablesDirectly($q);
};
$q = $query->where(function ($q) use ($companyable_names, $f) {
$q2 = $q->whereHas($companyable_names[0], $f);
for ($i = 1; $i < count($companyable_names); $i++) {
$q2 = $q2->orWhereHas($companyable_names[$i], $f);
}
});
return $q;
}
}
}

View file

@ -5,8 +5,13 @@ namespace App\Models;
trait CompanyableTrait
{
/**
* Boot the companyable trait for a model.
* This trait is used to scope models to the current company. To use this scope on companyable models,
* we use the "use Companyable;" statement at the top of the mode.
*
* We CANNOT USE THIS ON USERS, as it causes an infinite loop and prevents users from logging in, since this scope will be
* applied to the currently logged in (or logging in) user in addition to the user model for viewing lists of users.
*
* @see \App\Models\Company\Company::scopeCompanyables()
* @return void
*/
public static function bootCompanyableTrait()

View file

@ -224,6 +224,37 @@ class Component extends SnipeModel
return $this->qty - $this->numCheckedOut();
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets a value for qty if no value is given. The database does not allow this
* field to be null, and in the other areas of the code, we set a default, but the importer
* does not.
*
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
{
$this->attributes['qty'] = (!$value) ? 0 : intval($value);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
/**
* Query builder scope to order on company
*

View file

@ -339,6 +339,35 @@ class Consumable extends SnipeModel
return $remaining;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets a value for qty if no value is given. The database does not allow this
* field to be null, and in the other areas of the code, we set a default, but the importer
* does not.
*
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
{
$this->attributes['qty'] = (!$value) ? 0 : intval($value);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
/**
* Query builder scope to order on company
*

View file

@ -5,6 +5,8 @@ namespace App\Models;
use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model
@ -92,8 +94,19 @@ class CustomFieldset extends Model
array_push($rule, $field->attributes['format']);
$rules[$field->db_column_name()] = $rule;
//add not_array to rules for all fields
$rules[$field->db_column_name()][] = 'not_array';
// add not_array to rules for all fields but checkboxes
if ($field->element != 'checkbox') {
$rules[$field->db_column_name()][] = 'not_array';
}
if ($field->element == 'checkbox') {
$rules[$field->db_column_name()][] = 'checkboxes';
}
if ($field->element == 'radio') {
$rules[$field->db_column_name()][] = 'radio_buttons';
}
}
return $rules;

View file

@ -58,6 +58,18 @@ class Group extends SnipeModel
return $this->belongsToMany(\App\Models\User::class, 'users_groups');
}
/**
* Get the user that created the group
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function admin()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
/**
* Decode JSON permissions into array
*

View file

@ -160,75 +160,27 @@ class DefaultLabel extends RectangleSheet
$textY += $this->textSize + self::TEXT_MARGIN;
}
// Fields
// Render the selected fields with their labels
$fieldsDone = 0;
if ($settings->labels_display_name && $fieldsDone < $this->getSupportFields()) {
if ($asset->name) {
static::writeText(
$pdf, 'N: '.$asset->name,
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
if ($settings->labels_display_company_name && $fieldsDone < $this->getSupportFields()) {
if ($asset->company) {
static::writeText(
$pdf, 'C: '.$asset->company->name,
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
if ($settings->labels_display_tag && $fieldsDone < $this->getSupportFields()) {
if ($asset->asset_tag) {
static::writeText(
$pdf, 'T: '.$asset->asset_tag,
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
if ($settings->labels_display_serial && $fieldsDone < $this->getSupportFields()) {
if ($asset->serial) {
static::writeText(
$pdf, 'S: '.$asset->serial,
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
if ($settings->labels_display_model && $fieldsDone < $this->getSupportFields()) {
if ($asset->model) {
static::writeText(
$pdf, 'M: '.$asset->model->name,
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
if ($fieldsDone < $this->getSupportFields()) {
foreach ($record->get('fields') as $field) {
// Actually write the selected fields and their matching values
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$textX1, $textY,
'freesans', '', $this->textSize, 'L',
$textW, $this->textSize,
true, 0
);
$textY += $this->textSize + self::TEXT_MARGIN;
$fieldsDone++;
}
}
}
}
?>

View file

@ -18,8 +18,14 @@ class FieldOption {
// assignedTo directly on the asset is a special case where
// we want to avoid returning the property directly
// and instead return the entity's presented name.
if ($dataPath[0] === 'assignedTo'){
return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null;
if ($dataPath[0] === 'assignedTo') {
if ($asset->relationLoaded('assignedTo')) {
// If the "assignedTo" relationship was eager loaded then the way to get the
// relationship changes from $asset->assignedTo to $asset->assigned.
return $asset->assigned ? $asset->assigned->present()->fullName() : null;
}
return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null;
}
return $dataPath->reduce(function ($myValue, $path) {

View file

@ -0,0 +1,89 @@
<?php
namespace App\Models\Labels\Tapes\Dymo;
class LabelWriter_1933081 extends LabelWriter
{
private const BARCODE_MARGIN = 1.80;
private const TAG_SIZE = 2.80;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.80;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
public function getUnit() { return 'mm'; }
public function getWidth() { return 89; }
public function getHeight() { return 25; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 5; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$barcodeSize = $pa->h - self::TAG_SIZE;
if ($record->has('barcode2d')) {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
} else {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'R',
$usableWidth, self::TAG_SIZE, true, 0
);
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'b', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$currentX, $currentY,
'freesans', '', self::FIELD_SIZE, 'L',
$usableWidth, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace App\Models\Labels\Tapes\Dymo;
class LabelWriter_2112283 extends LabelWriter
{
private const BARCODE_MARGIN = 1.80;
private const TAG_SIZE = 2.80;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.80;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
public function getUnit() { return 'mm'; }
public function getWidth() { return 54; }
public function getHeight() { return 25; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 5; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$barcodeSize = $pa->h - self::TAG_SIZE;
if ($record->has('barcode2d')) {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
} else {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'R',
$usableWidth, self::TAG_SIZE, true, 0
);
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'b', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$currentX, $currentY,
'freesans', '', self::FIELD_SIZE, 'L',
$usableWidth, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View file

@ -86,5 +86,13 @@ class LabelWriter_30252 extends LabelWriter
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View file

@ -53,6 +53,7 @@ class License extends Depreciable
'purchase_date' => 'date_format:Y-m-d|nullable|max:10',
'expiration_date' => 'date_format:Y-m-d|nullable|max:10',
'termination_date' => 'date_format:Y-m-d|nullable|max:10',
'min_amt' => 'numeric|nullable|gte:0',
];
/**
@ -81,6 +82,7 @@ class License extends Depreciable
'supplier_id',
'termination_date',
'user_id',
'min_amt',
];
use Searchable;

View file

@ -126,6 +126,7 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild
{
return $query->leftJoin('users as license_seat_users', 'license_seats.assigned_to', '=', 'license_seat_users.id')
->leftJoin('departments as license_user_dept', 'license_user_dept.id', '=', 'license_seat_users.department_id')
->whereNotNull('license_seats.assigned_to')
->orderBy('license_user_dept.name', $order);
}
}

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