Compare commits

..

57 Commits

Author SHA1 Message Date
snyk-bot
61cf9dcfe0 fix: upgrade @tiptap/extension-highlight from 2.1.16 to 2.2.4
Snyk has created this PR to upgrade @tiptap/extension-highlight from 2.1.16 to 2.2.4.

See this package in npm:
https://www.npmjs.com/package/@tiptap/extension-highlight

See this project in Snyk:
https://app.snyk.io/org/nomadjimbob/project/66c7b29c-3171-4fcc-b6f1-0bc65698880c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-04-17 00:44:04 +00:00
5fbca80a3c disable Intervention\Image\ImageServiceProvider::class 2024-04-04 20:59:59 +10:00
4fa1410790 dependency updates 2024-04-04 20:33:31 +10:00
411fc5f37f dependency updates 2024-04-04 20:22:09 +10:00
f7c20719f7 dependency updates 2024-04-04 20:18:43 +10:00
e5a7aeede8 Merge pull request #431 from STEMMechanics/dependabot/npm_and_yarn/typescript-eslint/parser-6.19.1 2024-01-26 19:48:06 +10:00
dependabot[bot]
cfbf3a4033 Bump @typescript-eslint/parser from 6.19.0 to 6.19.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.19.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:47:25 +00:00
89cda7c415 Merge pull request #436 from STEMMechanics/dependabot/composer/doctrine/dbal-3.8.0 2024-01-26 19:47:21 +10:00
e211436675 Merge pull request #435 from STEMMechanics/dependabot/npm_and_yarn/dotenv-16.4.1 2024-01-26 19:47:09 +10:00
d1be96a1df Merge pull request #434 from STEMMechanics/dependabot/composer/laravel/sail-1.27.2 2024-01-26 19:46:59 +10:00
dependabot[bot]
7e87312d99 Bump laravel/sail from 1.27.1 to 1.27.2
Bumps [laravel/sail](https://github.com/laravel/sail) from 1.27.1 to 1.27.2.
- [Release notes](https://github.com/laravel/sail/releases)
- [Changelog](https://github.com/laravel/sail/blob/1.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/sail/compare/v1.27.1...v1.27.2)

---
updated-dependencies:
- dependency-name: laravel/sail
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:45:36 +00:00
dependabot[bot]
73c277acfb Bump dotenv from 16.3.1 to 16.4.1
Bumps [dotenv](https://github.com/motdotla/dotenv) from 16.3.1 to 16.4.1.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.3.1...v16.4.1)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 09:45:31 +00:00
0728233206 Merge pull request #430 from STEMMechanics/dependabot/npm_and_yarn/vitejs/plugin-vue-5.0.3 2024-01-26 19:45:07 +10:00
b127ce0514 Merge pull request #429 from STEMMechanics/dependabot/npm_and_yarn/knip-4.2.1 2024-01-26 19:44:57 +10:00
5924222296 Merge pull request #428 from STEMMechanics/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.19.1 2024-01-26 19:44:45 +10:00
6d2c601b5d Merge pull request #427 from STEMMechanics/dependabot/composer/laravel/pint-1.13.10 2024-01-26 19:43:32 +10:00
f2fb993596 Merge pull request #426 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.9 2024-01-26 19:43:08 +10:00
c40232099f Merge pull request #425 from STEMMechanics/dependabot/composer/intervention/image-3.3.1 2024-01-26 19:42:48 +10:00
dependabot[bot]
17d33a9ad2 Bump doctrine/dbal from 3.7.2 to 3.8.0
Bumps [doctrine/dbal](https://github.com/doctrine/dbal) from 3.7.2 to 3.8.0.
- [Release notes](https://github.com/doctrine/dbal/releases)
- [Commits](https://github.com/doctrine/dbal/compare/3.7.2...3.8.0)

---
updated-dependencies:
- dependency-name: doctrine/dbal
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-26 00:25:53 +00:00
dependabot[bot]
e380bafbb0 Bump @vitejs/plugin-vue from 4.6.2 to 5.0.3
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 4.6.2 to 5.0.3.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@5.0.3/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:50 +00:00
dependabot[bot]
4c63e68625 Bump knip from 4.2.0 to 4.2.1
Bumps [knip](https://github.com/webpro/knip/tree/HEAD/packages/knip) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/webpro/knip/releases)
- [Commits](https://github.com/webpro/knip/commits/4.2.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:39 +00:00
dependabot[bot]
8b5d150fe2 Bump @typescript-eslint/eslint-plugin from 6.19.0 to 6.19.1
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.19.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:33:30 +00:00
dependabot[bot]
10808f3622 Bump laravel/pint from 1.13.9 to 1.13.10
Bumps [laravel/pint](https://github.com/laravel/pint) from 1.13.9 to 1.13.10.
- [Release notes](https://github.com/laravel/pint/releases)
- [Changelog](https://github.com/laravel/pint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/laravel/pint/compare/v1.13.9...v1.13.10)

---
updated-dependencies:
- dependency-name: laravel/pint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:26 +00:00
dependabot[bot]
37ba8f0f29 Bump phpunit/phpunit from 10.5.8 to 10.5.9
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.8 to 10.5.9.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.9/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.8...10.5.9)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:18 +00:00
dependabot[bot]
51fb0150d9 Bump intervention/image from 3.3.0 to 3.3.1
Bumps [intervention/image](https://github.com/Intervention/image) from 3.3.0 to 3.3.1.
- [Commits](https://github.com/Intervention/image/compare/3.3.0...3.3.1)

---
updated-dependencies:
- dependency-name: intervention/image
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 00:27:11 +00:00
James Collins
03dd12cea1 Merge pull request #423 from STEMMechanics/dependabot/npm_and_yarn/vitest-1.2.1
Bump vitest from 0.34.6 to 1.2.1
2024-01-22 12:08:26 +10:00
dependabot[bot]
56ae719a2e Bump vitest from 0.34.6 to 1.2.1
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.6 to 1.2.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.2.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 02:07:55 +00:00
James Collins
68b7e50b9e Merge pull request #422 from STEMMechanics/dependabot/npm_and_yarn/eslint-plugin-jsdoc-48.0.2
Bump eslint-plugin-jsdoc from 46.10.1 to 48.0.2
2024-01-22 12:07:27 +10:00
James Collins
386e2009c4 Merge pull request #421 from STEMMechanics/dependabot/npm_and_yarn/knip-4.2.0
Bump knip from 2.43.0 to 4.2.0
2024-01-22 12:07:15 +10:00
James Collins
3da55bca81 Merge pull request #420 from STEMMechanics/dependabot/npm_and_yarn/vite-5.0.12
Bump vite from 4.5.1 to 5.0.12
2024-01-22 12:06:58 +10:00
dependabot[bot]
5d8f1457d4 Bump vite from 4.5.1 to 5.0.12
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 5.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:42:15 +00:00
James Collins
39d9ebc549 Merge pull request #419 from STEMMechanics/dependabot/composer/intervention/image-3.3.0
Bump intervention/image from 2.7.2 to 3.3.0
2024-01-22 11:41:32 +10:00
James Collins
b2f3664909 Merge pull request #418 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.8
Bump phpunit/phpunit from 10.5.7 to 10.5.8
2024-01-22 11:41:15 +10:00
James Collins
06c2f0e3d0 Merge pull request #417 from STEMMechanics/dependabot/npm_and_yarn/vite-4.5.2
Bump vite from 4.5.1 to 4.5.2
2024-01-22 11:40:54 +10:00
dependabot[bot]
2d2392c8ae Bump intervention/image from 2.7.2 to 3.3.0
Bumps [intervention/image](https://github.com/Intervention/image) from 2.7.2 to 3.3.0.
- [Commits](https://github.com/Intervention/image/compare/2.7.2...3.3.0)

---
updated-dependencies:
- dependency-name: intervention/image
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:22:46 +00:00
dependabot[bot]
5e7061bc61 Bump vite from 4.5.1 to 4.5.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:20:29 +00:00
James Collins
8451a2bd4b Merge pull request #415 from STEMMechanics/dependabot/npm_and_yarn/prettier-3.2.4
Bump prettier from 3.0.3 to 3.2.4
2024-01-22 11:11:23 +10:00
James Collins
dd135f10ec Merge pull request #413 from STEMMechanics/dependabot/composer/square/square-34.0.1.20240118
Bump square/square from 32.0.0.20231018 to 34.0.1.20240118
2024-01-22 11:11:00 +10:00
dependabot[bot]
247eb03cea Bump eslint-plugin-jsdoc from 46.10.1 to 48.0.2
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 46.10.1 to 48.0.2.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v46.10.1...v48.0.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:49 +00:00
dependabot[bot]
19c16cfbf1 Bump square/square from 32.0.0.20231018 to 34.0.1.20240118
Bumps [square/square](https://github.com/square/square-php-sdk) from 32.0.0.20231018 to 34.0.1.20240118.
- [Release notes](https://github.com/square/square-php-sdk/releases)
- [Changelog](https://github.com/square/square-php-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/square-php-sdk/compare/32.0.0.20231018...34.0.1.20240118)

---
updated-dependencies:
- dependency-name: square/square
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:22 +00:00
dependabot[bot]
456687edc5 Bump knip from 2.43.0 to 4.2.0
Bumps [knip](https://github.com/webpro/knip/tree/HEAD/packages/knip) from 2.43.0 to 4.2.0.
- [Release notes](https://github.com/webpro/knip/releases)
- [Commits](https://github.com/webpro/knip/commits/4.2.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:10:18 +00:00
James Collins
aab63bb627 Merge pull request #397 from STEMMechanics/dependabot/npm_and_yarn/unocss-0.58.3
Bump unocss from 0.57.7 to 0.58.3
2024-01-22 11:06:09 +10:00
dependabot[bot]
efb3cac129 Bump unocss from 0.57.7 to 0.58.3
Bumps [unocss](https://github.com/unocss/unocss) from 0.57.7 to 0.58.3.
- [Release notes](https://github.com/unocss/unocss/releases)
- [Commits](https://github.com/unocss/unocss/compare/v0.57.7...v0.58.3)

---
updated-dependencies:
- dependency-name: unocss
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 01:03:28 +00:00
James Collins
36314339fb Merge pull request #361 from STEMMechanics/dependabot/composer/guzzlehttp/guzzle-7.8.1
Bump guzzlehttp/guzzle from 7.8.0 to 7.8.1
2024-01-22 10:49:10 +10:00
dependabot[bot]
c6247e445f Bump phpunit/phpunit from 10.5.7 to 10.5.8
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.7 to 10.5.8.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.8/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.7...10.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 00:44:40 +00:00
b028856eb5 php 8.2 now required 2024-01-19 10:04:51 +10:00
1925b2ef0c dependency updates 2024-01-19 09:53:52 +10:00
dependabot[bot]
0d1bd4522e Bump prettier from 3.0.3 to 3.2.4
Bumps [prettier](https://github.com/prettier/prettier) from 3.0.3 to 3.2.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.0.3...3.2.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-18 00:55:39 +00:00
f2e84b63fa dependency updates 2024-01-15 09:02:21 +10:00
ac6257ed6d vs settings change 2024-01-15 09:02:16 +10:00
2486dec824 added rel="nofollow" to download links 2024-01-15 09:01:36 +10:00
04e6c0d0fc fix bad param 2024-01-06 15:57:46 +10:00
b948c42fe2 check if file_security is missing 2024-01-06 15:57:39 +10:00
5d7be1a482 dependency updates 2024-01-06 15:57:22 +10:00
4e81d06a6e dep updates 2023-12-25 19:23:46 +10:00
dependabot[bot]
0f2400ff2b Bump guzzlehttp/guzzle from 7.8.0 to 7.8.1
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/7.8/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-04 00:54:13 +00:00
7ed2332a3e dependency updates 2023-12-01 10:52:49 +10:00
373 changed files with 74837 additions and 1052 deletions

View File

@@ -3,6 +3,7 @@ APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_URL_API="${APP_URL}/api/"
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
@@ -29,7 +30,7 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
@@ -37,11 +38,24 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
AWS_PUBLIC_ACCESS_KEY_ID=
AWS_PUBLIC_SECRET_ACCESS_KEY=
AWS_PUBLIC_DEFAULT_REGION="us-west-002"
AWS_PUBLIC_BUCKET=
AWS_PUBLIC_USE_PATH_STYLE_ENDPOINT=false
AWS_PUBLIC_ENDPOINT=
AWS_PUBLIC_URL=
AWS_PRIVATE_ACCESS_KEY_ID=
AWS_PRIVATE_SECRET_ACCESS_KEY=
AWS_PRIVATE_DEFAULT_REGION="us-west-002"
AWS_PRIVATE_BUCKET=
AWS_PRIVATE_USE_PATH_STYLE_ENDPOINT=false
AWS_PRIVATE_ENDPOINT=
AWS_PRIVATE_URL=
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_API_KEY=
PUSHER_APP_ID=
PUSHER_APP_KEY=
@@ -51,7 +65,6 @@ PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"

59
.env.testing Normal file
View File

@@ -0,0 +1,59 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
APP_DEBUG=true
APP_URL=http://127.0.0.1
APP_URL_API="${APP_URL}/api/"
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
BROADCAST_DRIVER=log
CACHE_DRIVER=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=null
MAIL_PORT=null
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
CONTACT_ADDRESS="hello@stemmechanics.com.au"
CONTACT_SUBJECT="Contact from website"
STORAGE_LOCAL_URL="${APP_URL}/api/media/%ID%/download"
STORAGE_PUBLIC_URL="${APP_URL}/uploads/%NAME%"

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
.github/
.vscode/
vendor/

22
.eslintrc.js Normal file
View File

@@ -0,0 +1,22 @@
module.exports = {
env: {
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"prettier",
"plugin:jsdoc/recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"vue/multi-word-component-names": "off",
indent: ["off", 4, { ignoredNodes: ["ConditionalExpression"] }],
"@typescript-eslint/no-inferrable-types": "off",
},
plugins: ["jsdoc", "@typescript-eslint"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
};

2
.gitattributes vendored
View File

@@ -1,4 +1,4 @@
* text=auto eol=lf
* text=auto
*.blade.php diff=html
*.css diff=css

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"

42
.github/workflows/laravel.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Laravel
on:
push:
branches: ["main"]
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
- uses: actions/checkout@v3
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
run: composer install -q --no-interaction --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: vendor/bin/phpunit
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: "16.x"
- name: Install dependencies
run: npm ci
- name: Run Vue tests
env:
LARAVEL_BYPASS_ENV_CHECK: "1"
run: npm run test

272
.gitignore vendored
View File

@@ -1,20 +1,258 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
### Composer ###
composer.phar
/vendor/
# composer.lock
### Laravel ###
node_modules/
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
# Laravel 4 specific
bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
# Laravel 5 & Lumen specific with changed public path
public_html/storage
public_html/hot
storage/*.key
.env
Homestead.yaml
Homestead.json
/.vagrant
/.phpunit.cache
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Vue ###
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
### Vuejs ###
# Recommended template: Node.gitignore
dist/
### This Project ###
/public/uploads
/public/build
*.key
### TinyMCE ###
/public/tinymce
!/public/tinymce/skins/ui/stemmech/
### Synk ###
.dccache
### TempCodeRunner ###
tempCodeRunnerFile.*
### PHPUnit ###
.phpunit.result.cache
.gitignore
### Codesniffer ###
phpcs.phar
phpcbf.phar

14
.ls-lint.yml Normal file
View File

@@ -0,0 +1,14 @@
ls:
resources/js/store:
.ts: pascalcase
"*":
.js: camelcase
.ts: camelcase
.vue: pascalcase
.dir: snakecase
.type.ts: camelcase
ignore:
- node_modules
- vendor
- public/build
- public/tinymce

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"bracketSameLine": true,
"tabWidth": 4,
"htmlWhitespaceSensitivity": "css"
}

20
.vscode/settings.json vendored
View File

@@ -1,5 +1,19 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
"editor.formatOnType": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[php]": {
// "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
"editor.defaultFormatter": "wongjn.php-sniffer"
},
"cSpell.words": ["TIMESTAMPDIFF"]
}

View File

@@ -1,7 +1,7 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
@@ -31,23 +31,23 @@ If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Lar
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[OP.GG](https://op.gg)**
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
- **[Lendio](https://lendio.com)**
## Contributing

23355
_ide_helper.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class AnalyticsConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\AnalyticsSession::class;
/**
* The default includes to include in a request.
*
* @var array
*/
protected $includes = ['requests.type','requests.path'];
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
return true;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace App\Conductors;
use App\Models\Media;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use LogicException;
class ArticleConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Article::class;
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['attachments', 'user', 'gallery'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/articles') === false) {
$builder
->where('publish_at', '<=', now());
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (Carbon::parse($model->publish_at)->isFuture() === true) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/articles') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/articles') === true);
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
/**
* Include Attachments Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeAttachments(Model $model)
{
return $model->getAttachments()->map(function ($attachment) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
});
}
/**
* Include Gallery Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeGallery(Model $model)
{
return $model->getGallery()->map(function ($item) {
return MediaConductor::includeModel(request(), 'gallery', $item->media);
});
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
$cacheKey = "user:{$model['user_id']}";
$user = Cache::remember($cacheKey, now()->addDays(28), function () use ($model) {
return User::find($model['user_id']);
});
return UserConductor::includeModel(request(), 'user', $user);
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array|null The new value.
*/
public function transformHero(mixed $value): array|null
{
$cacheKey = "media:{$value}";
$media = Cache::remember($cacheKey, now()->addDays(28), function () use ($value) {
return Media::find($value);
});
return MediaConductor::includeModel(request(), 'hero', $media);
}
}

1055
app/Conductors/Conductor.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Conductors;
use App\Models\Media;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class EventConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Event::class;
/**
* The default sorting field
* @var string
*/
protected $sort = '-start_at';
/**
* The included fields
* @var string[]
*/
protected $includes = ['attachments'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
$builder
->where('status', '!=', 'draft')
->where('publish_at', '<=', now());
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Include Attachments Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeAttachments(Model $model)
{
/** @var \App\Models\User */
$user = auth()->user();
return $model->getAttachments()->map(function ($attachment) use ($user) {
if ($attachment->private === false || ($user !== null && $user->hasPermission('admin/events') === true)) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
}
});
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array|null The new value.
*/
public function transformHero(mixed $value): array|null
{
$cacheKey = "media:{$value}";
$media = Cache::remember($cacheKey, now()->addDays(28), function () use ($value) {
return Media::find($value);
});
return MediaConductor::includeModel(request(), 'hero', $media);
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Conductors;
use App\Models\MediaJob;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class MediaConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Media::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user', 'jobs'];
/**
* The default filters to use in a request.
*
* @var array
*/
// protected $defaultFilters = [
// 'status' => 'OK'
// ];
/**
* Return an array of model fields visible to the current user.
*
* @param Model $model The model in question.
* @return array The array of field names.
*/
public function fields(Model $model): array
{
$fields = parent::fields($model);
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/media') === false) {
$fields = arrayRemoveItem($fields, ['security_data', 'storage']);
}
return $fields;
}
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder): void
{
$user = auth()->user();
if ($user === null) {
$builder->where('security_type', '')
->orWhere('security_type', 'password');
} else {
$builder->where(function ($query) use ($user) {
$query->where('security_type', '')
->orWhere('security_type', 'password')
->orWhere(function ($subquery) use ($user) {
$subquery->where('security_type', 'permission')
->whereIn('security_data', $user->permissions);
});
});
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (strcasecmp('permission', $model->security_type) === 0) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission($model->security_data) === false) {
return false;
}
} elseif ($model->security_type !== '' && strcasecmp('password', $model->security_type) !== 0) {
return false;
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
$user = auth()->user();
return ($user !== null);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 ||
$user->hasPermission('admin/media') === true));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
$user = Cache::remember("user:{$model['user_id']}", now()->addDays(28), function () use ($model) {
return User::find($model['user_id']);
});
return UserConductor::includeModel(request(), 'user', $user);
}
/**
* Include job models in Media
*
* @param Model $model The reference model.
* @return mixed
*/
public function includeJobs(Model $model)
{
$jobs = $model->jobs()
->select(['id','created_at','updated_at','user_id','status','status_text','progress'])
->orderBy('created_at', 'desc')->get();
return $jobs;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Conductors;
use App\Models\MediaJob;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class MediaJobConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\MediaJob::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user'];
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
return false;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
return false;
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
return false;
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class ShortlinkConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Shortlink::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class SubscriptionConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Subscription::class;
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && (
(strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) ||
$user->hasPermission('admin/subscriptions') === true
));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 &&
$user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true));
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class UserConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\User::class;
/**
* Return the visible API fields.
*
* @param Model $model The model.
* @return string[] The fields visible.
*/
public function fields(Model $model): array
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'display_name'];
}
return parent::fields($model);
}
/**
* Transform the passed Model to an array
*
* @param Model $model The model to transform.
* @return array The transformed model.
*/
public function transform(Model $model): array
{
/** @var \App\Models\User */
$user = auth()->user();
$data = $model->toArray();
$limit = $this->fields($model);
if (
$user === null || (
$user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0
)
) {
$limit = ['id', 'display_name'];
} else {
$data['permissions'] = $user->permissions;
}
$data = arrayLimitKeys($data, $limit);
return $data;
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user !== null) {
return ($user->hasPermission('admin/users') === true || strcasecmp($user->id, $model->id) === 0);
}
return false;
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/users') === true);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanupTempFiles extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:cleanup-temp-files';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete temporary files that are older that 1 day';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$keepTime = (1 * 24 * 60 * 60); // 1 Day
$currentTimeStamp = time();
$deletedFileCount = 0;
foreach (glob(storage_path('app/tmp/*')) as $filename) {
$fileModifiedTimeStamp = filemtime($filename);
if (($currentTimeStamp - $fileModifiedTimeStamp) > $keepTime) {
unlink($filename);
$deletedFileCount++;
}
}
$this->comment('Deleted ' . $deletedFileCount . ' files');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use App\Models\MediaJob;
use Illuminate\Console\Command;
class RemoveStaleMediaJobs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:remove-stale-media-jobs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove media_jobs that have not been modified for 48 hours';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$threshold = now()->subHours(48);
$staleJobs = MediaJob::where('updated_at', '<=', $threshold)->get();
foreach ($staleJobs as $job) {
$job->delete();
}
$this->info(count($staleJobs) . ' stale media_jobs removed.');
}
}

View File

@@ -9,18 +9,25 @@ class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule The schedule.
* @return void
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('app:cleanup-temp-files')->everyThirtyMinutes();
$schedule->command('app:remove-stale-media-jobs')->everyThirtyMinutes();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}

169
app/Enum/CurlErrorCodes.php Normal file
View File

@@ -0,0 +1,169 @@
<?php
namespace App\Enum;
class CurlErrorCodes extends Enum
{
public const CURLE_UNSUPPORTED_PROTOCOL = 1;
public const CURLE_FAILED_INIT = 2;
public const CURLE_URL_MALFORMAT = 3;
public const CURLE_URL_MALFORMAT_USER = 4;
public const CURLE_COULDNT_RESOLVE_PROXY = 5;
public const CURLE_COULDNT_RESOLVE_HOST = 6;
public const CURLE_COULDNT_CONNECT = 7;
public const CURLE_FTP_WEIRD_SERVER_REPLY = 8;
public const CURLE_REMOTE_ACCESS_DENIED = 9;
public const CURLE_FTP_WEIRD_PASS_REPLY = 11;
public const CURLE_FTP_WEIRD_PASV_REPLY = 13;
public const CURLE_FTP_WEIRD_227_FORMAT = 14;
public const CURLE_FTP_CANT_GET_HOST = 15;
public const CURLE_FTP_COULDNT_SET_TYPE = 17;
public const CURLE_PARTIAL_FILE = 18;
public const CURLE_FTP_COULDNT_RETR_FILE = 19;
public const CURLE_QUOTE_ERROR = 21;
public const CURLE_HTTP_RETURNED_ERROR = 22;
public const CURLE_WRITE_ERROR = 23;
public const CURLE_UPLOAD_FAILED = 25;
public const CURLE_READ_ERROR = 26;
public const CURLE_OUT_OF_MEMORY = 27;
public const CURLE_OPERATION_TIMEDOUT = 28;
public const CURLE_FTP_PORT_FAILED = 30;
public const CURLE_FTP_COULDNT_USE_REST = 31;
public const CURLE_RANGE_ERROR = 33;
public const CURLE_HTTP_POST_ERROR = 34;
public const CURLE_SSL_CONNECT_ERROR = 35;
public const CURLE_BAD_DOWNLOAD_RESUME = 36;
public const CURLE_FILE_COULDNT_READ_FILE = 37;
public const CURLE_LDAP_CANNOT_BIND = 38;
public const CURLE_LDAP_SEARCH_FAILED = 39;
public const CURLE_FUNCTION_NOT_FOUND = 41;
public const CURLE_ABORTED_BY_CALLBACK = 42;
public const CURLE_BAD_FUNCTION_ARGUMENT = 43;
public const CURLE_INTERFACE_FAILED = 45;
public const CURLE_TOO_MANY_REDIRECTS = 47;
public const CURLE_UNKNOWN_TELNET_OPTION = 48;
public const CURLE_TELNET_OPTION_SYNTAX = 49;
public const CURLE_PEER_FAILED_VERIFICATION = 51;
public const CURLE_GOT_NOTHING = 52;
public const CURLE_SSL_ENGINE_NOTFOUND = 53;
public const CURLE_SSL_ENGINE_SETFAILED = 54;
public const CURLE_SEND_ERROR = 55;
public const CURLE_RECV_ERROR = 56;
public const CURLE_SSL_CERTPROBLEM = 58;
public const CURLE_SSL_CIPHER = 59;
public const CURLE_SSL_CACERT = 60;
public const CURLE_BAD_CONTENT_ENCODING = 61;
public const CURLE_LDAP_INVALID_URL = 62;
public const CURLE_FILESIZE_EXCEEDED = 63;
public const CURLE_USE_SSL_FAILED = 64;
public const CURLE_SEND_FAIL_REWIND = 65;
public const CURLE_SSL_ENGINE_INITFAILED = 66;
public const CURLE_LOGIN_DENIED = 67;
public const CURLE_TFTP_NOTFOUND = 68;
public const CURLE_TFTP_PERM = 69;
public const CURLE_REMOTE_DISK_FULL = 70;
public const CURLE_TFTP_ILLEGAL = 71;
public const CURLE_TFTP_UNKNOWNID = 72;
public const CURLE_REMOTE_FILE_EXISTS = 73;
public const CURLE_TFTP_NOSUCHUSER = 74;
public const CURLE_CONV_FAILED = 75;
public const CURLE_CONV_REQD = 76;
public const CURLE_SSL_CACERT_BADFILE = 77;
public const CURLE_REMOTE_FILE_NOT_FOUND = 78;
public const CURLE_SSH = 79;
public const CURLE_SSL_SHUTDOWN_FAILED = 80;
public const CURLE_AGAIN = 81;
public const CURLE_SSL_CRL_BADFILE = 82;
public const CURLE_SSL_ISSUER_ERROR = 83;
public const CURLE_FTP_PRET_FAILED = 84;
public const CURLE_RTSP_CSEQ_ERROR = 85;
public const CURLE_RTSP_SESSION_ERROR = 86;
public const CURLE_FTP_BAD_FILE_LIST = 87;
public const CURLE_CHUNK_FAILED = 88;
/**
* Curl Error messages
* @var string[]
*/
public static $messages = [
1 => 'Unsupported protocol.',
2 => 'Failed initalization.',
3 => 'Invalid URL format.',
4 => 'CURLE_URL_MALFORMAT_USER.',
5 => 'Could not resolve proxy.',
6 => 'Could not resolve host.',
7 => 'Could not connect to host.',
8 => 'Invalid reply from FTP server.',
9 => 'Access denied on host.',
11 => 'Invalid pass reply from FTP server.',
13 => 'Invalid pasv reply from FTP server.',
14 => 'Invalid 227 format from FTP server.',
15 => 'Could not get FTP host.',
17 => 'Could not set type for FTP transfer.',
18 => 'Invalid partial size.',
19 => 'Could not retrieve file from FTP server.',
21 => 'Quote error.',
22 => 'HTTP server returned error.',
23 => 'File write error.',
25 => 'Upload file error.',
26 => 'File read error.',
27 => 'Out of memory.',
28 => 'File transfer timed out.',
30 => 'Invalid port for FTP server.',
31 => 'Could not use rest for FTP server.',
33 => 'File range error.',
34 => 'Invalid POST for HTTP server.',
35 => 'SSL connectio error.',
36 => 'Invalid resume download.',
37 => 'Could not read file.',
38 => 'Could not bind to LDAP.',
39 => 'LDAP search failed.',
41 => 'Function not found.',
42 => 'Aborted by callback.',
43 => 'Bad function argument.',
45 => 'Interface failed.',
47 => 'Too many redirects.',
48 => 'Unknown telnet option.',
49 => 'Telnet option syntax invalid.',
51 => 'Peer failed verification.',
52 => 'Did not receive any data.',
53 => 'SSL engine was not found.',
54 => 'SSL engine failed.',
55 => 'Send data error.',
56 => 'Receive data error.',
58 => 'SSL certificate error.',
59 => 'SSL cipher error.',
60 => 'SSL CACertificate failed.',
61 => 'Invalid content encoding.',
62 => 'Invalid LDAP url.',
63 => 'Filesize exceeded.',
64 => 'SSL Failed.',
65 => 'CURLE_SEND_FAIL_REWIND.',
66 => 'SSL engine initalization failed.',
67 => 'CURLE_LOGIN_DENIED.',
68 => 'CURLE_TFTP_NOTFOUND.',
69 => 'CURLE_TFTP_PERM.',
70 => 'CURLE_REMOTE_DISK_FULL.',
71 => 'CURLE_TFTP_ILLEGAL.',
72 => 'CURLE_TFTP_UNKNOWNID.',
73 => 'Remote file already exists.',
74 => 'No such user on FTP server.',
75 => 'Conversion failed.',
76 => 'Conversion required.',
77 => 'SSL CACertificate bad file.',
78 => 'Remove file not found.',
79 => 'SSH error.',
80 => 'SSL Shutdown failed.',
81 => 'Again.',
82 => 'SSL bad CRL file.',
83 => 'SSL issuer error.',
84 => 'FTP pret failed.',
85 => 'CURLE_RTSP_CSEQ_ERROR.',
86 => 'CURLE_RTSP_SESSION_ERROR.',
87 => 'CURLE_FTP_BAD_FILE_LIST.',
88 => 'CURLE_CHUNK_FAILED.',
];
}

73
app/Enum/Enum.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
namespace App\Enum;
use ReflectionClass;
class Enum
{
/**
* Message list
*
* @var array<string<static>>
*/
public static $messages = [];
/**
* Caches reflections of enum subclasses.
*
* @var array<class-string<static>, ReflectionClass<static>>
*/
public static $reflectionCache = [];
/**
* Returns a reflection of the enum subclass.
*
* @return ReflectionClass<static>
*/
public static function getReflection(): ReflectionClass
{
$class = static::class;
return static::$reflectionCache[$class] ??= new ReflectionClass($class);
}
/**
* Returns the constants in the enum subclass
*
* @return array<static>
*/
public static function getConstants(): array
{
return static::getReflection()->getConstants();
}
/**
* Returns the constants values in the enum subclass
*
* @return array<static>
*/
public static function getConstantValues(): array
{
return array_values(static::getReflection()->getConstants());
}
/**
* Returns a message from the enum subclass
*
* @param integer $messageIndex The message index to retrieve.
* @param string $defaultMessage Message to use if index does not exist.
* @return string
*/
public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string
{
if (array_key_exists($messageIndex, self::$messages) === true) {
return self::$messages[$messageIndex];
}
return $defaultMessage;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Enum;
class HttpResponseCodes extends Enum
{
public const HTTP_CONTINUE = 100;
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_PROCESSING = 102;
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_ACCEPTED = 202;
public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
public const HTTP_NO_CONTENT = 204;
public const HTTP_RESET_CONTENT = 205;
public const HTTP_PARTIAL_CONTENT = 206;
public const HTTP_MULTI_STATUS = 207;
public const HTTP_ALREADY_REPORTED = 208;
public const HTTP_IM_USED = 226;
public const HTTP_MULTIPLE_CHOICES = 300;
public const HTTP_MOVED_PERMANENTLY = 301;
public const HTTP_FOUND = 302;
public const HTTP_SEE_OTHER = 303;
public const HTTP_NOT_MODIFIED = 304;
public const HTTP_USE_PROXY = 305;
public const HTTP_RESERVED = 306;
public const HTTP_TEMPORARY_REDIRECT = 307;
public const HTTP_PERMANENTLY_REDIRECT = 308;
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_PAYMENT_REQUIRED = 402;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_METHOD_NOT_ALLOWED = 405;
public const HTTP_NOT_ACCEPTABLE = 406;
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
public const HTTP_REQUEST_TIMEOUT = 408;
public const HTTP_CONFLICT = 409;
public const HTTP_GONE = 410;
public const HTTP_LENGTH_REQUIRED = 411;
public const HTTP_PRECONDITION_FAILED = 412;
public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
public const HTTP_REQUEST_URI_TOO_LONG = 414;
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const HTTP_EXPECTATION_FAILED = 417;
public const HTTP_I_AM_A_TEAPOT = 418;
public const HTTP_MISDIRECTED_REQUEST = 421;
public const HTTP_UNPROCESSABLE_ENTITY = 422;
public const HTTP_LOCKED = 423;
public const HTTP_FAILED_DEPENDENCY = 424;
public const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425;
public const HTTP_UPGRADE_REQUIRED = 426;
public const HTTP_PRECONDITION_REQUIRED = 428;
public const HTTP_TOO_MANY_REQUESTS = 429;
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public const HTTP_INTERNAL_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
public const HTTP_BAD_GATEWAY = 502;
public const HTTP_SERVICE_UNAVAILABLE = 503;
public const HTTP_GATEWAY_TIMEOUT = 504;
public const HTTP_VERSION_NOT_SUPPORTED = 505;
public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;
public const HTTP_INSUFFICIENT_STORAGE = 507;
public const HTTP_LOOP_DETECTED = 508;
public const HTTP_NOT_EXTENDED = 510;
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
/**
* HTTP Response Messages
* @var string[]
*/
public static $statusTexts = [
100 => 'Continue.',
101 => 'Switching Protocols.',
102 => 'Processing.',
// RFC2518
200 => 'OK.',
201 => 'Created.',
202 => 'Accepted.',
203 => 'Non-Authoritative Information.',
204 => 'No Content.',
205 => 'Reset Content.',
206 => 'Partial Content.',
207 => 'Multi-Status.',
// RFC4918
208 => 'Already Reported.',
// RFC5842
226 => 'IM Used.',
// RFC3229
300 => 'Multiple Choices.',
301 => 'Moved Permanently.',
302 => 'Found.',
303 => 'See Other.',
304 => 'Not Modified.',
305 => 'Use Proxy.',
307 => 'Temporary Redirect.',
308 => 'Permanent Redirect.',
// RFC7238
400 => 'Bad Request.',
401 => 'Unauthorized.',
402 => 'Payment Required.',
403 => 'Forbidden.',
404 => 'Not Found.',
405 => 'Method Not Allowed.',
406 => 'Not Acceptable.',
407 => 'Proxy Authentication Required.',
408 => 'Request Timeout.',
409 => 'Conflict.',
410 => 'Gone.',
411 => 'Length Required.',
412 => 'Precondition Failed.',
413 => 'Payload Too Large.',
414 => 'URI Too Long.',
415 => 'Unsupported Media Type.',
416 => 'Range Not Satisfiable.',
417 => 'Expectation Failed.',
418 => 'I\'m a teapot.',
// RFC2324
421 => 'Misdirected Request.',
// RFC7540
422 => 'Unprocessable Entity.',
// RFC4918
423 => 'Locked.',
// RFC4918
424 => 'Failed Dependency.',
// RFC4918
425 => 'Reserved for WebDAV advanced collections expired proposal.',
// RFC2817
426 => 'Upgrade Required.',
// RFC2817
428 => 'Precondition Required.',
// RFC6585
429 => 'Too Many Requests.',
// RFC6585
431 => 'Request Header Fields Too Large.',
// RFC6585
451 => 'Unavailable For Legal Reasons.',
// RFC7725
500 => 'Internal Server Error.',
501 => 'Not Implemented.',
502 => 'Bad Gateway.',
503 => 'Service Unavailable.',
504 => 'Gateway Timeout.',
505 => 'HTTP Version Not Supported.',
506 => 'Variant Also Negotiates.',
// RFC2295
507 => 'Insufficient Storage.',
// RFC4918
508 => 'Loop Detected.',
// RFC5842
510 => 'Not Extended.',
// RFC2774
511 => 'Network Authentication Required.',
// RFC6585
];
}

View File

@@ -3,12 +3,19 @@
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use PDOException;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
@@ -18,13 +25,74 @@ class Handler extends ExceptionHandler
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register(): void
{
// $this->renderable(function (HttpException $e, $request) {
// if ($request->is('api/*')) {
// $message = $e->getMessage();
// if ($message === '') {
// $message = HttpResponseCodes::$statusTexts[$e->getStatusCode()];
// }
// return response()->json([
// 'message' => $message
// ], $e->getStatusCode());
// }
// });
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'Resource not found'
], 404);
}
});
$this->renderable(function (PDOException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'The server is currently unavailable'
], 503);
}
});
$this->reportable(function (Throwable $e) {
//
if ($this->shouldReport($e) === true) {
if (App::runningUnitTests() === false) {
$this->sendEmail($e);
}
}
});
}
/**
* Send email
*
* @param Throwable $exception Throwable object.
* @return void
*/
public function sendEmail(Throwable $exception)
{
try {
$e = FlattenException::createFromThrowable($exception);
$handler = new HtmlErrorRenderer(true);
$css = $handler->getStylesheet();
$content = $handler->getBody($e);
Mail::send('emails.exception', compact('css', 'content'), function ($message) {
$message
->to('webmaster@stemmechanics.com.au')
->subject('Exception Generated')
;
});
} catch (Throwable $ex) {
Log::error($ex);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Filters;
use App\Models\Subscriber;
class SubscriptionFilter extends FilterAbstract
{
/**
* The model class to filter
*
* @var mixed
*/
protected $class = \App\Models\Subscription::class;
/**
* Return an array of attributes visible in the results
*
* @param array $attributes Attributes currently visible.
* @param User|null $user Current logged in user or null.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user)
{
if ($user?->hasPermission('admin/users') !== true) {
return ['id', 'email', 'confirmed_at'];
}
}
}

78
app/Helpers/Array.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
/* Array Helper Functions */
/**
* Remove an item from an array.
*
* @param array $arr The array to check.
* @param string|array $item The item or items to remove.
* @return array The filtered array.
*/
function arrayRemoveItem(array $arr, string|array $item): array
{
$filteredArr = $arr;
if (is_string($item) === true) {
$item = [$item];
}
foreach ($item as $str) {
$filteredArr = array_filter($arr, function ($item) use ($str) {
return $item !== $str;
});
}
return $filteredArr;
}
/**
* Return an array with specified the keys
*
* @param array $arr The array to filter.
* @param string|array $keys The keys to keep.
* @return array The filtered array.
*/
function arrayLimitKeys(array $arr, array $keys): array
{
return array_intersect_key($arr, array_flip($keys));
}
/**
* Return an array value or default value if it does not exist
*
* @param string $key The key value to return if exists.
* @param array $arr The array to check.
* @param mixed $value The value to return if key does not exist.
* @return mixed
*/
function arrayDefaultValue(string $key, array $arr, mixed $value): mixed
{
if (array_key_exists($key, $arr) === true) {
return $arr[$key];
}
return $value;
}
/**
* Return if an item exists in an array, case insensitive
*
* @param string $val The value to check.
* @param array $arr The array to check.
* @return boolean
*/
function existsInArray(string $val, array $arr): bool
{
$exists = false;
foreach ($arr as $el) {
if (strcasecmp($val, $el) === 0) {
$exists = true;
break;
}
}
return $exists;
}

92
app/Helpers/Temp.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
/* Temp File Helper Functions */
/**
* Generate a temporary file path.
*
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return string The filtered array.
*/
function generateTempFilePath(string $extension = '', string $part = ''): string
{
$temporaryDir = storage_path('app/tmp');
if (is_dir($temporaryDir) === false) {
mkdir($temporaryDir, 0777, true);
}
return $temporaryDir . DIRECTORY_SEPARATOR . uniqid('upload_', true) . ($extension !== '' ? ".{$extension}" : '') .
($part !== '' ? ".part-{$part}" : '');
}
/**
* Get Temp file information
*
* @param string $filePath The temp file name.
* @return array The temp file name details.
*/
function tempFileInfo(string $filePath): array
{
$part = '';
// Extract the part if it's present
if (preg_match('/\.part-(\d+)$/', $filePath, $matches) !== false) {
$part = $matches[1];
$filePath = substr($filePath, 0, -strlen($matches[0]));
}
$info = pathinfo($filePath);
$directory = $info['dirname'];
$name = $info['filename'];
$extension = '';
// If there's an extension, separate it
if (isset($info['extension']) === true) {
$extension = $info['extension'];
}
return [
'dirname' => $directory,
'basename' => $name . ($extension !== '' ? ".{$extension}" : ''),
'filename' => $name,
'extension' => $extension,
'part' => $part,
];
}
/**
* Check a temporary file exists.
*
* @param string $dir The file parent directory.
* @param string $name The file name.
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return boolean If the file exists.
*/
function tempFileExists(string $dir, string $name, string $extension = '', string $part = ''): bool
{
$filename = constructTempFileName($dir, $name, $extension, $part);
$exists = file_exists($filename);
return $exists;
}
/**
* Construct the temp file name based on the information
*
* @param string $dir The file parent directory.
* @param string $name The file name.
* @param string $extension The file extension to use.
* @param string $part The file part number.
* @return string The file path.
*/
function constructTempFileName(string $dir, string $name, string $extension = '', string $part = ''): string
{
$filename = $dir . DIRECTORY_SEPARATOR . $name . ($extension !== '' ? ".{$extension}" : '') .
($part !== "" ? ".part-{$part}" : '');
return $filename;
}

27
app/Helpers/TypeValue.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/* Type Value Helper Functions */
/**
* Is value true
*
* @param mixed $value Value to check.
* @return boolean
*/
function isTrue(mixed $value): bool
{
if (is_bool($value) === true && $value === true) {
return true;
}
if (is_numeric($value) === true && intval($value) === 1) {
return true;
}
if (is_string($value) === true && in_array(strtolower($value), ['true', '1'], true) === true) {
return true;
}
return false;
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\AnalyticsConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AnalyticsRequest;
use App\Models\AnalyticsItemRequest;
use App\Models\AnalyticsSession;
use Illuminate\Http\Request;
class AnalyticsController extends ApiController
{
/**
* AnalyticsController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'index',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
$request->rename([
'type' => 'requests.type',
'path' => 'requests.path'
]);
list($collection, $total) = AnalyticsConductor::request($request);
return $this->respondAsResource(
$collection,
['resourceName' => 'session',
'isCollection' => true,
'appendData' => ['total' => $total]
]
);
}//end if
return $this->respondForbidden();
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\AnalyticsSession $session The analytics session.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, AnalyticsSession $session)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
$session->load(['requests' => function ($query) {
$query->select('session_id', 'type', 'path', 'created_at');
}
]);
foreach ($session->requests as $requestItem) {
$requestItem->makeHidden('session_id');
}
return $this->respondAsResource(
$session,
['resourceName' => 'session']
);
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\AnalyticsRequest $request The user request.
* @return \Illuminate\Http\Response
*/
public function store(AnalyticsRequest $request)
{
if (AnalyticsConductor::creatable() === true) {
$analytics = null;
$user = $request->user();
$data = [
'type' => $request->input('type'),
'path' => $request->input('path', ''),
'useragent' => $request->userAgent(),
'ip' => $request->ip()
];
if (
$user !== null &&
$user->hasPermission('admin/analytics') === true &&
$request->has('session') === true
) {
$data['session_id'] = $request->input('session_id');
$analytics = AnalyticsItemRequest::create($data);
} else {
$analytics = AnalyticsItemRequest::create($data);
}
return $this->respondAsResource(
AnalyticsConductor::model($request, $analytics),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use App\Enum\HttpResponseCodes;
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ApiController extends Controller
{
/**
* Resource name
* @var string
*/
protected $resourceName = '';
/**
* Return generic json response with the given data.
*
* @param array $data Response data.
* @param integer $respondCode Response status code.
* @param array $headers Response headers.
* @return JsonResponse
*/
public function respondJson(
array $data,
int $respondCode = HttpResponseCodes::HTTP_OK,
array $headers = []
): JsonResponse {
return response()->json($data, $respondCode, $headers);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondForbidden(
string $message = 'You do not have permission to access the resource.'
): JsonResponse {
return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondNotFound(string $message = 'The resource was not found.'): JsonResponse
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_NOT_FOUND);
}
/**
* Return too large message
*
* @param string $message Response message.
* @return JsonResponse
*/
public function respondTooLarge(string $message = 'The request entity is too large.'): JsonResponse
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_REQUEST_ENTITY_TOO_LARGE);
}
/**
* Return no content.
*
* @return JsonResponse
*/
public function respondNoContent(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT);
}
/**
* Return no content
*
* @return JsonResponse
*/
public function respondNotImplemented(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_NOT_IMPLEMENTED);
}
/**
* Return created.
*
* @return JsonResponse
*/
public function respondCreated(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_CREATED);
}
/**
* Return accepted.
*
* @return JsonResponse
*/
public function respondAccepted(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_ACCEPTED);
}
/**
* Return server error.
*
* @return JsonResponse
*/
public function respondServerError(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR);
}
/**
* Return single error message
*
* @param string $message Error message.
* @param integer $responseCode Resource code.
* @return JsonResponse
*/
public function respondError(
string $message,
int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY
): JsonResponse {
return response()->json([
'message' => $message
], $responseCode);
}
/**
* Return formatted errors
*
* @param array $errors Error messages.
* @param integer $responseCode Resource code.
* @return JsonResponse
*/
public function respondWithErrors(
array $errors,
int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY
): JsonResponse {
$keys = array_keys($errors);
$error = $errors[$keys[0]];
if (count($keys) > 1) {
$additional_errors = (count($keys) - 1);
$error .= sprintf(' (and %d more %s', $additional_errors, Str::plural('error', $additional_errors));
}
return response()->json([
'message' => $error,
'errors' => $errors
], $responseCode);
}
/**
* Return resource data
*
* @param array|Model|Collection $data Resource data.
* @param array $options Respond options.
* @param callable|null $validationFn Optional validation function to check the data before responding.
* @return JsonResponse
*/
protected function respondAsResource(
mixed $data,
array $options = [],
$validationFn = null
): JsonResponse {
$isCollection = ($options['isCollection'] ?? false);
$appendData = ($options['appendData'] ?? null);
$resourceName = ($options['resourceName'] ?? '');
$transformResourceName = ($options['transformResourceName'] ?? true);
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
$validationData = [];
if (array_key_exists('appendData', $options) === true) {
$validationData = $options['appendData'];
}
if ($validationFn === null || $validationFn($validationData) === true) {
return $this->respondNotFound();
}
}
if (empty($resourceName) === true) {
$resourceName = $this->resourceName;
}
if (empty($resourceName) === true) {
$resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));
$resourceName = strtolower($resourceName);
}
$dataArray = [];
if ($data instanceof Collection) {
$dataArray = $data->toArray();
} elseif (is_array($data) === true) {
$dataArray = $data;
} elseif ($data instanceof Model) {
$dataArray = $data->toArray();
}
$resource = [];
if ($isCollection === true) {
$resource = [$transformResourceName === true ? Str::plural($resourceName) : $resourceName => $dataArray];
} else {
$resource = [$transformResourceName === true ? Str::singular($resourceName) : $resourceName => $dataArray];
}
if ($appendData !== null) {
$resource += $appendData;
}
return response()->json($resource, $respondCode);
}
/**
* Get the Controller Model Class name.
*
* @return string
*/
public function getModelClass(): string
{
$controllerClass = static::class;
$modelName = 'App\\Models\\' . Str::replaceLast('Controller', '', Str::afterLast($controllerClass, '\\'));
if (class_exists($modelName) === false) {
return $modelName;
}
return $modelName;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\ArticleRequest;
use App\Models\Media;
use App\Models\Article;
use App\Traits\HasAttachments;
use App\Traits\HasGallery;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ArticleController extends ApiController
{
use HasAttachments;
use HasGallery;
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'store',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Article $article)
{
if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response
*/
public function store(ArticleRequest $request)
{
if (ArticleConductor::creatable() === true) {
$article = Article::create($request->except(['attachments', 'gallery']));
if ($request->has('attachments') === true) {
$article->addAttachments($request->get('attachments'));
}
if ($request->has('gallery') === true) {
$article->galleryAddMany($request->get('gallery'));
}
return $this->respondAsResource(
ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function update(ArticleRequest $request, Article $article)
{
if (ArticleConductor::updatable($article) === true) {
if ($request->has('attachments') === true) {
$article->deleteAttachments();
$article->addAttachments($request->get('attachments'));
}
if ($request->has('gallery') === true) {
$article->gallery()->delete();
$article->galleryAddMany($request->get('gallery'));
}
$article->update($request->except(['attachments', 'gallery']));
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function destroy(Article $article)
{
if (ArticleConductor::destroyable($article) === true) {
$article->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AuthLoginRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\JsonResponse;
class AuthController extends ApiController
{
/**
* Resource name
* @var string
*/
protected $resourceName = 'user';
/**
* ApplicationController constructor.
*/
public function __construct()
{
// $this->middleware('auth:sanctum')
// ->only(['me']);
}
/**
* Current User details
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function me(Request $request): JsonResponse
{
$user = $request->user()->makeVisible(['permissions']);
return $this->respondAsResource($user);
}
/**
* Login user with supplied creditials
*
* @param App\Http\Controllers\Api\AuthLoginRequest $request Created request data.
* @return JsonResponse|void
*/
public function login(AuthLoginRequest $request)
{
$user = User::where('email', '=', $request->input('email'))->first();
if (
$user !== null &&
strlen($user->password) > 0 &&
Hash::check($request->input('password'), $user->password) === true
) {
if ($user->email_verified_at === null) {
return $this->respondWithErrors([
'email' => 'Email address has not been verified.'
]);
}
if ($user->disabled === true) {
return $this->respondWithErrors([
'email' => 'Account has been disabled.'
]);
}
$token = $user->createToken('user_token')->plainTextToken;
$user->logins()->create([
'token' => $token,
'login' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent()
]);
return $this->respondAsResource(
$user->makeVisible(['permissions']),
['appendData' => ['token' => $token]]
);
}//end if
return $this->respondWithErrors([
'email' => 'Invalid email or password',
'password' => 'Invalid email or password',
]);
}
/**
* Logout current user
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse
{
$user = $request->user();
$user->logins()->where('token', $user->currentAccessToken())->update(['logout' => now()]);
$user->currentAccessToken()->delete();
return $this->respondNoContent();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\ContactSendRequest;
use App\Jobs\SendEmailJob;
use App\Mail\Contact;
class ContactController extends ApiController
{
/**
* Send the request to the site admin by email
*
* @param \App\Http\Requests\User\ContactSendRequest $request Request data.
* @return \Illuminate\Http\Response
*/
public function send(ContactSendRequest $request)
{
dispatch((new SendEmailJob(
config('contact.contact_address'),
new Contact(
$request->input('name'),
$request->input('email'),
$request->input('content')
)
)))->onQueue('mail');
return $this->respondCreated();
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Models\Event;
use App\Conductors\EventConductor;
use App\Conductors\MediaConductor;
use App\Conductors\UserConductor;
use App\Http\Requests\EventRequest;
use App\Models\Media;
use App\Models\User;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class EventController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy', 'userAdd', 'userUpdate', 'userDelete']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = EventConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Event $event)
{
if (EventConductor::viewable($event) === true) {
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\EventRequest $request The request.
* @return \Illuminate\Http\Response
*/
public function store(EventRequest $request)
{
if (EventConductor::creatable() === true) {
$event = Event::create($request->except(['attachments']));
if ($request->has('attachments') === true) {
$event->addAttachments($request->get('attachments'));
}
return $this->respondAsResource(
EventConductor::model($request, $event),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\EventRequest $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function update(EventRequest $request, Event $event)
{
if (EventConductor::updatable($event) === true) {
if ($request->has('attachments') === true) {
$event->deleteAttachments();
$event->addAttachments($request->get('attachments'));
}
$event->update($request->except(['attachments']));
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function destroy(Event $event)
{
if (EventConductor::destroyable($event) === true) {
$event->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
/**
* List users of Event
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return JsonResponse
*/
public function userList(Request $request, Event $event): JsonResponse
{
$authUser = $request->user();
$eventUsers = $event->users;
if ($authUser !== null) {
$isAdmin = $authUser->hasPermission('admin/events');
$isEventUser = $eventUsers->contains($authUser->id);
if ($isAdmin === true || $isEventUser === true) {
if ($isAdmin === false) {
$eventUsers = $eventUsers->filter(function ($user) use ($authUser) {
return $user->id === $authUser->id;
});
}
return $this->respondAsResource(
UserConductor::collection($request, $eventUsers),
[
'isCollection' => true,
'resourceName' => 'users'
]
);
}
return $this->respondNotFound();
}//end if
return $this->respondForbidden();
}
/**
* Add user to Event
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return JsonResponse
*/
public function userAdd(Request $request, Event $event): JsonResponse
{
$authUser = $request->user();
if ($authUser !== null && $authUser->hasPermission('admin/events') === true) {
if ($request->has("users") === true) {
$eventUsers = $event->users()->pluck('user_id')->toArray(); // Get the current users in the event
$requestedUsers = $request->input("users"); // Get the requested users
$usersToAdd = array_diff($requestedUsers, $eventUsers); // Users to add
$usersToRemove = array_diff($eventUsers, $requestedUsers); // Users to remove
// Add missing users
foreach ($usersToAdd as $userToAdd) {
if (User::find($userToAdd) !== null) {
$event->users()->attach($userToAdd);
}
}
// Remove extra users
foreach ($usersToRemove as $userToRemove) {
$event->users()->detach($userToRemove);
}
return $this->respondNoContent();
}//end if
return $this->respondWithErrors(['users' => 'The user list was not found']);
}//end if
return $this->respondForbidden();
}
/**
* Update user
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @return void
*/
public function userUpdate(Request $request, Event $event): void
{
// only admin/events permitted
}
/**
* Delete user from event
*
* @param Request $request The HTTP request.
* @param Event $event Event model.
* @param User $user User model.
* @return JsonResponse
*/
public function userDelete(Request $request, Event $event, User $user): JsonResponse
{
$authUser = $request->user();
if ($authUser !== null && $authUser->hasPermission('admin/events') === true) {
$eventUsers = $event->users;
if ($eventUsers->find($user->id) !== null) {
$eventUsers->detach($user->id);
return $this->respondNoContent();
} else {
return $this->respondNotFound();
}
}
return $this->respondForbidden();
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Models\Media;
use Illuminate\Http\Request;
class InfoController extends ApiController
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$info = [
"version" => "1.0.0",
"max_upload_size" => Media::getMaxUploadSize()
];
return $this->respondJson($info);
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
class LogController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['show']);
}
/**
* Display the specified resource.
*
* @param Request $request The log request.
* @param string $name The log name.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, string $name)
{
if ($request->user()?->hasPermission('logs/' . $name) === true) {
switch (strtolower($name)) {
case 'discord':
$data = [];
$log = $request->get('log');
if ($log === null) {
$log = ['output', 'error'];
} else {
$log = explode(',', strtolower($log));
}
$lines = intval($request->get('lines', 50));
if ($lines > 100) {
$lines = 100;
} elseif ($lines < 0) {
$lines = 1;
}
$before = $request->get('before');
if ($before !== null) {
$before = preg_split(
"/([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/",
$before,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($before) !== 6) {
$before = null;
}
}
$after = $request->get('after');
if ($after !== null) {
$after = preg_split(
"/([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/",
$after,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($after) !== 6) {
$after = null;
}
}
$logFiles = [
[
'name' => 'output',
'path' => '/home/discordbot/.pm2/logs/stemmech-discordbot-out-0.log'
],[
'name' => 'error',
'path' => '/home/discordbot/.pm2/logs/stemmech-discordbot-error-0.log'
]
];
foreach ($logFiles as $logFile) {
if (in_array($logFile['name'], $log) === true) {
$logContent = '';
if (file_exists($logFile['path']) === true) {
$logContent = file_get_contents($logFile['path']);
}
$logArray = preg_split(
// phpcs:ignore Generic.Files.LineLength.TooLong
"/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}: (?:(?!\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}: )[\s\S])*)/",
$logContent,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
$logContent = '';
$logLineCount = 0;
$logLineSkip = false;
foreach (array_reverse($logArray) as $logLine) {
$lineDate = preg_split(
"/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}): /",
$logLine,
-1,
(PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)
);
if (count($lineDate) >= 6) {
$logLineSkip = false;
// Is line before
if (
$before !== null && (
$lineDate[0] > $before[0] ||
$lineDate[1] > $before[1] ||
$lineDate[2] > $before[2] ||
$lineDate[3] > $before[3] ||
$lineDate[4] > $before[4] ||
$lineDate[5] > $before[5]
)
) {
$logLineSkip = true;
continue;
}
// Is line after
if (
$after !== null && (
$after[0] > $lineDate[0] ||
$after[1] > $lineDate[1] ||
$after[2] > $lineDate[2] ||
$after[3] > $lineDate[3] ||
$after[4] > $lineDate[4] ||
$after[5] > $lineDate[5]
)
) {
$logLineSkip = true;
continue;
}
$logLineCount += 1;
}//end if
if ($logLineCount > $lines) {
break;
}
if ($logLineSkip === false) {
$logContent .= $logLine;
}
}//end foreach
$data[$logFile['name']] = $logContent;
}//end if
}//end foreach
return $this->respondJson([
'log' => $data
]);
}//end switch
}//end if
return $this->respondForbidden();
}
}

View File

@@ -0,0 +1,443 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\MediaJobConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\MediaRequest;
use App\Models\Media;
use App\Models\MediaJob;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\PersonalAccessToken;
class MediaController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = MediaConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $medium The request media.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Media $medium)
{
if (MediaConductor::viewable($medium) === true) {
return $this->respondAsResource(MediaConductor::model($request, $medium));
}
return $this->respondForbidden();
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
* @return \Illuminate\Http\Response
*/
public function store(MediaRequest $request)
{
// allowed to create a media item
if (MediaConductor::creatable() === false) {
return $this->respondForbidden();
}
// check for file
$file = $request->file('file');
if ($file === null) {
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
}
return $this->storeOrUpdate($request, null);
}
/**
* Update the media resource in storage.
*
* @param \App\Http\Requests\MediaRequest $request The update request.
* @param \App\Models\Media $medium The specified media.
* @return \Illuminate\Http\Response
*/
public function update(MediaRequest $request, Media $medium)
{
// allowed to update a media item
if (MediaConductor::updatable($medium) === false) {
return $this->respondForbidden();
}
return $this->storeOrUpdate($request, $medium);
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
* @param \App\Models\Media|null $medium The specified media.
* @return \Illuminate\Http\Response
*/
public function storeOrUpdate(MediaRequest $request, Media|null $medium)
{
$file = $request->file('file');
if ($file !== null) {
// validate file object
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors([$file => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(
[$file => 'An error occurred uploading the file to the server.']
);
}
}
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
}
// create/get media job
$mediaJob = null;
$data = [];
if ($request->missing('job_id') === true) {
/** @var \App\Models\User */
$user = auth()->user();
$mediaJob = new MediaJob();
$mediaJob->user_id = $user->id;
if ($medium !== null) {
$mediaJob->media_id = $medium->id;
}
if ($request->has('title') === true || $file !== null) {
$data['title'] = $request->get('title', '');
}
if ($request->has('name') === true || $file !== null) {
$data['name'] = (
$request->has('chunk') === true ? $request->get('name', '') : $file->getClientOriginalName()
);
}
if ($file !== null) {
$data['size'] = $request->has('chunk') === true ? intval($request->get('size', 0)) : $file->getSize();
$data['mime_type'] = (
$request->has('chunk') === true ? $request->get('mime_type', '') : $file->getMimeType()
);
}
if ($request->has('storage') === true || $file !== null) {
$data['storage'] = $request->get('storage', '');
}
if ($request->has('security_type') === true || $file !== null) {
$data['security']['type'] = $request->get('security_type', '');
$data['security']['data'] = $request->get('security_data', '');
if ($data['security']['type'] === '') {
$data['security']['data'] = '';
}
if ($medium === null || strcasecmp($data['security']['type'], $medium->security_type) !== 0) {
if ($request->has('storage') === false) {
$mime_type = $request->get('mime_type', $medium === null ? '' : $medium->mime_type);
$data['storage'] = Media::recommendedStorage($mime_type, $data['security']['type']);
}
}
}
if (
array_key_exists('storage', $data) === true && (
array_key_exists('security', $data) === true &&
array_key_exists('type', $data['security']) === true
) &&
array_key_exists('mime_type', $data) === true &&
$data['mime_type'] !== ""
) {
$error = Media::verifyStorage($data['mime_type'], $data['security']['type'], $data['storage']);
// Log::error($data['mime_type'] . ' - ' . $data['security']['type'] . ' - ' . $data['storage']);
switch ($error) {
case Media::STORAGE_VALID:
break;
case Media::STORAGE_MIME_MISSING:
return $this->respondWithErrors(['mime_type' => 'The file type is required.']);
case Media::STORAGE_NOT_FOUND:
return $this->respondWithErrors(['storage' => 'Storage was not found.']);
case Media::STORAGE_INVALID_SECURITY:
return $this->respondWithErrors(
['storage' => 'Storage invalid for this security requirement.']
);
default:
return $this->respondWithErrors(['storage' => 'Storage verification error occurred.']);
}
}
if ($request->has('transform') === true) {
$transform = [];
foreach (explode(',', $request->get('transform', '')) as $value) {
if (is_string($value) === true) {
if (preg_match('/^rotate-(-?\d+)$/', $value, $matches) !== false) {
$transform['rotate'] = $matches[1];
} elseif (preg_match('/^flip-([vh]|vh|hv)$/', $value, $matches) !== false) {
$transform['flip'] = $matches[1];
} elseif (preg_match('/^crop-(\d+)-(\d+)$/', $value, $matches) !== false) {
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2]];
} elseif (preg_match('/^crop-(\d+)-(\d+)-(\d+)-(\d+)$/', $value, $matches) !== false) {
$transform['crop'] = [
'width' => $matches[1],
'height' => $matches[2],
'x' => $matches[3],
'y' => $matches[4]
];
}
}
}
if (count($transform) > 0) {
$data['transform'] = $transform;
}
}//end if
$mediaJob->setStatusWaiting();
} else {
$mediaJob = MediaJob::find($request->get('job_id'));
if ($mediaJob === null || $mediaJob->exists() === false) {
$this->respondNotFound();
}
$data = json_decode($mediaJob->data, true);
if ($data === null) {
Log::error(`{$mediaJob->id} contains no data`);
return $this->respondServerError();
}
if (array_key_exists('name', $data) === false) {
Log::error(`{$mediaJob->id} data does not contain the name key`);
return $this->respondServerError();
}
}//end if
if ($mediaJob === null) {
Log::error(`media job does not exist`);
return $this->respondServerError();
}
// save uploaded file
if ($file !== null) {
if ($data['name'] === '') {
Log::error(`filename does not exist`);
return $this->respondServerError();
}
$temporaryFilePath = generateTempFilePath(
pathinfo($data['name'], PATHINFO_EXTENSION),
$request->get('chunk', '')
);
copy($file->path(), $temporaryFilePath);
if ($request->has('chunk') === true) {
$data['chunks'][$request->get('chunk', '1')] = $temporaryFilePath;
$data['chunk_count'] = $request->get('chunk_count', 1);
} else {
$data['file'] = $temporaryFilePath;
}
}
$mediaJob->data = json_encode($data, true);
$mediaJob->save();
$mediaJob->process();
return $this->respondAsResource(
MediaJobConductor::model($request, $mediaJob),
['resourceName' => 'media_job', 'respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Media $medium Specified media file.
* @return \Illuminate\Http\Response
*/
public function destroy(Media $medium)
{
if (MediaConductor::destroyable($medium) === true) {
$medium->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $media Specified media.
* @return \Illuminate\Http\Response
*/
public function download(Request $request, Media $media): Response
{
$headers = [];
/* Check file exists */
if (Storage::disk($media->storage)->exists($media->name) === false) {
return $this->respondNotFound();
}
$updated_at = Carbon::parse(Storage::disk($media->storage)->lastModified($media->name));
$headerPragma = 'no-cache';
$headerCacheControl = 'max-age=0, must-revalidate';
$headerExpires = $updated_at->toRfc2822String();
/* construct user if can */
$user = $request->user();
if ($user === null && $request->has('token') === true) {
$accessToken = PersonalAccessToken::findToken(urldecode($request->input('token')));
if (
$accessToken !== null && (config('sanctum.expiration') === null ||
$accessToken->created_at->lte(now()->subMinutes(config('sanctum.expiration'))) === false)
) {
$user = $accessToken->tokenable;
}
}
if ($media->security_type === '') {
/* no security */
$headerPragma = 'public';
$headerExpires = $updated_at->addMonth()->toRfc2822String();
} elseif (strcasecmp('password', $media->security_type) === 0) {
/* password */
if (
($user === null || $user->hasPermission('admin/media') === false) &&
($request->has('password') === false || $request->get('password') !== $media->security_data)
) {
return $this->respondForbidden();
}
} elseif (strcasecmp('permission', $media->security_type) === 0) {
/* permission */
if (
$user === null || (
$user->hasPermission('admin/media') === false &&
$user->hasPermission($media->security_data) === false
)
) {
return $this->respondForbidden();
}
}//end if
// deepcode ignore InsecureHash: Browsers expect Etag to be a md5 hash
$headerEtag = md5($updated_at->format('U'));
$headerLastModified = $updated_at->toRfc2822String();
$headers = [
'Cache-Control' => $headerCacheControl,
'Content-Disposition' => sprintf('inline; filename="%s"', basename($media->name)),
'Etag' => $headerEtag,
'Expires' => $headerExpires,
'Last-Modified' => $headerLastModified,
'Pragma' => $headerPragma,
];
$server = request()->server;
$requestModifiedSince = $server->has('HTTP_IF_MODIFIED_SINCE') &&
$server->get('HTTP_IF_MODIFIED_SINCE') === $headerLastModified;
$requestNoneMatch = $server->has('HTTP_IF_NONE_MATCH') &&
$server->get('HTTP_IF_NONE_MATCH') === $headerEtag;
if ($requestModifiedSince === true || $requestNoneMatch === true) {
return response()->make('', 304, $headers);
}
$headers['Content-Type'] = Storage::disk($media->storage)->mimeType($media->name);
$headers['Content-Length'] = Storage::disk($media->storage)->size($media->name);
$headers['Content-Disposition'] = 'attachment; filename="' . basename($media->name) . '"';
$stream = Storage::disk($media->storage)->readStream($media->name);
return response()->stream(
function () use ($stream) {
while (ob_get_level() > 0) {
ob_end_flush();
}
fpassthru($stream);
},
200,
$headers
);
}
/**
* Validate a File item in a request is valid
*
* @param UploadedFile $file The file to validate.
* @param string $errorKey The error key to use.
* @return JsonResponse|null
*/
private function validateFileObject(UploadedFile $file, string $errorKey = 'file'): JsonResponse|null
{
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors([$errorKey => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(
[$errorKey => 'An error occurred uploading the file to the server.']
);
}
}
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaJobConductor;
use App\Http\Controllers\Api\ApiController;
use App\Models\MediaJob;
use Illuminate\Http\Request;
class MediaJobController extends ApiController
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = MediaJobConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total],
'resourceName' => 'media_job'
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\MediaJob $mediaJob The request media job.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, MediaJob $mediaJob)
{
if (MediaJobConductor::viewable($mediaJob) === true) {
return $this->respondAsResource(
MediaJobConductor::model($request, $mediaJob),
['resourceName' => 'media_job']
);
}
return $this->respondForbidden();
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use thiagoalessio\TesseractOCR\TesseractOCR;
use FFMpeg;
use App\Enum\CurlErrorCodes;
class OCRController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
// $this->middleware('auth:sanctum')
// ->only(['show']);
}
/**
* Display the specified resource.
*
* @param Request $request The log request.
* @return \Illuminate\Http\Response
*/
public function show(Request $request)
{
// if ($request->user()?->hasPermission('logs/' . $name) === true) {
$url = $request->get('url');
if ($url !== null) {
$data = ['ocr' => []];
$filters = $request->get('filters', ['tesseract']);
if (is_array($filters) === false) {
$filters = explode(',', $filters);
}
$tesseractOEM = $request->get('tesseract.oem');
$tesseractDigits = $request->get('tesseract.digits');
$tesseractAllowlist = $request->get('tesseract.allowlist');
// Download URL
$urlDownloadFilePath = tempnam(sys_get_temp_dir(), 'download');
$maxDownloadSize = (1024 * 1024); // 1MB
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// We need progress updates to break the connection mid-way
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function (
$downloadSize,
$downloaded,
$uploadSize,
$uploaded
) use ($maxDownloadSize) {
return ($downloaded > $maxDownloadSize) ? 1 : 0;
});
$curlResult = curl_exec($ch);
$curlError = curl_errno($ch);
$curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch);
if ($curlError !== 0) {
$error = 'File size is larger then allowed';
if ($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) {
$error = CurlErrorCodes::getMessage($curlError);
}
return $this->respondWithErrors(['url' => $error]);
}
// Save url file
file_put_contents($urlDownloadFilePath, $curlResult);
$urlDownloadFilePathBase = preg_replace('/\\.[^.\\s]{3,4}$/', '', $urlDownloadFilePath);
// tesseract (overall)
$ocr = null;
foreach ($filters as $filterItem) {
if (str_starts_with($filterItem, 'tesseract') === true) {
$ocr = new TesseractOCR();
$ocr->image($urlDownloadFilePath);
if ($tesseractOEM !== null) {
$ocr->oem($tesseractOEM);
}
if ($tesseractDigits !== null) {
$ocr->digits();
}
if ($tesseractAllowlist !== null) {
$ocr->allowlist($tesseractAllowlist);
}
break;
}
}
// Image Filter Function
$tesseractImageFilterFunc = function ($filter, $options = null) use ($curlResult, $curlSize, $ocr) {
$result = '';
$img = imagecreatefromstring($curlResult);
if (
$img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) ||
($options === null && imagefilter($img, $filter) === true))
) {
ob_start();
imagepng($img);
$imgData = ob_get_contents();
ob_end_clean();
$imgDataSize = strlen($imgData);
$ocr->imageData($imgData, $imgDataSize);
imagedestroy($img);
$result = $ocr->run(500);
}
return $result;
};
// Image Scale Function
$tesseractImageScaleFunc = function ($scaleFunc) use ($curlResult, $ocr) {
$result = '';
$srcImage = imagecreatefromstring($curlResult);
$srcWidth = imagesx($srcImage);
$srcHeight = imagesy($srcImage);
$dstWidth = $scaleFunc($srcWidth);
$dstHeight = $scaleFunc($srcHeight);
$dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
ob_start();
imagepng($dstImage);
$imgData = ob_get_contents();
ob_end_clean();
$imgDataSize = strlen($imgData);
imagedestroy($srcImage);
imagedestroy($dstImage);
$ocr->imageData($imgData, $imgDataSize);
$result = $ocr->run(500);
return $result;
};
// filter: tesseract
if (in_array('tesseract', $filters) === true) {
$data['ocr']['tesseract'] = $ocr->run(500);
}
// filter: tesseract.grayscale
if (in_array('tesseract.grayscale', $filters) === true) {
$data['ocr']['tesseract.grayscale'] = $tesseractImageFilterFunc(IMG_FILTER_GRAYSCALE);
}
// filter: tesseract.double_scale
if (in_array('tesseract.double_scale', $filters) === true) {
$data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size * 2;
});
}
// filter: tesseract.half_scale
if (in_array('tesseract.half_scale', $filters) === true) {
$data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size / 2;
});
}
// filter: tesseract.edgedetect
if (in_array('tesseract.edgedetect', $filters) === true) {
$data['ocr']['tesseract.edgedetect'] = $tesseractImageFilterFunc(IMG_FILTER_EDGEDETECT);
}
// filter: tesseract.mean_removal
if (in_array('tesseract.mean_removal', $filters) === true) {
$data['ocr']['tesseract.mean_removal'] = $tesseractImageFilterFunc(IMG_FILTER_MEAN_REMOVAL);
}
// filter: tesseract.negate
if (in_array('tesseract.negate', $filters) === true) {
$data['ocr']['tesseract.negate'] = $tesseractImageFilterFunc(IMG_FILTER_NEGATE);
}
// filter: tesseract.pixelate
if (in_array('tesseract.pixelate', $filters) === true) {
$data['ocr']['tesseract.pixelate'] = $tesseractImageFilterFunc(IMG_FILTER_PIXELATE, 3);
}
// filter: keras
if (in_array('keras', $filters) === true) {
$cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url);
$command = escapeshellcmd($cmd);
$output = shell_exec($cmd);
if ($output !== null && strlen($output) > 0) {
$output = substr($output, (strpos($output, '----------START----------') + 25));
} else {
$output = '';
}
$data['ocr']['keras'] = $output;
}
unlink($urlDownloadFilePath);
return $this->respondJson($data);
}//end if
return $this->respondWithErrors(['url' => 'url is missing']);
}
// $ffmpeg = FFMpeg\FFMpeg::create();
// // Load the input video
// $inputFile = $ffmpeg->open('input.mp4');
// // Split the video into individual frames
// $videoFrames = $inputFile->frames();
// foreach ($videoFrames as $frame) {
// // Save the frame as a PNG
// $frame->save(new FFMpeg\Format\Video\PNG(), 'frame-' . $frame->getMetadata('pts') . '.png');
// // Pass the PNG to Tesseract for processing
// exec("tesseract frame-" . $frame->getMetadata('pts') . ".png output");
// }
// // Read the output from Tesseract
// $text = file_get_contents("output.txt");
// // Do something with the text from Tesseract
// echo $text;
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\ShortlinkConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\ShortlinkRequest;
use App\Models\Shortlink;
use Illuminate\Http\Request;
class ShortlinkController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = ShortlinkConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Shortlink $shortlink The request shortlink.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Shortlink $shortlink)
{
if (ShortlinkConductor::viewable($shortlink) === true) {
return $this->respondAsResource(ShortlinkConductor::model($request, $shortlink));
}
return $this->respondForbidden();
}
/**
* Store a new media resource
*
* @param \App\Http\Requests\ShortlinkRequest $request The shortlink.
* @return \Illuminate\Http\Response
*/
public function store(ShortlinkRequest $request)
{
if (ShortlinkConductor::creatable() === true) {
$shortlink = Shortlink::create($request->all());
return $this->respondAsResource(
ShortlinkConductor::model($request, $shortlink),
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}//end if
return $this->respondForbidden();
}
/**
* Update the media resource in storage.
*
* @param \App\Http\Requests\ShortlinkRequest $request The update request.
* @param \App\Models\Shortlink $shortlink The specified shortlink.
* @return \Illuminate\Http\Response
*/
public function update(ShortlinkRequest $request, Shortlink $shortlink)
{
if (ShortlinkConductor::updatable($shortlink) === true) {
$shortlink->update($request->all());
return $this->respondAsResource(ShortlinkConductor::model($request, $shortlink));
}//end if
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Shortlink $shortlink Specified shortlink.
* @return \Illuminate\Http\Response
*/
public function destroy(Shortlink $shortlink)
{
if (ShortlinkConductor::destroyable($shortlink) === true) {
$shortlink->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
}

View File

@@ -0,0 +1,369 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\EventConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
use App\Http\Requests\UserVerifyEmailRequest;
use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Conductors\UserConductor;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
class UserController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->except([
'index',
'show',
'register',
'exists',
'forgotPassword',
'resetPassword',
'verifyEmail',
'resendVerifyEmailCode',
'eventList',
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = UserConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Store a newly created user in the database.
*
* @param \App\Http\Requests\UserRequest $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function store(UserRequest $request)
{
if (UserConductor::creatable() === true) {
$user = User::create($request->all());
return $this->respondAsResource(
UserConductor::model($request, $user),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Display the specified user.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\User $user The user model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, User $user)
{
if (UserConductor::viewable($user) === true) {
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\UserRequest $request The user update request.
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function update(UserRequest $request, User $user)
{
if (UserConductor::updatable($user) === true) {
$input = [];
$updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
}
$input = $request->only($updatable);
if (array_key_exists('password', $input) === true) {
$input['password'] = Hash::make($request->input('password'));
}
$user->update($input);
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Remove the user from the database.
*
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
if (UserConductor::destroyable($user) === true) {
$user->delete();
return $this->respondNoContent();
}
return $this->respondForbidden();
}
/**
* Register a new user
*
* @param \App\Http\Requests\UserRegisterRequest $request The register user request.
* @return JsonResponse
*/
public function register(UserRegisterRequest $request): JsonResponse
{
try {
$userData = $request->only([
'first_name',
'last_name',
'email',
'phone',
'password',
'display_name',
]);
$userData['password'] = Hash::make($userData['password']);
$user = User::where('email', $request->input('email'))
->whereNull('password')
->first();
if ($user === null) {
$user = User::create($userData);
} else {
unset($userData['email']);
$user->update($userData);
}//end if
$code = $user->codes()->create([
'action' => 'verify-email',
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
return response()->json([
'message' => 'Check your email for a welcome code.'
]);
} catch (\Exception $e) {
return response()->json([
'message' => 'A server error occurred. Please try again later' . $e
], 500);
}//end try
}
/**
* Generates a new reset password code
*
* @param \App\Http\Requests\UserForgotPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function forgotPassword(UserForgotPasswordRequest $request)
{
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([
'action' => 'reset-password'
]);
dispatch((new SendEmailJob($user->email, new ForgotPassword($user, $code->code))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Resets a user password
*
* @param \App\Http\Requests\UserResetPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function resetPassword(UserResetPasswordRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'reset-password')->first();
if ($code !== null) {
$user = $code->user()->first();
$code->delete();
$user->codes()->where('action', 'verify-email')->delete();
$user->password = Hash::make($request->input('password'));
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
$user->save();
dispatch((new SendEmailJob($user->email, new ChangedPassword($user))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondError([
'code' => 'The code was not found or has expired.'
]);
}
/**
* Verify an email code
*
* @param \App\Http\Requests\UserVerifyEmailRequest $request The verify email request.
* @return \Illuminate\Http\Response
*/
public function verifyEmail(UserVerifyEmailRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'verify-email')->first();
if ($code !== null) {
$user = $code->user()->first();
$new_email = $code->data;
if ($new_email === null) {
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
} else {
dispatch((new SendEmailJob($user->email, new ChangedEmail($user, $user->email, $new_email))))
->onQueue('mail');
$user->email = $new_email;
$user->email_verified_at = now();
}
$code->delete();
$user->save();
return $this->respondNoContent();
}//end if
return $this->respondWithErrors([
'code' => 'The code was not found or has expired.'
]);
}
/**
* Resend a new verify email
*
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request.
* @return JsonResponse
*/
public function resendVerifyEmail(UserResendVerifyEmailRequest $request): JsonResponse
{
UserCode::clearExpired();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate();
$code->save();
if ($code->data === null) {
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
} else {
dispatch((new SendEmailJob($user->email, new ChangeEmailVerify($user, $code->code, $code->data))))
->onQueue('mail');
}
}
return response()->json(['message' => 'Verify email sent if user registered and required']);
}
/**
* Resend verification email
*
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend user request.
* @return \Illuminate\Http\Response
*/
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete();
if ($user->email_verified_at === null) {
$code = $user->codes()->create([
'action' => 'verify-email'
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
}
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Return a JSON event list of a user.
*
* @param Request $request The http request.
* @param User $user The specified user.
* @return JsonResponse
*/
public function eventList(Request $request, User $user): JsonResponse
{
if (
$request->user() !== null && (
$request->user() === $user || $request->user()->hasPermission('admin/events') === true
)
) {
$collection = $user->events;
$total = $collection->count();
$collection = EventConductor::collection($request, $collection);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -8,5 +8,6 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
use AuthorizesRequests;
use ValidatesRequests;
}

View File

@@ -19,8 +19,8 @@ class Kernel extends HttpKernel
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
// \App\Http\Middleware\TrimStrings::class,
// \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
@@ -40,15 +40,19 @@ class Kernel extends HttpKernel
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\ForceJsonResponse::class,
\App\Http\Middleware\UnmangleRequest::class,
'useSanctumGuard',
\App\Http\Middleware\LogRequest::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
@@ -59,10 +63,11 @@ class Kernel extends HttpKernel
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'unmangle' => \App\Http\Middleware\UnmangleRequest::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'useSanctumGuard' => \App\Http\Middleware\UseSanctumGuard::class
];
}

View File

@@ -3,15 +3,21 @@
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param mixed $request Request.
* @return ?string
*/
protected function redirectTo(Request $request): ?string
protected function redirectTo(mixed $request): ?string
{
return $request->expectsJson() ? null : route('login');
if ($request->expectsJson() === false) {
return route('login');
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*/
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use App\Models\AnalyticsItemRequest;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
class LogRequest
{
/**
* Handle an incoming request.
*
* @param Illuminate\Http\Request $request HTTP Request.
* @param \Closure $next Closure.
* @return Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): Response
{
// Make it an after middleware
$response = $next($request);
try {
AnalyticsItemRequest::create([
'type' => 'apirequest',
'path' => $request->path(),
]);
return $response;
} catch (\Error $e) {
report($e);
return $response;
}
}
}

View File

@@ -2,25 +2,28 @@
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Request $request Request.
* @param \Closure $next Closure.
* @param string|null ...$guards Guards.
* @return Response
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
$guards = empty($guards) === true ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
if (Auth::guard($guard)->check() === true) {
return redirect(RouteServiceProvider::HOME);
}
}

View File

@@ -17,12 +17,9 @@ class TrustProxies extends Middleware
/**
* The headers that should be used to detect proxies.
*
* @var int
* @var integer
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
// @codingStandardsIgnoreStart
protected $headers = (Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB);
// @codingStandardsIgnoreEnd
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UnmangleRequest
{
/**
* Handle an incoming request.
*
* @param Request $request Request.
* @param \Closure $next Next.
* @param string|null ...$guards Guards.
* @return Response response.
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
if (isset($_SERVER['QUERY_STRING']) === true) {
$params = $request->all();
$string = $_SERVER['QUERY_STRING'];
$parts = explode('&', $string);
foreach ($parts as $part) {
$key = $part;
$splitPos = strpos($key, '=');
if ($splitPos !== false) {
$key = urldecode(substr($key, 0, $splitPos));
}
$replace_key = str_replace('.', '_', $key);
if (strpos($key, '.') !== false && array_key_exists($replace_key, $params) === true) {
$params[$key] = $params[$replace_key];
unset($params[$replace_key]);
}
}
$request->replace($params);
}//end if
return $next($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UseSanctumGuard
{
/**
* Handle an incoming request.
*
* @param Request $request Request object.
* @param \Closure $next Closure object.
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
Auth::shouldUse('sanctum');
return $next($request);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class AnalyticsRequest extends BaseRequest
{
/**
* Get the validation rules that apply to POST requests.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'type' => 'required|string',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
return [
'type' => 'string',
'useragent' => 'string',
'ip' => 'ipv4|ipv6',
'session' => 'number',
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class ArticleRequest extends BaseRequest
{
/**
* Get the validation rules that apply to POST requests.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'slug' => 'required|string|min:6|unique:articles',
'title' => 'required|string|min:6|max:255',
'publish_at' => 'required|date',
'user_id' => 'required|uuid|exists:users,id',
'content' => 'required|string|min:6',
'hero' => 'required|uuid|exists:media,id',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
return [
'slug' => [
'string',
'min:6',
Rule::unique('articles')->ignoreModel($this->article),
],
'title' => 'string|min:6|max:255',
'publish_at' => 'date',
'user_id' => 'uuid|exists:users,id',
'content' => 'string|min:6',
'hero' => 'uuid|exists:media,id',
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AuthLoginRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6',
];
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class BaseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) {
return $this->postAuthorize();
} elseif (
(
request()->isMethod('put') === true || request()->isMethod('patch') === true
) && method_exists($this, 'putAuthorize') === true
) {
return $this->putAuthorize();
} elseif (request()->isMethod('delete') === true && method_exists($this, 'destroyAuthorize') === true) {
return $this->deleteAuthorize();
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$rules = [];
if (method_exists($this, 'baseRules') === true) {
$rules = $this->baseRules();
}
if (method_exists($this, 'postRules') === true && request()->isMethod('post') === true) {
$rules = $this->mergeRules($rules, $this->postRules());
} elseif (
method_exists($this, 'putRules') === true && (
request()->isMethod('put') === true || request()->isMethod('patch') === true
)
) {
$rules = $this->mergeRules($rules, $this->putRules());
} elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) {
$rules = $this->mergeRules($rules, $this->destroyRules());
}
return $rules;
}
/**
* Merge two collections of rules.
*
* @param array $collection1 The first collection of rules.
* @param array $collection2 The second collection of rules to merge.
* @return array
*/
private function mergeRules(array $collection1, array $collection2): array
{
$rules = [];
foreach ($collection1 as $key => $ruleset) {
if (array_key_exists($key, $collection2) === true) {
if (is_string($collection1[$key]) === true && is_string($collection2[$key]) === true) {
$rules[$key] = $collection1[$key] . '|' . $collection2[$key];
} else {
$key_ruleset = [];
if (is_array($collection1[$key]) === true) {
$key_ruleset = $collection1[$key];
} elseif (is_string($collection1[$key]) === true) {
$key_ruleset = explode('|', $collection1[$key]);
}
if (is_array($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, $collection2[$key]);
} elseif (is_string($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, explode('|', $collection2[$key]));
}
if (count($key_ruleset) > 0) {
$rules[$key] = $key_ruleset;
}
}//end if
} else {
$rules[$key] = $ruleset;
}//end if
}//end foreach
foreach ($collection2 as $key => $ruleset) {
if (array_key_exists($key, $rules) === false) {
$rules[$key] = $collection2[$key];
}
}
return $rules;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class ContactSendRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255',
'content' => 'required|max:2000',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class EventRequest extends BaseRequest
{
/**
* Apply the base rules to this request
*
* @return array<string, mixed>
*/
public function baseRules(): array
{
return [
'title' => 'min:6',
'location' => [
Rule::in(['online', 'physical']),
],
'address' => 'string|nullable',
'start_at' => 'date',
'end_at' => 'date|after:start_date',
'publish_at' => 'date|nullable',
'status' => [
Rule::in(['draft', 'soon', 'open', 'closed', 'cancelled', 'scheduled', 'full']),
],
'registration_type' => [
Rule::in(['none', 'email', 'link', 'message']),
],
'registration_data' => [
Rule::when(strcasecmp('email', $this->attributes->get('registration_type')) == 0, 'required|email'),
Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url'),
Rule::when(strcasecmp('message', $this->attributes->get('registration_type')) == 0, 'required|message'),
],
'hero' => 'uuid|exists:media,id',
'location_url' => 'sometimes|string|max:255',
];
}
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
protected function postRules(): array
{
return [
'title' => 'required',
'location' => 'required',
'address' => 'required_if:location,physical',
'start_at' => 'required',
'end_at' => 'required',
'status' => 'required',
'registration_type' => 'required',
'hero' => 'required',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class MediaRequest extends BaseRequest
{
/**
* POST request rules
*
* @return array
*/
public function postRules(): array
{
return [
'job_id' => [
Rule::requiredIf(function () {
return request()->has('chunk') && request('chunk') != 1;
}),
'string',
],
'name' => [
Rule::requiredIf(function () {
return request()->has('chunk') && request('chunk') == 1;
}),
'string',
],
'chunk' => 'required_with:chunk_count|integer|min:1|max:999|lte:chunk_count',
'chunk_count' => 'required_with:chunk|integer|min:1',
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class ShortlinkRequest extends BaseRequest
{
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'code' => 'required|string|max:255|min:2|unique:shortlinks',
'url' => 'required|string|max:255|min:2',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
$shortlink = $this->route('shortlink');
return [
'code' => ['required', 'string', 'max:255', 'min:2', Rule::unique('shortlinks')->ignore($shortlink->id)],
'url' => 'required|string|max:255|min:2',
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
class SubscriptionRequest extends BaseRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function postRules(): array
{
return [
'email' => 'required|email|unique:subscriptions',
// 'captcha_token' => [new Recaptcha()],
];
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function destroyRules(): array
{
return [
'email' => 'required|email',
// 'captcha_token' => [new Recaptcha()],
];
}
/**
* Get the custom error messages.
*
* @return array
*/
public function messages(): array
{
return [
'email.unique' => 'This email address has already subscribed',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserForgotPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Rules\Uniqueish;
use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'display_name' => ['required','string','max:255', new Uniqueish('users')],
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
];
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Http\Requests;
use App\Rules\RequiredIfAny;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\RequiredIf;
use App\Rules\Uniqueish;
use Illuminate\Support\Arr;
class UserRequest extends BaseRequest
{
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
public function postRules(): array
{
$user = auth()->user();
$isAdminUser = $user->hasPermission('admin/users');
return [
'first_name' => (
$isAdminUser === true ? 'required_with:last_name,display_name,phone' : 'required'
) . '|string|max:255|min:2',
'last_name' => (
$isAdminUser === true ? 'required_with:first_name,display_name,phone' : 'required'
) . '|string|max:255|min:2',
'display_name' => [
$isAdminUser === true ? 'required_with:first_name,last_name,phone' : 'required',
'string',
'max:255',
new Uniqueish('users')
],
'email' => 'required|string|email|max:255|unique:users',
'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date'
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
public function putRules(): array
{
$user = auth()->user();
$ruleUser = $this->route('user');
$isAdminUser = $user->hasPermission('admin/users');
$requiredIfFieldsPresent = function (array $fields) use ($ruleUser): RequiredIf {
return new RequiredIf(function () use ($fields, $ruleUser) {
$input = $this->all();
$values = Arr::only($input, $fields);
foreach ($values as $key => $value) {
if ($value !== null && $value !== '') {
return true;
}
}
$fields = array_diff($fields, array_keys($values));
foreach ($fields as $field) {
if ($ruleUser->$field !== '') {
return true;
}
}
return false;
});
};
return [
'first_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['last_name', 'display_name', 'phone']) : 'required',
'string',
'between:2,255',
],
'last_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['first_name', 'last_name', 'phone']) : 'required',
'string',
'between:2,255',
],
'display_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['first_name', 'display_name', 'phone']) : 'required',
'string',
'between:2,255',
(new Uniqueish('users', 'display_name'))->ignore($ruleUser->id)
],
'email' => [
'string',
'email',
'max:255',
Rule::unique('users')->ignore($ruleUser->id)->when(
$this->email !== $ruleUser->email,
function ($query) {
return $query->where('email', $this->email);
}
),
],
'phone' => ['nullable', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => "nullable|string|min:8"
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResendVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'code' => 'required|digits:6',
'password' => 'required|string|min:8',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'code' => 'required|digits:6',
// 'captcha_token' => [new Recaptcha()],
];
}
}

393
app/Jobs/MediaWorkerJob.php Normal file
View File

@@ -0,0 +1,393 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use App\Models\MediaJob;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\FFProbe;
use FFMpeg\Format\VideoInterface;
use Intervention\Image\Facades\Image;
/** @property on $format */
class MediaWorkerJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* MediaJob item
*
* @var MediaJob
*/
protected $mediaJob;
/**
* Create a new job instance.
*
* @param MediaJob $mediaJob The mediaJob model.
* @return void
*/
public function __construct(MediaJob $mediaJob)
{
$this->mediaJob = $mediaJob;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
$media = $this->mediaJob->media()->first();
$newMedia = false;
$data = json_decode($this->mediaJob->data, true);
try {
// FILE
if (array_key_exists('file', $data) === true) {
if (file_exists($data['file']) === false) {
$this->throwMediaJobFailure('temporary upload file no longer exists');
}
// convert HEIC files to JPG
$fileExtension = File::extension($data['file']);
if ($fileExtension === 'heic') {
$this->mediaJob->setStatusProcessing(0, 0, 'converting image');
// Get the path without the file name
$uploadedFileDirectory = dirname($data['file']);
// Convert the HEIC file to JPG
$jpgFileName = pathinfo($data['file'], PATHINFO_FILENAME) . '.jpg';
$jpgFilePath = $uploadedFileDirectory . '/' . $jpgFileName;
if (file_exists($jpgFilePath) === true) {
$this->throwMediaJobFailure('file already exists on server');
}
Image::make($data['file'])->save($jpgFilePath);
// Update the uploaded file path and file name
unlink($data['file']);
$data['file'] = $jpgFileName;
}//end if
// get security
$security = [];
if ($media === null) {
if (array_key_exists('security', $data) === true) {
$security = $data['security'];
}
} else {
$security['type'] = $media->security_type;
$security['data'] = $media->security_data;
}
// get storage
$storage = '';
if ($media === null) {
if (array_key_exists('storage', $data) === true) {
$storage = $data['storage'];
}
} else {
$storage = $media->storage;
}
if ($storage === '') {
if (count($security) === 0 || $security['type'] === '') {
if (strpos($data['mime_type'], 'image/') === 0) {
$storage = 'local';
} else {
$storage = 'cdn';
}
} else {
$storage = 'private';
}
}
// Check if file already exists
$exists = Storage::disk($storage)->exists($data['name']);
if ($exists === true) {
if (array_key_exists('noreplace', $data) === true && isTrue($data['noreplace']) === true) {
$this->throwMediaJobFailure('file already exists on server');
}
}
if ($exists === true) {
$pathInfo = pathinfo($data['name']);
$basename = $pathInfo['filename'];
$extension = $pathInfo['extension'];
$index = 0;
do {
$index++;
$data['name'] = $basename . '-' . $index . '.' . $extension;
} while (Storage::disk($storage)->exists($data['name']) === true);
}
if ($media === null) {
$newMedia = true;
$media = new Media([
'user_id' => $this->mediaJob->user_id,
'title' => $data['title'],
'name' => $data['name'],
'mime_type' => $data['mime_type'],
'size' => $data['size'],
'security_type' => $data['security']['type'],
'security_data' => $data['security']['data'],
'storage' => $storage,
]);
}//end if
$media->setStagingFile($data['file']);
} else {
if ($media === null) {
$this->throwMediaJobFailure('The media item no longer exists');
}
}//end if
if (array_key_exists('transform', $data) === true) {
$media->createStagingFile();
// Modifications
if (strpos($media->mime_type, 'image/') === 0) {
$modified = false;
$image = Image::make($media->getStagingFilePath());
// ROTATE
if (array_key_exists("rotate", $data['transform']) === true) {
$rotate = intval($data['transform']['rotate']);
if ($rotate !== 0) {
$this->mediaJob->setStatusProcessing(0, 0, 'rotating image');
$image = $image->rotate($rotate);
$modified = true;
}
}
// FLIP-H/V
if (array_key_exists('flip', $data['transform']) === true) {
if (stripos($data['transform']['flip'], 'h') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping image');
$image = $image->flip('h');
$modified = true;
}
if (stripos($data['transform']['flip'], 'v') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping image');
$image = $image->flip('v');
$modified = true;
}
}
// CROP
if (array_key_exists("crop", $data['transform']) === true) {
$cropData = $data['transform']['crop'];
$width = intval(arrayDefaultValue("width", $cropData, $image->getWidth()));
$height = intval(arrayDefaultValue("height", $cropData, $image->getHeight()));
$x = intval(arrayDefaultValue("x", $cropData, 0));
$y = intval(arrayDefaultValue("y", $cropData, 0));
$this->mediaJob->setStatusProcessing(0, 0, 'cropping image');
$image = $image->crop($width, $height, $x, $y);
$modified = true;
}//end if
if ($modified === true) {
$image->save();
}
} elseif (strpos($data['mime_type'], 'video/') === 0) {
$stagingFilePath = $media->getStagingFilePath();
$ffmpeg = FFMpeg\FFMpeg::create();
$video = $ffmpeg->open($stagingFilePath);
$format = $this->detectVideoFormat($stagingFilePath);
$modified = false;
if ($format === null) {
$this->mediaJob->setStatusFailed('Unsupported video format');
return;
}
/** @var FFMpeg\Media\Video::filters */
$filters = $video->filters();
// ROTATE
if (array_key_exists("rotate", $data['transform']) === true) {
$rotate = intval($data['transform']['rotate']);
$rotate = (($rotate % 360 + 360) % 360); // remove excess rotations
$rotate = intval(round($rotate / 90) * 90); // round to nearest 90%
if ($rotate > 0) {
$this->mediaJob->setStatusProcessing(0, 0, 'rotating video');
if ($rotate === 90) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_270);
$modified = true;
} elseif ($rotate === 180) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_180);
$modified = true;
} elseif ($rotate === 270) {
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_90);
$modified = true;
}
}
}
// FLIP-H/V
if (array_key_exists('flip', $data['transform']) === true) {
if (stripos($data['transform']['flip'], 'h') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping video');
$filters->hflip()->synchronize();
$modified = true;
}
if (stripos($data['transform']['flip'], 'v') !== false) {
$this->mediaJob->setStatusProcessing(0, 0, 'flipping video');
$filters->vflip()->synchronize();
$modified = true;
}
}
// CROP
if (array_key_exists("crop", $data['transform']) === true) {
$cropData = $data['transform']['crop'];
$videoStream = $video->getStreams()->videos()->first();
$width = intval(arrayDefaultValue("width", $cropData, $videoStream->get('width')));
$height = intval(arrayDefaultValue("height", $cropData, $videoStream->get('height')));
$x = intval(arrayDefaultValue("x", $cropData, 0));
$y = intval(arrayDefaultValue("y", $cropData, 0));
$cropDimension = new Dimension($width, $height);
$this->mediaJob->setStatusProcessing(0, 0, 'cropping video');
$filters->crop($cropDimension, $x, $y)->synchronize();
$modified = true;
}//end if
$tempFilePath = generateTempFilePath(pathinfo($stagingFilePath, PATHINFO_EXTENSION));
if (method_exists($format, 'on') === true) {
$mediaJob = $this->mediaJob;
$format->on('progress', function ($video, $format, $percentage) use ($mediaJob) {
$mediaJob->setStatusProcessing($percentage, 100, 'transcoded');
});
}
if ($modified === true) {
$video->save($format, $tempFilePath);
$media->changeStagingFile($tempFilePath);
}
}//end if
}//end if
// Update attributes
if (array_key_exists('title', $data) === true) {
$media->title = $data['title'];
}
// Relocate file (if requested)
if (array_key_exists('security', $data) === true && array_key_exists('type', $data['security']) === true) {
$media->security_type = $data['security']['type'];
$media->security_data = $data['security']['data'];
}
if (array_key_exists('storage', $data) === true) {
if ($media->storage !== $data['storage']) {
$media->createStagingFile();
Storage::disk($media->storage)->delete($media->name);
$media->storage = $data['storage'];
}
}
// Finish media object
if ($media->hasStagingFile() === true) {
$this->mediaJob->setStatusProcessing(0, 0, 'transferring to cdn');
$media->deleteFile();
$media->saveStagingFile(true);
}
$media->save();
$this->mediaJob->media_id = $media->id;
$this->mediaJob->setStatusComplete();
} catch (\Exception $e) {
if ($this->mediaJob->status !== 'failed') {
$this->mediaJob->setStatusFailed('Unexpected server error occurred');
}
if ($media !== null) {
$media->deleteStagingFile();
if ($newMedia === true) {
$media->delete();
}
}
Log::error($e->getMessage() . "\n" . $e->getFile() . " - " . $e->getLine() . "\n" . $e->getTraceAsString());
$this->fail($e);
}//end try
}
/**
* Detects the format of a video using FFProbe
*
* @param string $videoPath The video file path.
* @return VideoInterface | null
*/
public function detectVideoFormat(string $videoPath): VideoInterface | null
{
$ffprobe = FFProbe::create();
$videoStream = $ffprobe
->streams($videoPath) // Provide the path to the video file
->videos() // Filter video streams
->first();
$codecName = $videoStream->get('codec_name');
$codecToFormatClass = [
'h264' => 'FFMpeg\Format\Video\X264',
'wmv2' => 'FFMpeg\Format\Video\WMV',
'vp9' => 'FFMpeg\Format\Video\WebM',
'theora' => 'FFMpeg\Format\Video\Ogg',
'mpeg4' => 'FFMpeg\Format\Video\Mpeg4',
// Add more mappings as needed
];
if (isset($codecToFormatClass[$codecName]) === false) {
Log::info("Unsupported codec: $codecName");
return null;
}
$formatClassName = $codecToFormatClass[$codecName];
if (class_exists($formatClassName) === false) {
Log::info("Format class does not exist: $formatClassName");
return null;
}
return new $formatClassName();
}
/**
* Set failure status of MediaJob and throw exception.
*
* @param string $error The failure message.
* @return void
*/
private function throwMediaJobFailure(string $error): void
{
$this->mediaJob->setStatusFailed($error);
throw new \Exception($error);
}
}

57
app/Jobs/SendEmailJob.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendEmailJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Mail to receipt
*
* @var string
*/
public $to;
/**
* Mailable item
*
* @var Mailable
*/
public $mailable;
/**
* Create a new job instance.
*
* @param string $to The email receipient.
* @param Mailable $mailable The mailable.
* @return void
*/
public function __construct(string $to, Mailable $mailable)
{
$this->to = $to;
$this->mailable = $mailable;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
Mail::to($this->to)->send($this->mailable);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangeEmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* The new email address.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, int $code, string $new_email)
{
$this->user = $user;
$this->code = $code;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👋🏻 Lets change your email!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.change_email_verify',
text: 'emails.user.change_email_verify_plain',
);
}
}

79
app/Mail/ChangedEmail.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedEmail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The old email.
*
* @var string
*/
public $old_email;
/**
* The new email.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param string $old_email The previous email address.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, string $old_email, string $new_email)
{
$this->user = $user;
$this->old_email = $old_email;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👍 Your email has been changed!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.changed_email',
text: 'emails.user.changed_email_plain',
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👍 Your password has been changed!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.changed_password',
text: 'emails.user.changed_password_plain',
);
}
}

78
app/Mail/Contact.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class Contact extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The contact name.
*
* @var string
*/
public $name;
/**
* The contact email.
*
* @var string
*/
public $email;
/**
* The contact content.
*
* @var string
*/
public $content;
/**
* Create a new message instance.
*
* @param string $name The contact name.
* @param string $email The contact email.
* @param string $content The contact content.
* @return void
*/
public function __construct(string $name, string $email, string $content)
{
$this->name = $name;
$this->email = $email;
$this->content = $content;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: config('contact.contact_subject'),
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.contact',
text: 'emails.user.contact_plain',
);
}
}

70
app/Mail/EmailVerify.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class EmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '👋🏻 Welcome to STEMMechanics!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.email_verify',
text: 'emails.user.email_verify_plain',
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ExceptionMail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Exception Mail',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'view.name',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ForgotPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user
*
* @var \App\Models\User
*/
public $user;
/**
* The reset code
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '🤦 Forgot your password?',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.forgot_password',
text: 'emails.user.forgot_password_plain',
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class SubscriptionConfirm extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The email address.
*
* @var string
*/
public $email;
/**
* Create a new message instance.
*
* @param string $email The email address.
* @return void
*/
public function __construct(string $email)
{
$this->email = $email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '🗞️ You\'re on the mailing list!',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.subscription_confirm',
text: 'emails.user.subscription_confirm_plain',
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class SubscriptionUnsubscribed extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The email address.
*
* @var string
*/
public $email;
/**
* Create a new message instance.
*
* @param string $email The email address.
* @return void
*/
public function __construct(string $email)
{
$this->email = $email;
}
/**
* Get the message envelope.
*
* @return Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'You have been unsubscribed',
);
}
/**
* Get the message content definition.
*
* @return Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
view: 'emails.user.subscription_unsubscribed',
text: 'emails.user.subscription_unsubscribed_plain',
);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AnalyticsItemRequest extends Model
{
use HasFactory;
/**
* The table name
*
* @var string
*/
protected $table = 'analytics_requests';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'type',
'path'
];
/**
* Model Boot.
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::creating(function (AnalyticsItemRequest $analytics) {
if (isset($analytics->session_id) !== true) {
$request = request();
if ($request !== null) {
$session = AnalyticsSession::where('ip', $request->ip())
->where('useragent', $request->userAgent())
->where('ended_at', '>=', now()->subMinutes(30))
->first();
if ($session === null) {
$session = AnalyticsSession::create([
'ip' => $request->ip(),
'useragent' => $request->userAgent(),
'ended_at' => now()
]);
} else {
$session->update(['ended_at' => now()]);
}
$analytics->session_id = $session->id;
}
}//end if
});
}
/**
* Return the Analytics Session model.
*
* @return BelongsTo
*/
public function session(): BelongsTo
{
return $this->belongsTo(AnalyticsSession::class, 'id', 'session_id');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class AnalyticsSession extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'ip',
'useragent',
'ended_at'
];
/**
* Set the "useragent" attribute.
*
* @param mixed $value
* @return void
*/
public function setUseragentAttribute($value)
{
$this->attributes['useragent'] = $value !== null ? $value : '';
}
/**
* Returns the related requests for this session.
*
* @return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function requests(): HasMany {
return $this->hasMany(AnalyticsItemRequest::class, 'session_id', 'id');
}
}

43
app/Models/Article.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use App\Traits\HasAttachments;
use App\Traits\HasGallery;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Article extends Model
{
use HasFactory;
use Uuids;
use HasGallery;
use HasAttachments;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'slug',
'publish_at',
'content',
'user_id',
'hero'
];
/**
* Get the article user
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

86
app/Models/Attachment.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Cache;
class Attachment extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'media_id',
'private',
];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'private' => false,
];
/**
* Get the media for this attachment.
*
* @return BelongsTo
*/
public function media(): BelongsTo
{
return $this->belongsTo(Media::class);
}
/**
* Get associated Media object.
*
* @return null|Media
*/
public function getMediaAttribute(): ?Media
{
$mediaId = '0';
$media = null;
if (Cache::has("attachment:{$this->id}:media") === true) {
$mediaId = Cache::get("attachment:{$this->id}:media");
} else {
$media = $this->media()->first();
if ($media === null) {
return null;
}
$mediaId = $media->id;
Cache::put("attachment:{$this->id}:media", $mediaId, now()->addDays(28));
}
return Cache::remember("media:{$mediaId}", now()->addDays(28), function () use ($media) {
if ($media !== null) {
return $media;
}
return $this->media()->first();
});
}
/**
* Set the media for this item.
*
* @param Media $media The media model.
* @return void
*/
public function setMediaAttribute(Media $media): void
{
$this->media()->associate($media)->save();
Cache::put("attachment:{$this->id}:media", $media->id, now()->addDays(28));
}
}

51
app/Models/Event.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models;
use App\Traits\HasAttachments;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Event extends Model
{
use HasAttachments;
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'location',
'location_url',
'address',
'start_at',
'end_at',
'publish_at',
'status',
'registration_type',
'registration_data',
'hero',
'content',
'price',
'ages',
'open_at',
];
/**
* Get all the associated users.
*
* @return BelongsToMany
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'event_user', 'event_id', 'user_id');
}
}

45
app/Models/EventUsers.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EventUser extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'event_id',
'user_id',
];
/**
* Get the event for this attachment.
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
/**
* Get the user for this attachment.
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

107
app/Models/Gallery.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\Cache;
class Gallery extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'media_id',
];
/**
* Boot the model.
*
* @return void
*/
protected static function boot(): void
{
parent::boot();
$clearCache = function ($gallery) {
Cache::forget("gallery:{$gallery->id}:media");
};
static::saving($clearCache);
static::deleting($clearCache);
}
/**
* Get gallery addendum model.
*
* @return Illuminate\Database\Eloquent\Relations\MorphTo Addenum model.
*/
public function addendum(): MorphTo
{
return $this->morphTo();
}
/**
* Get the media for this item.
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo The media model.
*/
public function media(): BelongsTo
{
return $this->belongsTo(Media::class);
}
/**
* Get the media for this item.
*
* @return null|Media The media model.
*/
public function getMediaAttribute(): ?Media
{
$mediaId = '0';
$media = null;
if (Cache::has("gallery:{$this->id}:media") === true) {
$mediaId = Cache::get("gallery:{$this->id}:media");
} else {
$media = $this->media()->first();
if ($media === null) {
return null;
}
$mediaId = $media->id;
Cache::put("gallery:{$this->id}:media", $mediaId, now()->addDays(28));
}
return Cache::remember("media:{$mediaId}", now()->addDays(28), function () use ($media) {
if ($media !== null) {
return $media;
}
return $this->media()->first();
});
}
/**
* Set the media for this item.
*
* @param Media $media The media model.
* @return void
*/
public function setMediaAttribute(Media $media): void
{
$this->media()->associate($media)->save();
Cache::put("gallery:{$this->id}:media", $media->id, now()->addDays(28));
}
}

1064
app/Models/Media.php Normal file

File diff suppressed because it is too large Load Diff

239
app/Models/MediaJob.php Normal file
View File

@@ -0,0 +1,239 @@
<?php
namespace App\Models;
use App\Jobs\MediaWorkerJob;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MediaJob extends Model
{
use HasFactory;
use Uuids;
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'user_id' => null,
'media_id' => null,
'status' => '',
'status_text' => '',
'progress' => 0,
'progress_max' => 0,
'data' => '',
];
/**
* Set MediaJob status to failed.
*
* @param string $statusText The failed reason.
* @return void
*/
public function setStatusFailed(string $statusText = ''): void
{
$this->setStatus('failed', $statusText, 0);
}
/**
* Set MediaJob status to queued.
*
* @return void
*/
public function setStatusQueued(): void
{
$this->setStatus('queued', '', 0);
}
/**
* Set MediaJob status to waiting.
*
* @return void
*/
public function setStatusWaiting(): void
{
$this->setStatus('waiting', '', 0);
}
/**
* Set MediaJob status to processing.
*
* @param integer $progress The processing progress value.
* @param integer $progressMax The processing progress maximum value.
* @param string $statusText The processing status text.
* @return void
*/
public function setStatusProcessing(int $progress = 0, int $progressMax = 0, string $statusText = ''): void
{
if ($statusText === '') {
$statusText = $this->status_text;
}
$this->setStatus('processing', $statusText, $progress, $progressMax);
}
/**
* Set MediaJob status to complete.
*
* @return void
*/
public function setStatusComplete(): void
{
$this->setStatus('complete');
}
/**
* Set MediaJon status to invalid.
*
* @param string $text The status text.
* @return void
*/
public function setStatusInvalid(string $text = ''): void
{
$this->setStatus('invalid', $text);
}
/**
* Set MediaJob status details.
*
* @param string $status The status string.
* @param string $text The status text.
* @param integer $progress The status progress value.
* @param integer $progress_max The status progress maximum value.
* @return void
*/
protected function setStatus(string $status, string $text = '', int $progress = 0, int $progress_max = 0): void
{
$this->status = $status;
$this->status_text = $text;
$this->progress = $progress;
$this->progress_max = $progress_max;
$this->save();
}
/**
* Process the MediaJob.
*
* @return void
*/
public function process(): void
{
$data = json_decode($this->data, true);
if ($data !== null) {
if (array_key_exists('chunks', $data) === true) {
if (array_key_exists('chunk_count', $data) === false) {
$this->setStatusInvalid('chunk_count is missing');
return;
}
if (array_key_exists('name', $data) === false) {
$this->setStatusInvalid('name is missing');
return;
}
$numChunks = count($data['chunks']);
$maxChunks = intval($data['chunk_count']);
if ($numChunks >= $maxChunks) {
// merge file and dispatch
$this->setStatusProcessing(0, $maxChunks, 'combining chunks');
$newFile = generateTempFilePath(pathinfo($data['name'], PATHINFO_EXTENSION));
$failed = false;
for ($index = 1; $index <= $maxChunks; $index++) {
if (array_key_exists($index, $data['chunks']) === false) {
$failed = `{$index} chunk is missing`;
} else {
$tempFileName = $data['chunks'][$index];
if ($failed === false) {
$chunkContents = file_get_contents($tempFileName);
if ($chunkContents === false) {
$failed = `{$index} chunk is empty`;
} else {
file_put_contents($newFile, $chunkContents, FILE_APPEND);
}
}
unlink($tempFileName);
$this->setStatusProcessing($index, $maxChunks);
}
}
unset($data['chunks']);
$this->data = json_encode($data);
if ($failed !== false) {
$this->setStatusInvalid($failed);
} else {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $newFile);
finfo_close($finfo);
$data['file'] = $newFile;
$data['size'] = filesize($newFile);
$data['mime_type'] = $mime;
if (
array_key_exists('storage', $data) === true &&
array_key_exists('security_type', $data) === true &&
array_key_exists('mime_type', $data) === true &&
$data['mime_type'] !== ""
) {
$error = Media::verifyStorage($data['mime_type'], $data['security_type'], $data['storage']);
switch ($error) {
case Media::STORAGE_VALID:
break;
case Media::STORAGE_MIME_MISSING:
$this->setStatusInvalid('The file type cannot be determined.');
return;
case Media::STORAGE_NOT_FOUND:
$this->setStatusInvalid('Storage was not found.');
return;
case Media::STORAGE_INVALID_SECURITY:
$this->setStatusInvalid('Storage invalid for security value.');
return;
default:
$this->setStatusInvalid('Storage verification error occurred.');
return;
}
}
$this->data = json_encode($data);
$this->setStatusQueued();
MediaWorkerJob::dispatch($this)->onQueue('media');
}//end if
}//end if
} else {
$this->setStatusQueued();
MediaWorkerJob::dispatch($this)->onQueue('media');
}//end if
}//end if
}
/**
* Return the job owner
*
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Return the media item
*
* @return BelongsTo
*/
public function media(): BelongsTo
{
return $this->belongsTo(Media::class);
}
}

35
app/Models/Permission.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Permission extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'permission',
'user',
];
/**
* Get the User associated with this model
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

37
app/Models/Shortlink.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Enum\HttpResponseCodes;
use App\Jobs\OptimizeMediaJob;
use App\Traits\Uuids;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Shortlink extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'code',
'url',
];
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
];
}

View File

@@ -3,14 +3,25 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Cache;
use Laravel\Sanctum\HasApiTokens;
use OwenIt\Auditing\Contracts\Auditable;
class User extends Authenticatable
class User extends Authenticatable implements Auditable
{
use HasApiTokens, HasFactory, Notifiable;
use HasApiTokens;
use HasFactory;
use Notifiable;
use Uuids;
use \OwenIt\Auditing\Auditable;
/**
* The attributes that are mass assignable.
@@ -18,9 +29,12 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $fillable = [
'name',
'first_name',
'last_name',
'email',
'phone',
'password',
'display_name',
];
/**
@@ -31,6 +45,7 @@ class User extends Authenticatable
protected $hidden = [
'password',
'remember_token',
'permissions'
];
/**
@@ -40,6 +55,192 @@ class User extends Authenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
// protected $hidden = [
// 'permissions'
// ];
/**
* The attributes to append.
*
* @var string[]
*/
protected $appends = [
'permissions'
];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'phone' => '',
];
/**
* Boot the model.
*
* @return void
*/
protected static function boot(): void
{
parent::boot();
$clearCache = function ($user) {
Cache::forget(
"user:{$user->id}",
"user:{$user->id}:permissions"
);
};
static::saving($clearCache);
static::deleting($clearCache);
}
/**
* Get the list of permissions of the user
*
* @return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function permissions(): HasMany
{
return $this->hasMany(Permission::class);
}
/**
* Get the permission attribute
*
* @return array
*/
public function getPermissionsAttribute(): array
{
$cacheKey = "user:{$this->id}:permissions";
return Cache::remember($cacheKey, now()->addDays(28), function () {
return $this->permissions()->pluck('permission')->toArray();
});
}
/**
* Set the permission attribute
*
* @param array $newPermissions The new permissions to set to the user.
* @return void
*/
public function setPermissionsAttribute(array $newPermissions): void
{
$existingPermissions = $this->permissions->pluck('permission')->toArray();
$this->revokePermission(array_diff($this->permissions, $newPermissions));
$this->givePermission(array_diff($newPermissions, $this->permissions));
$cacheKey = "user:{$this->id}:permissions";
Cache::delete($cacheKey);
}
/**
* Test if user has permission
*
* @param string $permission Permission to test.
* @return boolean
*/
public function hasPermission(string $permission): bool
{
return in_array($permission, $this->permissions);
}
/**
* Give permissions to the user
*
* @param string|array $permissions The permission(s) to give.
* @return Illuminate\Database\Eloquent\Collection
*/
public function givePermission($permissions): Collection
{
if (is_array($permissions) === false) {
$permissions = [$permissions];
}
$newPermissions = array_map(function ($permission) {
return ['permission' => $permission];
}, array_diff($permissions, $this->permissions));
$cacheKey = "user:{$this->id}:permissions";
Cache::forget($cacheKey);
return $this->permissions()->createMany($newPermissions);
}
/**
* Revoke permissions from the user
*
* @param string|array $permissions The permission(s) to revoke.
* @return integer
*/
public function revokePermission($permissions): int
{
if (is_array($permissions) === false) {
$permissions = [$permissions];
}
$cacheKey = "user:{$this->id}:permissions";
Cache::forget($cacheKey);
return $this->permissions()
->whereIn('permission', $permissions)
->delete();
}
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function media(): HasMany
{
return $this->hasMany(Media::class);
}
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
/**
* Get associated user codes
*
* @return HasMany
*/
public function codes(): HasMany
{
return $this->hasMany(UserCode::class);
}
/**
* Get the list of logins of the user
*
* @return HasMany
*/
public function logins(): HasMany
{
return $this->hasMany(UserLogins::class);
}
/**
* Get the events associated with the user.
*
* @return BelongsToMany
*/
public function events(): BelongsToMany
{
return $this->belongsToMany(Event::class, 'event_user', 'user_id', 'event_id');
}
}

83
app/Models/UserCode.php Normal file
View File

@@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserCode extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'action',
'user_id',
'data',
];
/**
* Boot function from Laravel.
*
* @return void
*/
protected static function boot(): void
{
parent::boot();
static::creating(function ($model) {
UserCode::clearExpired();
if (empty($model->{'code'}) === true) {
while (true) {
$code = random_int(100000, 999999);
if (UserCode::where('code', $code)->count() === 0) {
$model->{'code'} = $code;
break;
}
}
}
});
}
/**
* Generate new code
*
* @return void
*/
public function regenerate(): void
{
while (true) {
$code = random_int(100000, 999999);
if (UserCode::where('code', $code)->count() === 0) {
$this->code = $code;
break;
}
}
}
/**
* Clear expired user codes
*
* @return void
*/
public static function clearExpired(): void
{
UserCode::where('updated_at', '<=', now()->subDays(5))->delete();
}
/**
* Get associated user
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

39
app/Models/UserLogins.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserLogins extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'token',
'login',
'logout',
'ip_address',
'user_agent',
];
/**
* Get the file user
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -2,12 +2,16 @@
namespace App\Providers;
use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register(): void
{
@@ -16,9 +20,34 @@ class AppServiceProvider extends ServiceProvider
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
//
Request::macro('rename', function ($param, $newParam = null) {
if (is_array($param) === false) {
if ($newParam === null) {
return;
}
$param = [$param => $newParam];
}
$paramArray = $this->all();
foreach ($param as $oldParam => $newParam) {
if (isset($paramArray[$oldParam]) === true) {
$paramArray[$newParam] = $paramArray[$oldParam];
unset($paramArray[$oldParam]);
}
}
$this->replace($paramArray);
});
Storage::macro('public', function ($diskName) {
$public = config("filesystems.disks.{$diskName}.public", false);
return $public;
});
}
}

View File

@@ -13,11 +13,14 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string>
*/
protected $policies = [
//
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot(): void
{

View File

@@ -9,6 +9,8 @@ class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{

View File

@@ -5,7 +5,10 @@ namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
class EventServiceProvider extends ServiceProvider
{
@@ -20,8 +23,11 @@ class EventServiceProvider extends ServiceProvider
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot(): void
{
@@ -30,6 +36,8 @@ class EventServiceProvider extends ServiceProvider
/**
* Determine if events and listeners should be automatically discovered.
*
* @return boolean
*/
public function shouldDiscoverEvents(): bool
{

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