Compare commits

..

510 Commits

Author SHA1 Message Date
Shift
5e838ad61e Define test classes as final 2023-05-24 21:53:18 +00:00
Shift
a365977702 Ignore PHPUnit cache folder 2023-05-24 21:53:17 +00:00
8b2542ebef removed obsolete recaptcha 2023-05-25 07:49:09 +10:00
James Collins
69c5018367 Merge pull request #49 from STEMMechanics/shift-91885
Laravel 10.x Shift
2023-05-25 07:47:14 +10:00
James Collins
4f536ae5a9 Merge branch 'main' into shift-91885 2023-05-25 07:46:59 +10:00
James Collins
95395d1da7 Merge pull request #50 from STEMMechanics/shift-91887
Laravel Consolidate Namespaces Shift
2023-05-25 07:38:32 +10:00
Shift
c6c639afc2 Apply code style 2023-05-24 21:36:42 +00:00
41147b26f2 dependency updates 2023-05-25 07:35:33 +10:00
Shift
fbf437ac99 Shift cleanup 2023-05-24 21:33:19 +00:00
Shift
5faf49688d Remove redundant typing from DocBlocks 2023-05-24 21:33:17 +00:00
Shift
4d7d0ed74d Add type hints from DocBlocks 2023-05-24 21:33:16 +00:00
Shift
979b9f704c Add type hints for Laravel 10 2023-05-24 21:33:15 +00:00
Shift
4124cf39db Set return type of base TestCase methods
From the [PHPUnit 8 release notes][1], the `TestCase` methods below now declare a `void` return type:

- `setUpBeforeClass()`
- `setUp()`
- `assertPreConditions()`
- `assertPostConditions()`
- `tearDown()`
- `tearDownAfterClass()`
- `onNotSuccessfulTest()`

[1]: https://phpunit.de/announcements/phpunit-8.html
2023-05-24 21:33:13 +00:00
Shift
c83e21d588 Rename password_resets table 2023-05-24 21:33:13 +00:00
Shift
c88630e9af Adopt anonymous migrations 2023-05-24 21:33:12 +00:00
Shift
40b265e145 Bump Laravel dependencies 2023-05-24 21:33:11 +00:00
Shift
3ad2b2fb8e Default config files
In an effort to make upgrading the constantly changing config files
easier, Shift defaulted them and merged your true customizations -
where ENV variables may not be used.
2023-05-24 21:33:11 +00:00
Shift
8b671065e9 Shift config files 2023-05-24 21:33:11 +00:00
Shift
028e1a191e Shift core files 2023-05-24 21:33:08 +00:00
Shift
a133f82997 Remove explicit call to register policies 2023-05-24 21:33:00 +00:00
Shift
c4f3eb9a4e Remove default lang files 2023-05-24 21:32:59 +00:00
Shift
8a52c4529f Use Faker methods
Accessing Faker properties was deprecated in Faker 1.14.
2023-05-24 21:32:59 +00:00
Shift
d0493f3dd0 Convert string references to ::class
PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.
2023-05-24 21:32:58 +00:00
Shift
b845552c37 Apply code style 2023-05-24 21:32:56 +00:00
4e97209494 fixes to support analytics 2023-05-25 07:18:28 +10:00
aa0b010bed ignore certain spellings 2023-05-25 07:18:28 +10:00
cb79ea64cf filter changes to support collections 2023-05-25 07:18:28 +10:00
James Collins
38409d0d63 Merge pull request #48 from STEMMechanics/dependabot/npm_and_yarn/eslint-plugin-jsdoc-44.2.5
Bump eslint-plugin-jsdoc from 39.9.1 to 44.2.5
2023-05-25 07:17:35 +10:00
James Collins
32dfb4eef3 Merge pull request #47 from STEMMechanics/dependabot/composer/guzzlehttp/guzzle-7.7.0
Bump guzzlehttp/guzzle from 7.6.1 to 7.7.0
2023-05-25 07:17:19 +10:00
James Collins
a6de64a089 Merge pull request #41 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.1.3
Bump phpunit/phpunit from 9.6.7 to 10.1.3
2023-05-25 07:17:01 +10:00
James Collins
b226814676 Merge pull request #34 from STEMMechanics/dependabot/npm_and_yarn/vite-plugin-compression2-0.9.1
Bump vite-plugin-compression2 from 0.8.4 to 0.9.1
2023-05-25 07:16:45 +10:00
James Collins
805de3291b Merge pull request #30 from STEMMechanics/dependabot/npm_and_yarn/vue-final-modal-4.4.2
Bump vue-final-modal from 3.4.11 to 4.4.2
2023-05-25 07:16:13 +10:00
James Collins
b0ab63e30e Merge pull request #27 from STEMMechanics/dependabot/npm_and_yarn/prettier-2.8.8
Bump prettier from 2.8.2 to 2.8.8
2023-05-25 07:15:50 +10:00
James Collins
13bfc52b77 Merge pull request #23 from STEMMechanics/dependabot/npm_and_yarn/tinymce/tinymce-vue-5.1.0
Bump @tinymce/tinymce-vue from 4.0.7 to 5.1.0
2023-05-25 07:15:22 +10:00
dependabot[bot]
bbce78cab4 Bump phpunit/phpunit from 9.6.7 to 10.1.3
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.6.7 to 10.1.3.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.1.3/ChangeLog-10.1.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/9.6.7...10.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 21:13:12 +00:00
James Collins
1e084d5131 Merge pull request #22 from STEMMechanics/dependabot/composer/nunomaduro/collision-7.1.0
Bump nunomaduro/collision from 6.4.0 to 7.1.0
2023-05-25 07:12:27 +10:00
dependabot[bot]
af699161da Bump eslint-plugin-jsdoc from 39.9.1 to 44.2.5
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.9.1 to 44.2.5.
- [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/v39.9.1...v44.2.5)

---
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>
2023-05-24 01:11:37 +00:00
2e1c2cd0b2 added autocomplete 2023-05-22 17:31:50 +10:00
cd7366b8ff remove debug 2023-05-22 16:45:22 +10:00
ec74f6594c prettier 2023-05-22 16:29:27 +10:00
5d2e9affc0 support editing/deleting multiple items 2023-05-22 16:17:44 +10:00
06b7ce4db0 added 2023-05-22 16:17:26 +10:00
dependabot[bot]
4bf695f559 Bump guzzlehttp/guzzle from 7.6.1 to 7.7.0
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.6.1 to 7.7.0.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/7.7/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.6.1...7.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-22 01:46:03 +00:00
59daa1ff08 fix disabling css 2023-05-21 17:51:27 +10:00
04044673e2 added gap 2023-05-21 17:51:19 +10:00
3b837dc6b0 fix checkboxes not always checking 2023-05-21 09:27:51 +10:00
2f029e2523 dont reduce size of checkbox labels when active 2023-05-21 09:02:42 +10:00
66d477795f better handle 503 errors 2023-05-21 08:50:14 +10:00
78f23db801 updated maintenance page 2023-05-21 07:50:29 +10:00
e62a21c469 loop images 2023-05-20 22:02:49 +10:00
9756622148 fix click propagation 2023-05-20 21:57:39 +10:00
065cb1b746 added prev/next arrows 2023-05-20 21:49:43 +10:00
54a7ad86dc dependencies update 2023-05-20 20:56:44 +10:00
0e7c86ac2b fix properties on email inputs 2023-05-20 20:46:37 +10:00
e023964cb2 added media sorting to editor 2023-05-20 20:13:35 +10:00
16f4eb65ef empty sort now reverts to default instead of 503 2023-05-20 19:24:28 +10:00
11eb12324e when saving object, show original query list 2023-05-20 18:34:06 +10:00
7a0d3fc8a0 fix delete option 2023-05-20 17:57:52 +10:00
3ed2aadc34 media controller should not directly delete files 2023-05-20 17:56:48 +10:00
b2004e3483 fix not uploading multiple files 2023-05-20 17:49:04 +10:00
55f363a64f reduce gap around images 2023-05-20 16:55:55 +10:00
7a6ed9f7f4 reduce banner height 2023-05-20 16:55:47 +10:00
8fa8c85077 fix status check 2023-05-20 16:42:15 +10:00
245ffc9d45 include tinymce themes 2023-05-20 16:42:05 +10:00
6a9a2f0a9e firefox fix 2023-05-19 14:17:18 +10:00
9dbefe5a8a add page loading icon 2023-05-19 14:00:15 +10:00
cce2a79ee4 fix table name 2023-05-19 13:41:38 +10:00
8e94ab2d7d rel=prefetch 2023-05-19 13:37:32 +10:00
6e0337cdeb prefetching and performance improvements 2023-05-19 13:21:02 +10:00
1c3b8f065e remove older code 2023-05-19 13:20:53 +10:00
c43d5574b4 removed elements page 2023-05-19 12:50:49 +10:00
0e5c654b02 fix potential sql injections 2023-05-18 09:33:57 +10:00
14d6d59581 dependency updates 2023-05-16 15:29:51 +10:00
3796961293 added copy shortlink option to dropdown 2023-05-16 11:19:34 +10:00
c471a97a23 add event users 2023-05-11 16:49:12 +10:00
fc853bd5f1 add private option to attachments 2023-05-11 13:37:40 +10:00
d0ea0ae4d3 support empty values with prefixes 2023-05-11 09:14:37 +10:00
8a6d1281bb remove debug 2023-05-11 09:06:17 +10:00
8797d51ef4 default filter on status is OK 2023-05-11 09:04:58 +10:00
2c8ac1f155 Revert media status to OK 2023-05-11 09:04:39 +10:00
42706de9df added defaultFilters option 2023-05-11 09:04:23 +10:00
3ce99b8751 improve error handling on upload failures 2023-05-11 08:40:30 +10:00
44c4f16c5c fix test styling 2023-05-11 08:40:10 +10:00
cdccde528e added --loading variables 2023-05-11 08:12:32 +10:00
f32655c156 fix firefox missing :has selector 2023-05-10 20:31:53 +10:00
d7255f004d use const form 2023-05-10 20:21:16 +10:00
56a1aaa19c duplicate form object 2023-05-10 20:20:59 +10:00
86491bfb2e use seperate form id 2023-05-10 20:20:50 +10:00
8dc43ccfce support form-id and check form exists 2023-05-10 20:20:34 +10:00
7ff49700fd support form-id 2023-05-10 20:20:27 +10:00
e14c7aafb3 added status page link 2023-05-10 12:42:21 +10:00
0f727168be dependency updates 2023-05-09 14:39:30 +10:00
0de47b3104 added location url option 2023-05-09 10:58:51 +10:00
6b3eb97568 dont validate empty url strings 2023-05-09 10:58:43 +10:00
bd4ba41b0b fix small screens and make full height 2023-05-09 10:30:41 +10:00
fb2f2d9739 removed forgot-username 2023-05-09 10:22:10 +10:00
a468ae01ff fix rules 2023-05-09 10:07:59 +10:00
6d534bd1c3 revert put requirements 2023-05-08 21:59:42 +10:00
31820317de check if password exists on login 2023-05-08 21:53:37 +10:00
e2efa1f1bd updated rules 2023-05-08 21:51:52 +10:00
86a0936cd4 only allow ghost users by admins 2023-05-08 21:46:07 +10:00
645b623a40 ignore id on fallback 2023-05-08 21:45:27 +10:00
96bd56a828 obsolete 2023-05-08 21:45:11 +10:00
870f1c5194 fix phone requirement 2023-05-08 20:28:26 +10:00
5d1adf7af8 support creating ghost users 2023-05-08 20:25:01 +10:00
a1170a1347 added RequiredIf option 2023-05-08 20:24:54 +10:00
9b5aab6e6e modelValue should support boolean 2023-05-08 20:08:23 +10:00
a25776fbbb fix grammer 2023-05-08 19:40:46 +10:00
54b5929aa4 forced 0 margin top 2023-05-08 19:40:41 +10:00
729fc3fd39 added shortlinks on frontend 2023-05-08 19:28:07 +10:00
b4cf05ad44 fixed error code 2023-05-08 19:28:01 +10:00
4da8b32b1a added Length 2023-05-08 19:26:02 +10:00
c35342df59 fix active watch 2023-05-08 19:18:27 +10:00
7d8d407d07 fix button dropdown colours in dark mode 2023-05-08 19:16:46 +10:00
1ceb109a28 fix append item height 2023-05-08 19:11:40 +10:00
a8181ff2b7 fix static active 2023-05-08 19:09:53 +10:00
e42c4c3023 fix row column + buttonrow 2023-05-08 17:29:22 +10:00
ff040eec58 added title and description upload support 2023-05-08 17:27:01 +10:00
5d663d21b3 check lastDialog exists before resolving 2023-05-08 17:26:52 +10:00
b73c2d3726 emit value if non set on init 2023-05-08 16:53:17 +10:00
ee96acbe4f use modelValue to automatic selection changes 2023-05-08 16:39:42 +10:00
fd22b79d42 dynamic disabling 2023-05-08 16:39:27 +10:00
99f56b9ef8 cleanup hovering 2023-05-08 16:09:52 +10:00
9a686c1112 added tab-color-hover 2023-05-08 16:09:44 +10:00
ffcf823a7f reduced top margin 2023-05-08 15:16:50 +10:00
a85c4bf115 added no-help property 2023-05-08 15:16:25 +10:00
29d7167c24 fix borders 2023-05-08 15:11:09 +10:00
3c6a570394 fix label padding on small controls 2023-05-08 14:43:46 +10:00
419fa322a3 use default alignment of toolbar 2023-05-08 14:41:34 +10:00
71a2e1b6dd update media button to medium size 2023-05-08 14:41:25 +10:00
a352c21198 fix typing 2023-05-08 12:36:11 +10:00
4accb60a24 use new format 2023-05-08 12:34:33 +10:00
88b92a9572 remove footer top margin 2023-05-08 12:32:20 +10:00
e39fa78981 cleanup 2023-05-08 12:25:59 +10:00
4205113b00 block width 100% 2023-05-08 12:25:45 +10:00
ad47efefaf cleanup 2023-05-08 12:21:03 +10:00
890399dd74 set default first/last name 2023-05-08 12:20:57 +10:00
2698ede55e added fallback 2023-05-08 12:11:20 +10:00
c9f0ea2512 remove macros 2023-05-08 12:11:14 +10:00
228f9c7c6b remove obsolete fields 2023-05-08 12:11:09 +10:00
63e05e924a fix footer on small displays 2023-05-08 11:16:56 +10:00
a66cde3934 show form errors 2023-05-08 11:14:53 +10:00
0b8a2904ec cleaned up 2023-05-08 11:14:44 +10:00
5fffa97ea7 lightened danger lighter 2023-05-08 11:14:36 +10:00
1d14d86d8e added 2023-05-08 11:14:21 +10:00
217ab89667 fix padding 2023-05-08 10:43:48 +10:00
ac2dd23ad7 remove usernames 2023-05-08 10:40:48 +10:00
7a4f72378d control-help div always shown 2023-05-08 10:40:33 +10:00
8190839823 added new rules 2023-05-08 10:40:04 +10:00
4e9d97268f remove any query items 2023-05-06 22:38:32 +10:00
985f7c94da force ssl and remove obsolete stuff 2023-05-06 22:38:24 +10:00
3d28e73369 added used counter 2023-05-06 18:29:03 +10:00
4076b138b9 wrong schema 2023-05-06 18:23:24 +10:00
dependabot[bot]
99d4c709bd Bump nunomaduro/collision from 6.4.0 to 7.1.0
Bumps [nunomaduro/collision](https://github.com/nunomaduro/collision) from 6.4.0 to 7.1.0.
- [Release notes](https://github.com/nunomaduro/collision/releases)
- [Changelog](https://github.com/nunomaduro/collision/blob/v7.x/CHANGELOG.md)
- [Commits](https://github.com/nunomaduro/collision/compare/v6.4.0...v7.1.0)

---
updated-dependencies:
- dependency-name: nunomaduro/collision
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 08:21:01 +00:00
93951cfbc8 shortlink support 2023-05-06 18:20:17 +10:00
4ac86c434e added phpdotenv 2023-05-06 18:17:07 +10:00
171cfa7aab added table 2023-05-06 18:16:53 +10:00
a494dbe662 remove recapcha statement 2023-05-06 15:30:33 +10:00
c9d02fb11c dont parse data 2023-05-05 21:35:02 +10:00
3ba65385c5 override padding 2023-05-04 19:02:09 +10:00
58a2da1996 updated homepage 2023-05-04 18:58:53 +10:00
845d6ba12b use correct timezone 2023-05-04 18:46:58 +10:00
f0b55b7b2e added total to UserCollection 2023-05-04 16:49:32 +10:00
e6dd75c2a8 dont show 4th card on single column 2023-05-04 06:52:03 +10:00
7b7154085e dependency updates 2023-05-04 06:48:06 +10:00
1c119e80e9 fix style colouring 2023-05-04 06:46:15 +10:00
3d86f859c6 darkmode support and new variables 2023-05-03 21:49:55 +10:00
0bfad00df7 fix saving attachments 2023-05-03 20:46:14 +10:00
64efa723b3 fix incorrect index 2023-05-03 20:46:07 +10:00
0072b28965 updated front page layout 2023-05-03 20:23:35 +10:00
21fa5d24af added 3rd accent 2023-05-03 20:23:19 +10:00
16ec3c515e added align-items-stretch 2023-05-03 20:23:13 +10:00
6868144e25 fix identifying sessions 2023-05-03 07:35:55 +10:00
0e5af96900 added use 2023-05-02 21:42:25 +10:00
ded5caf271 restructure api request 2023-05-02 21:31:30 +10:00
e8e1e91d1c update the media value url 2023-05-02 21:05:07 +10:00
6a16d545ec remove debug 2023-05-02 20:51:05 +10:00
976b6fbb78 update image gallery 2023-05-02 20:49:36 +10:00
efc5571fb3 hide container when no toasts are present 2023-05-02 20:28:43 +10:00
6ad4b3a6c4 embed variant types into Media model 2023-05-02 19:16:39 +10:00
cc0fe080cf analytics backend update 2023-05-01 19:04:08 +10:00
dependabot[bot]
1de89fba5f Bump vite-plugin-compression2 from 0.8.4 to 0.9.1
Bumps [vite-plugin-compression2](https://github.com/nonzzz/vite-compression-plugin) from 0.8.4 to 0.9.1.
- [Release notes](https://github.com/nonzzz/vite-compression-plugin/releases)
- [Changelog](https://github.com/nonzzz/vite-compression-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonzzz/vite-compression-plugin/compare/v0.8.4...v0.9.1)

---
updated-dependencies:
- dependency-name: vite-plugin-compression2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 01:25:02 +00:00
0c668c9c62 fix styling on smaller screens 2023-04-28 08:57:17 +10:00
6a3c3a3566 fix small screen layout 2023-04-27 16:51:52 +10:00
e17f79e0b1 improve small screen layout 2023-04-27 16:51:46 +10:00
f0461fb65f update to buttonrow 2023-04-27 16:51:38 +10:00
aa38927522 upgrade to large 2023-04-27 15:57:01 +10:00
881d06deea reduce margin for edit button 2023-04-27 15:45:32 +10:00
683869214b added view opton 2023-04-27 15:43:36 +10:00
3322a6e005 added margin between buttons 2023-04-27 15:33:17 +10:00
65a48454ba missing button-block class 2023-04-27 15:33:11 +10:00
0de8e17593 declare time before usage 2023-04-27 14:42:21 +10:00
17beb4152b fix helper text 2023-04-27 14:42:13 +10:00
b09097294f fix const 2023-04-27 14:09:33 +10:00
c6e1b0248d performance improvements 2023-04-27 14:05:34 +10:00
0a956e1fc5 show large images 2023-04-27 13:35:02 +10:00
e2dee426bf fix test expectation 2023-04-27 13:33:29 +10:00
37a738c094 fix toTitleCase 2023-04-27 13:31:02 +10:00
bef4c3440b performance improvements 2023-04-27 13:24:40 +10:00
b36ad8042f use webp image 2023-04-27 08:34:50 +10:00
6ec38853ff store images locally by default 2023-04-27 07:25:13 +10:00
69144a665f updated config 2023-04-27 06:55:33 +10:00
382a1d0ef8 move cdn 2023-04-27 05:47:23 +10:00
41c751a76d just use medium size 2023-04-26 21:45:34 +10:00
01e46042fb change to webp 2023-04-26 21:42:01 +10:00
f1a28b6efe change to webp 2023-04-26 21:41:43 +10:00
7fddeeeaae dont force webp as original is not 2023-04-26 21:16:33 +10:00
bacf35bb4b use webp 2023-04-26 21:15:27 +10:00
4a83c7e171 explicitly use webp 2023-04-26 20:58:39 +10:00
3cbce25394 performance improvements 2023-04-26 20:34:04 +10:00
dependabot[bot]
c2b58cc82d Bump vue-final-modal from 3.4.11 to 4.4.2
Bumps [vue-final-modal](https://github.com/vue-final/vue-final-modal/tree/HEAD/packages/vue-final-modal) from 3.4.11 to 4.4.2.
- [Release notes](https://github.com/vue-final/vue-final-modal/releases)
- [Changelog](https://github.com/vue-final/vue-final-modal/blob/master/packages/vue-final-modal/CHANGELOG.md)
- [Commits](https://github.com/vue-final/vue-final-modal/commits/v4.4.2/packages/vue-final-modal)

---
updated-dependencies:
- dependency-name: vue-final-modal
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-26 10:26:28 +00:00
ac326d2d74 disable recaptcha 2023-04-26 20:25:38 +10:00
50aaf8b343 added analytics box 2023-04-26 20:08:25 +10:00
f667ac6430 fix margins 2023-04-26 20:01:50 +10:00
e2e599ed35 bugfixes 2023-04-26 20:00:19 +10:00
897e422e15 dont replace some statuses after end 2023-04-26 19:55:24 +10:00
0031be6882 add progress text 2023-04-26 19:55:10 +10:00
45880ed7a8 remove debug 2023-04-26 18:52:39 +10:00
b7e964174a add upload progress 2023-04-26 18:52:23 +10:00
77b8f60cb1 fix styling 2023-04-26 18:52:16 +10:00
4cbde00ef3 added disabled to other types 2023-04-26 18:32:15 +10:00
196472181e added disabled class 2023-04-26 18:32:05 +10:00
2fa7a0c7a5 update fields when file changes 2023-04-26 18:19:11 +10:00
38e982e70b fix styling 2023-04-26 18:08:35 +10:00
8a9b57547a fix double search 2023-04-26 18:01:20 +10:00
6dd3d6255d fix small styling 2023-04-26 18:01:13 +10:00
656de567d7 fix css for disabled input 2023-04-26 17:54:16 +10:00
24e80d7851 fix hero loading in edit 2023-04-26 17:47:22 +10:00
bc642b48da move edit button higher 2023-04-26 17:45:41 +10:00
cc07998a8a bugfix temp file creation 2023-04-26 17:33:34 +10:00
178309bb1e updated autoload 2023-04-26 17:33:23 +10:00
40c19f47c7 added temp helper 2023-04-26 17:33:16 +10:00
7f82b24b0c fix css priority 2023-04-26 16:02:35 +10:00
f31a8da0e1 added edit button 2023-04-26 13:22:20 +10:00
8ed158ab3c input select shows current value 2023-04-26 12:30:47 +10:00
08af379a57 bug fixes 2023-04-26 12:29:34 +10:00
79dbdd3e5a fix margin 2023-04-26 12:29:28 +10:00
f7e8d5bdf7 remove obsolete options 2023-04-26 12:29:23 +10:00
52eba56e34 remove obsolete variable 2023-04-26 12:13:39 +10:00
4c42276deb bug fixes 2023-04-26 12:00:26 +10:00
b4eb772662 whitespacing 2023-04-26 12:00:11 +10:00
825730c3f9 reduce margins on medium size 2023-04-26 12:00:04 +10:00
b7a2253b01 add userHasPermission helper 2023-04-26 11:59:53 +10:00
ce1174d41b remove download options 2023-04-26 11:32:02 +10:00
f2da168a03 updated 2023-04-26 11:29:22 +10:00
fec4b29261 updated 2023-04-26 11:29:18 +10:00
01b8dadd5f rounded corners 2023-04-26 11:29:06 +10:00
3ee97468f9 change posts to articles 2023-04-26 10:57:27 +10:00
c6d318bbc3 use large instead of scaled image 2023-04-26 09:38:31 +10:00
4ebb07a79a fix missing calc 2023-04-26 09:35:34 +10:00
2580d0874f bug fixes 2023-04-25 19:34:01 +10:00
2168e693d8 support dark-mode 2023-04-25 19:24:20 +10:00
bc2a25346b dont display if no items 2023-04-25 11:57:39 +10:00
03e969e08c fix icon spacing 2023-04-25 11:55:18 +10:00
37e3872782 fix margins on small devices 2023-04-25 11:44:29 +10:00
71442e4160 remove debug code 2023-04-25 11:39:40 +10:00
c2b69a769a use new range input 2023-04-24 21:56:31 +10:00
c0bc0e03c0 added range type 2023-04-24 21:56:25 +10:00
f49bef1112 added rules 2023-04-24 21:30:37 +10:00
191b2978ec update css 2023-04-24 21:23:25 +10:00
2771cdd053 remove debug 2023-04-24 20:53:33 +10:00
2d576645d8 update community images and links 2023-04-24 19:21:09 +10:00
be9884b468 set dialog width 2023-04-24 18:31:04 +10:00
df280ce7a6 change discord button name 2023-04-24 18:29:50 +10:00
c1182b3b90 fix classes 2023-04-24 15:38:58 +10:00
425a3561ac change SMFooter to SMPageFooter 2023-04-24 15:37:11 +10:00
12be354cc9 remove sm- prefix 2023-04-24 15:25:36 +10:00
7e2917d447 obsolete 2023-04-24 15:25:31 +10:00
cb0de6a2d5 obsolete 2023-04-24 15:21:48 +10:00
95318d1b36 update SMFormFooter to SMButtonRow 2023-04-24 15:21:05 +10:00
b92456c178 update all SMHeaders 2023-04-24 14:59:59 +10:00
bcb25b5d5e grammar 2023-04-24 14:42:31 +10:00
6a79204204 update position 2023-04-24 14:41:48 +10:00
d3776a8d3f bugfix SMHeader scroll 2023-04-24 14:40:48 +10:00
325c6a0448 update to new SMHeader 2023-04-24 14:14:49 +10:00
7c089eed80 remove old smheading 2023-04-24 14:14:43 +10:00
8b40a46c1b added component 2023-04-24 14:11:33 +10:00
ef4061b96c updates rules page 2023-04-24 13:40:59 +10:00
dependabot[bot]
506849b450 Bump prettier from 2.8.2 to 2.8.8
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.2 to 2.8.8.
- [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/2.8.2...2.8.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 02:25:08 +00:00
dependabot[bot]
e73ce8c943 Bump @tinymce/tinymce-vue from 4.0.7 to 5.1.0
Bumps [@tinymce/tinymce-vue](https://github.com/tinymce/tinymce-vue) from 4.0.7 to 5.1.0.
- [Release notes](https://github.com/tinymce/tinymce-vue/releases)
- [Changelog](https://github.com/tinymce/tinymce-vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tinymce/tinymce-vue/compare/4.0.7...5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 02:24:28 +00:00
bb4543bc65 dependency update 2023-04-24 12:24:09 +10:00
James Collins
f8d5850a89 Create dependabot.yml 2023-04-24 12:24:03 +10:00
cce994d35c added minecraft curve 2023-04-24 12:18:06 +10:00
02cfb53147 Added SMSocialIcons component 2023-04-24 12:16:48 +10:00
0755df03cf use exact active class 2023-04-24 10:35:28 +10:00
51f0ad7497 only add variant if created 2023-04-24 09:53:47 +10:00
b4a49d20c8 fix styling 2023-04-24 08:40:41 +10:00
44ccaf10d4 test for NaN price 2023-04-24 08:22:29 +10:00
967c14b93a bug fixes 2023-04-23 20:44:11 +10:00
0e42fc657b missing handleinput on select 2023-04-23 20:37:02 +10:00
2d7a91e368 fix params.id 2023-04-23 20:35:31 +10:00
072ab038fe fix missimg media icon and select styling 2023-04-23 20:34:52 +10:00
6723c27d5e ignore missing media 2023-04-23 20:28:17 +10:00
f5fc700886 typos 2023-04-23 20:25:15 +10:00
96a5ba0ceb media create 2023-04-23 20:21:08 +10:00
dfe5e72526 fix event create 2023-04-23 20:20:07 +10:00
d1cbebee84 sort oldest to newest 2023-04-23 20:02:23 +10:00
eac313feb8 footer spacing 2023-04-23 20:00:23 +10:00
a71ed56cf2 align text center 2023-04-23 19:57:08 +10:00
e3bc72e8d2 bugfix 2023-04-23 19:55:39 +10:00
0fadfed7f6 bug fixes 2023-04-23 19:53:10 +10:00
031db78590 bug fixes 2023-04-23 19:25:52 +10:00
6fd32fb84b support small 2023-04-23 16:42:58 +10:00
98e6464be6 support media input 2023-04-23 16:19:15 +10:00
52ce2afad2 added media type 2023-04-23 16:19:05 +10:00
28f89c9469 cleanup 2023-04-23 16:18:57 +10:00
6f7de8da66 reduce margin 2023-04-23 15:42:10 +10:00
50caf2753d added checkbox 2023-04-23 15:42:05 +10:00
30c0caa04d FormControl value is know unknown 2023-04-23 15:41:53 +10:00
e53d8c14a9 added Booleanish 2023-04-23 15:41:41 +10:00
eca89358db show status 2023-04-23 14:35:45 +10:00
1e58c71e67 fix search fields 2023-04-23 14:32:20 +10:00
43beaefc07 reverse sort 2023-04-23 14:32:00 +10:00
f74faadace fix date range filter 2023-04-23 14:30:05 +10:00
77aa622610 bug fixes 2023-04-23 13:56:27 +10:00
95aadd45ee bug fixes 2023-04-23 12:18:14 +10:00
89880016ea loading icon 2023-04-22 22:34:35 +10:00
beb91553ef min height 2023-04-22 22:31:05 +10:00
b2037c9575 css updates 2023-04-22 22:25:42 +10:00
ce4cec5589 fix 2023-04-22 21:25:04 +10:00
e8a597ec6b disable dark mode temp 2023-04-22 21:21:25 +10:00
a663e2bd56 updates 2023-04-22 21:18:07 +10:00
84bfd3cda2 updates 2023-04-21 15:46:12 +10:00
3dfe96fa89 updated css 2023-04-21 11:49:37 +10:00
93cbcef93f updated 2023-04-21 11:37:20 +10:00
5c758536a4 renamed 2023-04-21 11:37:13 +10:00
bc5a9aa9f1 remove image 2023-04-21 11:37:05 +10:00
b8ed77f6d5 add option to replace existing files 2023-04-21 10:07:17 +10:00
6c25cd029f added support to ignore existing files 2023-04-21 10:05:40 +10:00
68d59eda69 fix migration rename column issues 2023-04-21 09:58:05 +10:00
2534d4c159 dependency update 2023-04-21 08:55:32 +10:00
54252e768c dependency updates 2023-04-21 07:12:41 +10:00
7a2f263061 updates 2023-04-21 07:11:00 +10:00
5ae6e02ce8 updates 2023-04-19 22:31:47 +10:00
fb9944ef14 lots of changes 2023-04-19 16:26:13 +10:00
190493179f cleanup 2023-04-19 14:52:32 +10:00
4b1bc23622 added loading support 2023-04-19 14:49:15 +10:00
2ad5b04a48 added large option 2023-04-19 14:49:03 +10:00
afbbbcb4d1 remove debug 2023-04-19 14:28:23 +10:00
820c3aec9d remove debug permission 2023-04-19 14:27:31 +10:00
eafbcd8389 cleanup 2023-04-19 14:26:37 +10:00
f0459b3f6e cleanup 2023-04-19 14:26:27 +10:00
ff93265890 show banners and dates 2023-04-19 13:40:38 +10:00
320e282dc8 added banner colors 2023-04-19 13:40:29 +10:00
d23c911c78 fix ul margin 2023-04-19 13:13:51 +10:00
51df812a6c add display_name support 2023-04-19 09:40:35 +10:00
a96aba57f7 add php test debugging 2023-04-19 09:40:20 +10:00
36c71da4bb lots o updates 2023-04-18 21:47:44 +10:00
b53fca9648 removed dataset 2023-04-18 17:04:24 +10:00
9fafb8bd2a removed 2023-04-18 17:03:33 +10:00
41c7ba35a0 table width 100% 2023-04-18 16:59:57 +10:00
4cc5702da7 updated text 2023-04-18 15:47:17 +10:00
65f626f15e added cod 2023-04-18 15:47:09 +10:00
2abf6f67af added cod 2023-04-18 15:47:04 +10:00
72cde997ab added italic, updated small 2023-04-18 15:46:54 +10:00
8b27cb4690 removed small, added li 2023-04-18 15:46:47 +10:00
fb0cec0850 fix error not applying to border 2023-04-18 15:29:25 +10:00
a9a0bfdad0 fix small div 2023-04-18 15:26:47 +10:00
00a752173d added new formcard 2023-04-18 15:24:41 +10:00
24caa9a4f4 added space-between 2023-04-18 15:24:28 +10:00
a1075e000a fix styling 2023-04-18 15:11:52 +10:00
c416902280 cleanup 2023-04-18 15:04:06 +10:00
a29d707183 toolbar should be 100% 2023-04-18 15:04:00 +10:00
69d08a85ac input should be 100% 2023-04-18 15:03:51 +10:00
475ea08517 changes! 2023-04-18 13:52:36 +10:00
b4c97c20d6 inner container items 2023-04-18 13:52:28 +10:00
f7da2c8185 override default container center 2023-04-18 13:52:19 +10:00
a1a630fc02 align container center default 2023-04-18 13:52:10 +10:00
22ef117493 missing important 2023-04-18 13:51:58 +10:00
04b80d5ff8 updated darkmode 2023-04-18 13:35:06 +10:00
b764979c3b added accent-2 dark 2023-04-18 13:34:57 +10:00
a78c0491ef added accent-2 2023-04-18 13:32:25 +10:00
6082beb964 updated 2023-04-18 13:32:16 +10:00
9d9a5fd9d2 added narrow option 2023-04-18 13:01:56 +10:00
4332f389a1 change to use body page instead of data-set 2023-04-18 13:01:45 +10:00
a26b60e726 added accent colors 2023-04-18 12:48:40 +10:00
465d76cd08 added click to hide 2023-04-18 12:48:28 +10:00
e0300148cf cleanup 2023-04-18 12:08:02 +10:00
4442c6c625 cleanup styling 2023-04-18 12:07:58 +10:00
289eb86d97 cleanup 2023-04-18 12:07:51 +10:00
59724777e9 update header sizes 2023-04-18 12:07:46 +10:00
99e0b297b2 added change emitter 2023-04-18 11:29:38 +10:00
7036747042 use center option 2023-04-18 10:44:02 +10:00
b9cd3e3f9f added transitions 2023-04-18 10:43:54 +10:00
c8e90b6887 default is start, added center option 2023-04-18 10:43:25 +10:00
56973b62f6 added extra items 2023-04-18 10:43:15 +10:00
990a13e777 fixes 2023-04-18 10:21:55 +10:00
cd37623746 cleanup 2023-04-18 10:21:45 +10:00
193620e4e4 reduce font weight 2023-04-18 09:55:34 +10:00
78d85e2440 added clear option 2023-04-18 09:55:25 +10:00
9fa9689db9 fix padding on icon only buttons 2023-04-18 09:55:16 +10:00
81fc33183c changed dropdown to chevron 2023-04-18 09:20:21 +10:00
2600011736 fix dropdown clicks and overflow 2023-04-18 09:11:25 +10:00
e5c297eb7c hide easydatatable 2023-04-18 09:11:15 +10:00
a3766aca6c apply margin-top to all h3 2023-04-18 08:29:20 +10:00
857689dc22 remove obsolete loader 2023-04-18 08:29:08 +10:00
2ed5917e96 css disabled 2023-04-18 08:29:00 +10:00
84380bf333 #app as flex 2023-04-18 08:28:53 +10:00
e7d517f264 use new table 2023-04-17 22:57:40 +10:00
40a9cc424e cleanup 2023-04-17 22:57:35 +10:00
b725bc2b5b change search button to icon 2023-04-17 22:57:28 +10:00
50306c319e added 2023-04-17 22:57:18 +10:00
c1e86c6897 added back links 2023-04-17 22:57:07 +10:00
eb02142afc bug fixes 2023-04-17 22:56:49 +10:00
5f0526eef7 cleanup 2023-04-17 19:56:39 +10:00
cbdc55df8f fix scrollbar padding 2023-04-17 19:56:33 +10:00
e0022b15c5 remove obsolete code 2023-04-17 19:56:25 +10:00
983edc53d1 update workshop route name 2023-04-17 19:38:41 +10:00
2af1dcd24e update router namespace 2023-04-17 19:38:28 +10:00
2814a5f044 fix responsive 2023-04-17 19:34:42 +10:00
802fd87850 change button to primary type 2023-04-17 19:34:34 +10:00
50a6a39632 dark mode always on home page 2023-04-17 19:28:49 +10:00
49d0d3b35a updated page 2023-04-17 16:11:57 +10:00
8017f017f2 bug fixes 2023-04-17 16:11:51 +10:00
864798be7c updated h3 2023-04-17 16:11:45 +10:00
e20ef40e02 remove input group 2023-04-17 16:01:04 +10:00
955f9021f7 updated the new input group slots 2023-04-17 15:58:37 +10:00
e006090be2 removed 2023-04-17 15:58:27 +10:00
1c6bc56e08 added prepend and append slots 2023-04-17 15:58:21 +10:00
aa2da29b4c remove input group 2023-04-17 15:19:39 +10:00
9e47d28660 bug fix 2023-04-17 15:19:33 +10:00
a5383c87c7 cleanup 2023-04-17 15:07:53 +10:00
152a637e31 update button font color 2023-04-17 15:07:45 +10:00
5d947000ca fix validator and font weight on smaller 2023-04-17 15:07:35 +10:00
bf4f378108 added cta button 2023-04-17 14:58:08 +10:00
7f03228efa added sizes to button 2023-04-17 14:57:59 +10:00
7e6fd1859e added offset option 2023-04-17 14:30:41 +10:00
979c77c1b9 bring back model function and cleanup 2023-04-17 14:26:42 +10:00
15c9603902 support new conductor features 2023-04-17 14:11:42 +10:00
20dd8bcb3a apply includes if no fields are set 2023-04-17 14:11:33 +10:00
bec4b03a17 return a blank string when name attribute 2023-04-17 13:54:37 +10:00
2686a162e7 cleanup and fields support includes fields 2023-04-17 13:54:17 +10:00
7d9c982cf5 bug fixes and updates 2023-04-17 07:16:31 +10:00
d1c09ce74e added 2023-04-13 07:39:30 +10:00
fe5f429039 dependency udates 2023-04-12 18:53:28 +10:00
8937571214 fix background colors 2023-04-12 14:08:33 +10:00
f9591951cb fix footer sheme 2023-04-12 14:08:19 +10:00
956d2a25f2 fix navbar variables 2023-04-12 14:08:10 +10:00
365bec10a6 added support for scheme 2023-04-12 13:45:14 +10:00
c69c11b0fe added base-darker 2023-04-12 13:45:08 +10:00
40b8414f8a update css 2023-04-12 13:39:08 +10:00
06cb735b68 update component 2023-04-12 13:39:03 +10:00
985f32e06e update page 2023-04-12 13:38:57 +10:00
36469b20b3 use variables 2023-04-12 13:38:28 +10:00
2173e4c6b8 update footer 2023-04-12 13:38:19 +10:00
d7a35e651e remove breadcrumb and bg reference 2023-04-12 13:38:08 +10:00
4238c977f3 added dark/light logo 2023-04-12 10:07:39 +10:00
74e9a3204f added dark/light d-none 2023-04-12 10:06:54 +10:00
7bf94ced84 added css variables 2023-04-12 09:53:41 +10:00
1b3a40c22a support css scheme 2023-04-12 09:53:28 +10:00
28bef07e37 update color 2023-04-12 09:53:05 +10:00
3b7cb57e7a fix hover and spacing css 2023-04-12 08:56:42 +10:00
3f069e6d22 removed depreciated progressbar 2023-04-12 08:49:29 +10:00
12e7269591 removed depreciated carousel 2023-04-12 08:49:20 +10:00
0127cf0a6b added fontaine 2023-04-12 08:44:54 +10:00
fd1522a2ca fix event test to specifically set status 2023-04-11 17:10:53 +10:00
c79ec065d3 update structure 2023-04-11 14:09:10 +10:00
19fe484049 fix factory 2023-04-11 14:09:03 +10:00
db3d831bc0 remove whitespace 2023-04-11 14:08:53 +10:00
d27e707044 add permission default value 2023-04-11 14:08:41 +10:00
c142440068 cleanup 2023-04-11 13:19:48 +10:00
df6456f5d2 fix error changing defaults after rename 2023-04-11 13:15:24 +10:00
1578a2b8d1 fix bad table name in drop 2023-04-11 13:14:57 +10:00
4a43c152d5 remove unused constraints 2023-04-11 13:14:50 +10:00
0d1ee37272 remove page transitions 2023-04-10 21:23:15 +10:00
5c0b97cd1e h1 align left 2023-04-10 21:23:08 +10:00
511e8d6074 update post view 2023-04-10 21:18:31 +10:00
826e4a7de2 remove breadcrumbs 2023-04-10 21:18:24 +10:00
1ac66b4ece update default background color 2023-04-10 21:18:16 +10:00
8f58de9f4e remove carousel 2023-04-10 20:52:13 +10:00
611d997df9 remove padding 2023-04-10 20:52:07 +10:00
3f66e3f1f1 remove page loader 2023-04-10 20:51:59 +10:00
6154fa5dcc added new element 2023-04-10 20:46:31 +10:00
dd0914cd89 started removal of rounded borders 2023-04-10 20:46:25 +10:00
b8000d9a64 white hamburger 2023-04-10 19:51:13 +10:00
d94bd66c54 white stem 2023-04-10 19:51:05 +10:00
2ebd2018db updated navbar design 2023-04-10 19:25:54 +10:00
04b41e16e1 added components 2023-04-10 18:03:02 +10:00
7ad73f3c84 bug fixes and support new media 2023-04-10 14:49:53 +10:00
359698d54f added apiAttachmentResource 2023-04-10 14:49:27 +10:00
26ea658f9c updated types 2023-04-10 14:49:13 +10:00
a13be0530f added media variants helper 2023-04-10 14:49:07 +10:00
b018b11c57 fix docs 2023-04-10 14:48:55 +10:00
aac023351a remove logging 2023-04-10 14:48:42 +10:00
fd2fbea03f S3 jobs 2023-04-10 14:47:53 +10:00
f3bbdec77c rewrote to support S3 2023-04-10 14:47:38 +10:00
b54ace0272 added respondAccepted 2023-04-10 14:47:09 +10:00
55bc78d9cb return fulfilled hero and user 2023-04-10 14:47:00 +10:00
fabe027d54 changed default permission from null to '' 2023-04-10 14:46:48 +10:00
b4f4450573 return fulfilled hero 2023-04-10 14:46:21 +10:00
990cc66600 support null models 2023-04-10 14:45:58 +10:00
aa76147144 added public/private aws and CF 2023-04-10 14:45:46 +10:00
ee46af08ca update media table 2023-04-10 14:44:23 +10:00
c33de944ef dependencies update 2023-04-10 14:44:08 +10:00
79e5103b08 updated config 2023-04-06 21:20:54 +10:00
4797b213ee added clamav config 2023-04-06 21:18:54 +10:00
c232042af5 use static function in Media modalk 2023-04-06 21:17:38 +10:00
252448f4a9 added uniqueFileName rule 2023-04-06 21:14:57 +10:00
ec4febe5e9 added additional rows for media table 2023-04-06 21:12:12 +10:00
35ca0d90f7 added the route macro apiAttachmentResource 2023-04-06 17:28:07 +10:00
eae3d4689b add the storage macro public 2023-04-06 17:27:29 +10:00
67c9d4084c added sunspikes/clamav-validator 2023-04-06 17:26:10 +10:00
fb52428219 update dependencies 2023-04-06 17:25:31 +10:00
23e620e168 reduce to XLarge 2023-04-06 08:52:43 +10:00
324054b3db fix button text justification 2023-04-06 08:36:40 +10:00
d9dcbeef7b preview uses thumb size 2023-04-05 19:20:04 +10:00
9b31d52d7e renamed header to Files 2023-04-05 19:18:47 +10:00
260d2d28ad support relational links 2023-04-05 15:56:58 +10:00
dd74bdda6a explicit ask for small images 2023-04-05 15:53:21 +10:00
7486390da5 show file to large error 2023-04-05 14:53:35 +10:00
399 changed files with 43442 additions and 11931 deletions

View File

@@ -38,11 +38,24 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID= AWS_PUBLIC_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_PUBLIC_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1 AWS_PUBLIC_DEFAULT_REGION="us-west-002"
AWS_BUCKET= AWS_PUBLIC_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false 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_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=

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"

4
.gitignore vendored
View File

@@ -26,7 +26,7 @@ storage/*.key
Homestead.yaml Homestead.yaml
Homestead.json Homestead.json
/.vagrant /.vagrant
.phpunit.result.cache /.phpunit.cache
### macOS ### ### macOS ###
# General # General
@@ -237,7 +237,7 @@ dist/
### This Project ### ### This Project ###
/public/uploads /public/uploads
/public/build /public/build
/public/tinymce # /public/tinymce
*.key *.key
### Synk ### ### Synk ###

View File

@@ -15,5 +15,6 @@
"[php]": { "[php]": {
// "editor.defaultFormatter": "bmewburn.vscode-intelephense-client" // "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
"editor.defaultFormatter": "wongjn.php-sniffer" "editor.defaultFormatter": "wongjn.php-sniffer"
} },
"cSpell.words": ["TIMESTAMPDIFF"]
} }

View File

@@ -0,0 +1,83 @@
<?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 LogicException;
class AnalyticsConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Analytics::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The default includes to include in a request.
*
* @var array
*/
protected $includes = ['duration'];
/**
* 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
{
$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
{
$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
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/analytics') === true);
}
}

View File

@@ -0,0 +1,151 @@
<?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 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'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
*/
public function scope(Builder $builder): void
{
$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) {
$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
{
$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
{
$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
{
$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->attachments()->get()->map(function ($attachment) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
});
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array The new value.
*/
public function transformHero(mixed $value): array
{
return MediaConductor::includeModel(request(), 'hero', Media::find($value));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
namespace App\Conductors; namespace App\Conductors;
use App\Models\Media;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException; use Illuminate\Database\Eloquent\InvalidCastException;
@@ -13,22 +14,27 @@ class EventConductor extends Conductor
* The Model Class * The Model Class
* @var string * @var string
*/ */
protected $class = '\App\Models\Event'; protected $class = \App\Models\Event::class;
/** /**
* The default sorting field * The default sorting field
* @var string * @var string
*/ */
protected $sort = 'start_at'; protected $sort = '-start_at';
/**
* The included fields
* @var string[]
*/
protected $includes = ['attachments'];
/** /**
* Run a scope query on the collection before anything else. * Run a scope query on the collection before anything else.
* *
* @param Builder $builder The builder in use. * @param Builder $builder The builder in use.
* @return void
*/ */
public function scope(Builder $builder) public function scope(Builder $builder): void
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) { if ($user === null || $user->hasPermission('admin/events') === false) {
@@ -44,7 +50,7 @@ class EventConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow model to be visible. * @return boolean Allow model to be visible.
*/ */
public static function viewable(Model $model) public static function viewable(Model $model): bool
{ {
if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) { if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user(); $user = auth()->user();
@@ -61,7 +67,7 @@ class EventConductor extends Conductor
* *
* @return boolean Allow creating model. * @return boolean Allow creating model.
*/ */
public static function creatable() public static function creatable(): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true); return ($user !== null && $user->hasPermission('admin/events') === true);
@@ -73,7 +79,7 @@ class EventConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow updating model. * @return boolean Allow updating model.
*/ */
public static function updatable(Model $model) public static function updatable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true); return ($user !== null && $user->hasPermission('admin/events') === true);
@@ -85,26 +91,37 @@ class EventConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow deleting model. * @return boolean Allow deleting model.
*/ */
public static function destroyable(Model $model) public static function destroyable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true); return ($user !== null && $user->hasPermission('admin/events') === true);
} }
/** /**
* Transform the model * Include Attachments Field.
* *
* @param Model $model The model to transform. * @param Model $model Them model.
* @return array The transformed model. * @return mixed The model result.
* @throws InvalidCastException Cannot cast item to model.
*/ */
public function transform(Model $model) public function includeAttachments(Model $model)
{ {
$result = $model->toArray(); $user = auth()->user();
$result['attachments'] = $model->attachments()->get()->map(function ($attachment) {
return MediaConductor::model(request(), $attachment->media);
});
return $result; return $model->attachments()->get()->map(function ($attachment) use ($user) {
if ($attachment->private === false || ($user !== null && ($user->hasPermission('admin/events') === true || $attachment->users->contains($user) === true))) {
return MediaConductor::includeModel(request(), 'attachments', $attachment->media);
}
});
}
/**
* Transform the Hero field.
*
* @param mixed $value The current value.
* @return array The new value.
*/
public function transformHero(mixed $value): array
{
return MediaConductor::includeModel(request(), 'hero', Media::find($value));
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
class MediaConductor extends Conductor class MediaConductor extends Conductor
{ {
@@ -11,7 +12,7 @@ class MediaConductor extends Conductor
* The Model Class * The Model Class
* @var string * @var string
*/ */
protected $class = '\App\Models\Media'; protected $class = \App\Models\Media::class;
/** /**
* The default sorting field * The default sorting field
@@ -19,6 +20,22 @@ class MediaConductor extends Conductor
*/ */
protected $sort = 'created_at'; protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user'];
/**
* 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. * Return an array of model fields visible to the current user.
@@ -26,13 +43,13 @@ class MediaConductor extends Conductor
* @param Model $model The model in question. * @param Model $model The model in question.
* @return array The array of field names. * @return array The array of field names.
*/ */
public function fields(Model $model) public function fields(Model $model): array
{ {
$fields = parent::fields($model); $fields = parent::fields($model);
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/media') === false) { if ($user === null || $user->hasPermission('admin/media') === false) {
$fields = arrayRemoveItem($fields, 'permission'); $fields = arrayRemoveItem($fields, ['permission', 'storage']);
} }
return $fields; return $fields;
@@ -42,15 +59,14 @@ class MediaConductor extends Conductor
* Run a scope query on the collection before anything else. * Run a scope query on the collection before anything else.
* *
* @param Builder $builder The builder in use. * @param Builder $builder The builder in use.
* @return void
*/ */
public function scope(Builder $builder) public function scope(Builder $builder): void
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null) { if ($user === null) {
$builder->whereNull('permission'); $builder->where('permission', '');
} else { } else {
$builder->whereNull('permission')->orWhereIn('permission', $user->permissions); $builder->where('permission', '')->orWhereIn('permission', $user->permissions);
} }
} }
@@ -60,9 +76,9 @@ class MediaConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow model to be visible. * @return boolean Allow model to be visible.
*/ */
public static function viewable(Model $model) public static function viewable(Model $model): bool
{ {
if ($model->permission !== null) { if ($model->permission !== '') {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission($model->permission) === false) { if ($user === null || $user->hasPermission($model->permission) === false) {
return false; return false;
@@ -77,7 +93,7 @@ class MediaConductor extends Conductor
* *
* @return boolean Allow creating model. * @return boolean Allow creating model.
*/ */
public static function creatable() public static function creatable(): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null); return ($user !== null);
@@ -89,7 +105,7 @@ class MediaConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow updating model. * @return boolean Allow updating model.
*/ */
public static function updatable(Model $model) public static function updatable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 || $user->hasPermission('admin/media') === true)); return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 || $user->hasPermission('admin/media') === true));
@@ -101,9 +117,32 @@ class MediaConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow deleting model. * @return boolean Allow deleting model.
*/ */
public static function destroyable(Model $model) public static function destroyable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true)); 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)
{
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
}
} }

View File

@@ -1,109 +0,0 @@
<?php
namespace App\Conductors;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class PostConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Post';
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
* @return void
*/
public function scope(Builder $builder)
{
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === 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)
{
if (Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable()
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model)
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true);
}
/**
* Transform the model
*
* @param Model $model The model to transform.
* @return array The transformed model.
* @throws InvalidCastException Cannot cast item to model.
*/
public function transform(Model $model)
{
$result = $model->toArray();
$result['attachments'] = $model->attachments()->get()->map(function ($attachment) {
return MediaConductor::model(request(), $attachment->media);
});
return $result;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
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
{
$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
{
$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
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/shortlinks') === true);
}
}

View File

@@ -10,7 +10,7 @@ class SubscriptionConductor extends Conductor
* The Model Class * The Model Class
* @var string * @var string
*/ */
protected $class = '\App\Models\Subscription'; protected $class = \App\Models\Subscription::class;
/** /**
@@ -19,7 +19,7 @@ class SubscriptionConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow updating model. * @return boolean Allow updating model.
*/ */
public static function updatable(Model $model) public static function updatable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true)); return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true));
@@ -31,7 +31,7 @@ class SubscriptionConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow deleting model. * @return boolean Allow deleting model.
*/ */
public static function destroyable(Model $model) public static function destroyable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true)); return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true));

View File

@@ -10,7 +10,7 @@ class UserConductor extends Conductor
* The Model Class * The Model Class
* @var string * @var string
*/ */
protected $class = '\App\Models\User'; protected $class = \App\Models\User::class;
/** /**
@@ -19,11 +19,11 @@ class UserConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return string[] The fields visible. * @return string[] The fields visible.
*/ */
public function fields(Model $model) public function fields(Model $model): array
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) { if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'username']; return ['id', 'display_name'];
} }
return parent::fields($model); return parent::fields($model);
@@ -35,14 +35,16 @@ class UserConductor extends Conductor
* @param Model $model The model to transform. * @param Model $model The model to transform.
* @return array The transformed model. * @return array The transformed model.
*/ */
public function transform(Model $model) public function transform(Model $model): array
{ {
$user = auth()->user(); $user = auth()->user();
$data = $model->toArray(); $data = $model->toArray();
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) { if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
$fields = ['id', 'username']; $fields = ['id', 'display_name'];
$data = arrayLimitKeys($data, $fields); $data = arrayLimitKeys($data, $fields);
} else {
$data['permissions'] = $user->permissions;
} }
return $data; return $data;
@@ -54,7 +56,7 @@ class UserConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow updating model. * @return boolean Allow updating model.
*/ */
public static function updatable(Model $model) public static function updatable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
if ($user !== null) { if ($user !== null) {
@@ -70,7 +72,7 @@ class UserConductor extends Conductor
* @param Model $model The model. * @param Model $model The model.
* @return boolean Allow deleting model. * @return boolean Allow deleting model.
*/ */
public static function destroyable(Model $model) public static function destroyable(Model $model): bool
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/users') === true); return ($user !== null && $user->hasPermission('admin/users') === true);

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StoreUploadedFileJob;
use Illuminate\Console\Command;
use App\Models\Media;
use File;
use Symfony\Component\Console\Input\InputOption;
class MediaMigrate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:migrate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate the uploads folder to the CDN';
/**
* Configure the command options.
*/
protected function configure(): void
{
$this->addOption(
'replace',
null,
InputOption::VALUE_NONE,
'Replace existing files'
);
}
/**
* Execute the console command.
*/
public function handle(): void
{
$replace = $this->option('replace');
$files = File::allFiles(public_path('uploads'));
foreach ($files as $file) {
$filename = pathinfo($file, PATHINFO_BASENAME);
$medium = Media::where('name', $filename)->first();
if ($medium !== null) {
$medium->update(['status' => 'Processing media']);
StoreUploadedFileJob::dispatch($medium, $file, $replace)->onQueue('media');
} else {
unlink($file);
}
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StoreUploadedFileJob;
use Illuminate\Console\Command;
use App\Models\Media;
use Symfony\Component\Console\Input\InputOption;
class MediaRebuild extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:rebuild';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rebuild the media table';
/**
* Configure the command options.
*/
protected function configure(): void
{
$this->addOption(
'replace',
null,
InputOption::VALUE_NONE,
'Replace existing files'
);
$this->addOption(
'all',
null,
InputOption::VALUE_NONE,
'Rebuild all variants'
);
}
/**
* Execute the console command.
*/
public function handle(): void
{
$replace = $this->option('replace');
$all = $this->option('replace');
$media = [];
if ($all === true) {
$media = Media::all();
} else {
$media = Media::where(['variants' => ''])->orWhere(['variants' => '[]'])->orWhere(['variants' => '{}'])->get();
}
foreach ($media as $medium) {
StoreUploadedFileJob::dispatch($medium, '', $replace)->onQueue('media');
}
}
}

View File

@@ -11,19 +11,16 @@ class Kernel extends ConsoleKernel
* Define the application's command schedule. * Define the application's command schedule.
* *
* @param \Illuminate\Console\Scheduling\Schedule $schedule The schedule. * @param \Illuminate\Console\Scheduling\Schedule $schedule The schedule.
* @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule): void
{ {
// $schedule->command('inspire')->hourly(); // $schedule->command('inspire')->hourly();
} }
/** /**
* Register the commands for the application. * Register the commands for the application.
*
* @return void
*/ */
protected function commands() protected function commands(): void
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');

View File

@@ -80,7 +80,7 @@ class CurlErrorCodes extends Enum
public const CURLE_RTSP_SESSION_ERROR = 86; public const CURLE_RTSP_SESSION_ERROR = 86;
public const CURLE_FTP_BAD_FILE_LIST = 87; public const CURLE_FTP_BAD_FILE_LIST = 87;
public const CURLE_CHUNK_FAILED = 88; public const CURLE_CHUNK_FAILED = 88;
/** /**
* Curl Error messages * Curl Error messages
@@ -163,7 +163,7 @@ class CurlErrorCodes extends Enum
86 => 'CURLE_RTSP_SESSION_ERROR.', 86 => 'CURLE_RTSP_SESSION_ERROR.',
87 => 'CURLE_FTP_BAD_FILE_LIST.', 87 => 'CURLE_FTP_BAD_FILE_LIST.',
88 => 'CURLE_CHUNK_FAILED.', 88 => 'CURLE_CHUNK_FAILED.',
]; ];
} }

View File

@@ -8,7 +8,7 @@ class Enum
{ {
/** /**
* Message list * Message list
* *
* @var array<string<static>> * @var array<string<static>>
*/ */
public static $messages = []; public static $messages = [];
@@ -57,12 +57,12 @@ class Enum
/** /**
* Returns a message from the enum subclass * Returns a message from the enum subclass
* *
* @return string * @return string
*/ */
public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string
{ {
if(array_key_exists($messageIndex, self::$messages) === true) { if (array_key_exists($messageIndex, self::$messages) === true) {
return self::$messages[$messageIndex]; return self::$messages[$messageIndex];
} }

View File

@@ -12,24 +12,6 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/** /**
* A 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.
* *
@@ -44,10 +26,8 @@ class Handler extends ExceptionHandler
/** /**
* Register the exception handling callbacks for the application. * Register the exception handling callbacks for the application.
*
* @return void
*/ */
public function register() public function register(): void
{ {
// $this->renderable(function (HttpException $e, $request) { // $this->renderable(function (HttpException $e, $request) {
// if ($request->is('api/*')) { // if ($request->is('api/*')) {

View File

@@ -11,7 +11,7 @@ class SubscriptionFilter extends FilterAbstract
* *
* @var mixed * @var mixed
*/ */
protected $class = '\App\Models\Subscription'; protected $class = \App\Models\Subscription::class;
/** /**

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

@@ -0,0 +1,19 @@
<?php
/* Temp File Helper Functions */
/**
* Generate a temporary file path.
*
* @return str The filtered array.
*/
function generateTempFilePath(): string
{
$temporaryDir = storage_path('app/tmp');
if (is_dir($temporaryDir) === false) {
mkdir($temporaryDir, 0777, true);
}
return $temporaryDir . DIRECTORY_SEPARATOR . uniqid('upload_', true);
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\AnalyticsConductor;
use App\Conductors\Conductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AnalyticsRequest;
use App\Models\Media;
use App\Models\Analytics;
use Illuminate\Http\JsonResponse;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MassAssignmentException;
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) {
$searchFields = ['attribute', 'type', 'useragent', 'ip'];
$queryRequest = new Request();
$queryRequest->merge($request->only($searchFields));
foreach ($searchFields as $field) {
unset($request[$field]);
}
$query = Analytics::query()
->selectRaw('session,
MIN(created_at) as created_at,
TIMESTAMPDIFF(MINUTE, MIN(created_at), MAX(created_at)) as duration');
$query = Conductor::filterQuery($query, $queryRequest);
list($collection, $total) = AnalyticsConductor::collection($request, $query
->groupBy('session')
->get());
return $this->respondAsResource(
$collection,
['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\Analytics $analytics The analyics model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, int $session)
{
if ($request->user() !== null && $request->user()?->hasPermission('admin/analytics') === true) {
list($collection, $total) = AnalyticsConductor::collection($request, Analytics::query()
->where('session', $session)
->get());
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
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'),
'attribute' => $request->input('attribute', ''),
'useragent' => $request->userAgent(),
'ip' => $request->ip()
];
if ($user !== null && $user->hasPermission('admin/analytics') === true && $request->has('session') === true) {
$data['session'] = $request->input('session');
$analytics = Analytics::create($data);
} else {
$analytics = Analytics::createWithSession($data);
}
return $this->respondAsResource(
AnalyticsConductor::model($request, $analytics),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}//end if
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\AnalyticsRequest $request The analytics update request.
* @param \App\Models\Analytics $analytics The specified analytics.
* @return \Illuminate\Http\Response
*/
public function update(AnalyticsRequest $request, Analytics $analytics)
{
if (AnalyticsConductor::updatable($analytics) === true) {
$analytics->update($request->all());
return $this->respondAsResource(AnalyticsConductor::model($request, $analytics));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Analytics $analytics The specified analytics.
* @return \Illuminate\Http\Response
*/
public function destroy(Analytics $analytics)
{
if (AnalyticsConductor::destroyable($analytics) === true) {
$analytics->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -23,9 +24,8 @@ class ApiController extends Controller
* @param array $data Response data. * @param array $data Response data.
* @param integer $respondCode Response status code. * @param integer $respondCode Response status code.
* @param array $headers Response headers. * @param array $headers Response headers.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondJson(array $data, int $respondCode = HttpResponseCodes::HTTP_OK, array $headers = []) public function respondJson(array $data, int $respondCode = HttpResponseCodes::HTTP_OK, array $headers = []): JsonResponse
{ {
return response()->json($data, $respondCode, $headers); return response()->json($data, $respondCode, $headers);
} }
@@ -34,9 +34,8 @@ class ApiController extends Controller
* Return forbidden message * Return forbidden message
* *
* @param string $message Response message. * @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondForbidden(string $message = 'You do not have permission to access the resource.') public function respondForbidden(string $message = 'You do not have permission to access the resource.'): JsonResponse
{ {
return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN); return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN);
} }
@@ -45,9 +44,8 @@ class ApiController extends Controller
* Return forbidden message * Return forbidden message
* *
* @param string $message Response message. * @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondNotFound(string $message = 'The resource was not found.') public function respondNotFound(string $message = 'The resource was not found.'): JsonResponse
{ {
return response()->json(['message' => $message], HttpResponseCodes::HTTP_NOT_FOUND); return response()->json(['message' => $message], HttpResponseCodes::HTTP_NOT_FOUND);
} }
@@ -56,39 +54,43 @@ class ApiController extends Controller
* Return too large message * Return too large message
* *
* @param string $message Response message. * @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondTooLarge(string $message = 'The request entity is too large.') public function respondTooLarge(string $message = 'The request entity is too large.'): JsonResponse
{ {
return response()->json(['message' => $message], HttpResponseCodes::HTTP_REQUEST_ENTITY_TOO_LARGE); return response()->json(['message' => $message], HttpResponseCodes::HTTP_REQUEST_ENTITY_TOO_LARGE);
} }
/** /**
* Return no content * Return no content
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondNoContent() public function respondNoContent(): JsonResponse
{ {
return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT); return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT);
} }
/** /**
* Return created * Return created
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondCreated() public function respondCreated(): JsonResponse
{ {
return response()->json([], HttpResponseCodes::HTTP_CREATED); return response()->json([], HttpResponseCodes::HTTP_CREATED);
} }
/**
* Return accepted
*/
public function respondAccepted(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_ACCEPTED);
}
/** /**
* Return single error message * Return single error message
* *
* @param string $message Error message. * @param string $message Error message.
* @param integer $responseCode Resource code. * @param integer $responseCode Resource code.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondError(string $message, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY) public function respondError(string $message, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY): JsonResponse
{ {
return response()->json([ return response()->json([
'message' => $message 'message' => $message
@@ -100,9 +102,8 @@ class ApiController extends Controller
* *
* @param array $errors Error messages. * @param array $errors Error messages.
* @param integer $responseCode Resource code. * @param integer $responseCode Resource code.
* @return \Illuminate\Http\JsonResponse
*/ */
public function respondWithErrors(array $errors, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY) public function respondWithErrors(array $errors, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY): JsonResponse
{ {
$keys = array_keys($errors); $keys = array_keys($errors);
$error = $errors[$keys[0]]; $error = $errors[$keys[0]];
@@ -121,28 +122,35 @@ class ApiController extends Controller
/** /**
* Return resource data * Return resource data
* *
* @param array|Model|Collection $data Resource data. * @param array|Model|Collection $data Resource data.
* @param array $options Respond options. * @param array $options Respond options.
* @return \Illuminate\Http\JsonResponse
*/ */
protected function respondAsResource( protected function respondAsResource(
mixed $data, mixed $data,
array $options = [], array $options = [],
) { $validationFn = null
): JsonResponse {
$isCollection = $options['isCollection'] ?? false; $isCollection = $options['isCollection'] ?? false;
$appendData = $options['appendData'] ?? null; $appendData = $options['appendData'] ?? null;
$resourceName = $options['resourceName'] ?? null; $resourceName = $options['resourceName'] ?? null;
$respondCode = $options['respondCode'] ?? HttpResponseCodes::HTTP_OK; $respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
if ($data === null || ($data instanceof Collection && $data->count() === 0)) { if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
return $this->respondNotFound(); $validationData = [];
if (array_key_exists('appendData', $options) === true) {
$validationData = $options['appendData'];
}
if ($validationFn === null || $validationFn($validationData) === true) {
return $this->respondNotFound();
}
} }
if(is_null($resourceName) === true || empty($resourceName) === true) { if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = $this->resourceName; $resourceName = $this->resourceName;
} }
if(is_null($resourceName) === true || empty($resourceName) === true) { if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = get_class($this); $resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1)); $resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller')); $resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));

View File

@@ -3,11 +3,11 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor; use App\Conductors\MediaConductor;
use App\Conductors\PostConductor; use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Requests\PostRequest; use App\Http\Requests\ArticleRequest;
use App\Models\Media; use App\Models\Media;
use App\Models\Post; use App\Models\Article;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PostController extends ApiController class ArticleController extends ApiController
{ {
/** /**
* ApplicationController constructor. * ApplicationController constructor.
@@ -38,12 +38,13 @@ class PostController extends ApiController
*/ */
public function index(Request $request) public function index(Request $request)
{ {
list($collection, $total) = PostConductor::request($request); list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }
@@ -51,13 +52,13 @@ class PostController extends ApiController
* Display the specified resource. * Display the specified resource.
* *
* @param \Illuminate\Http\Request $request The endpoint request. * @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Post $post The post model. * @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show(Request $request, Post $post) public function show(Request $request, Article $article)
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -66,15 +67,15 @@ class PostController extends ApiController
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The user request. * @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(PostRequest $request) public function store(ArticleRequest $request)
{ {
if (PostConductor::creatable() === true) { if (ArticleConductor::creatable() === true) {
$post = Post::create($request->all()); $article = Article::create($request->all());
return $this->respondAsResource( return $this->respondAsResource(
PostConductor::model($request, $post), ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED] ['respondCode' => HttpResponseCodes::HTTP_CREATED]
); );
} else { } else {
@@ -85,15 +86,15 @@ class PostController extends ApiController
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The post update request. * @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(PostRequest $request, Post $post) public function update(ArticleRequest $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$post->update($request->all()); $article->update($request->all());
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -102,13 +103,13 @@ class PostController extends ApiController
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy(Post $post) public function destroy(Article $article)
{ {
if (PostConductor::destroyable($post) === true) { if (ArticleConductor::destroyable($article) === true) {
$post->delete(); $article->delete();
return $this->respondNoContent(); return $this->respondNoContent();
} else { } else {
return $this->respondForbidden(); return $this->respondForbidden();
@@ -117,18 +118,18 @@ class PostController extends ApiController
/** /**
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the article attachments.
* @throws InvalidFormatException * @throws InvalidFormatException
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws InvalidCastException * @throws InvalidCastException
*/ */
public function getAttachments(Request $request, Post $post) public function getAttachments(Request $request, Article $article): JsonResponse
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
$medium = $post->attachments->map(function ($attachment) { $medium = $article->attachments->map(function ($attachment) {
return $attachment->media; return $attachment->media;
}); });
@@ -140,18 +141,18 @@ class PostController extends ApiController
/** /**
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
*/ */
public function storeAttachment(Request $request, Post $post) public function storeAttachment(Request $request, Article $article): JsonResponse
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
if($request->has("medium") && Media::find($request->medium)) { if ($request->has("medium") && Media::find($request->medium)) {
$post->attachments()->create(['media_id' => $request->medium]); $article->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated(); return $this->respondCreated();
} }
@@ -163,67 +164,65 @@ class PostController extends ApiController
/** /**
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Article $article The related model.
* @return JsonResponse * @throws BindingResolutionException
* @throws BindingResolutionException * @throws MassAssignmentException
* @throws MassAssignmentException
*/ */
public function updateAttachments(Request $request, Post $post) public function updateAttachments(Request $request, Article $article): JsonResponse
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$mediaIds = $request->attachments; $mediaIds = $request->attachments;
if(is_array($mediaIds) === false) { if (is_array($mediaIds) === false) {
$mediaIds = explode(',', $request->attachments); $mediaIds = explode(',', $request->attachments);
} }
$mediaIds = array_map('trim', $mediaIds); // trim each media ID $mediaIds = array_map('trim', $mediaIds); // trim each media ID
$attachments = $post->attachments; $attachments = $article->attachments;
// Delete attachments that are not in $mediaIds // Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if (!in_array($attachment->media_id, $mediaIds)) { if (!in_array($attachment->media_id, $mediaIds)) {
$attachment->delete(); $attachment->delete();
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id == $mediaId) { if ($attachment->media_id == $mediaId) {
$found = true; $found = true;
break; break;
} }
} }
if (!$found) { if (!$found) {
$post->attachments()->create(['media_id' => $mediaId]); $article->attachments()->create(['media_id' => $mediaId]);
} }
} }
return $this->respondNoContent(); return $this->respondNoContent();
} }//end if
return $this->respondForbidden(); return $this->respondForbidden();
} }
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Article $article The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse * @throws BindingResolutionException
* @throws BindingResolutionException
*/ */
public function deleteAttachment(Request $request, Post $post, Media $medium) public function deleteAttachment(Request $request, Article $article, Media $medium): JsonResponse
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$attachments = $post->attachments; $attachments = $article->attachments;
$deleted = false; $deleted = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id === $medium->id) { if ($attachment->media_id === $medium->id) {
$attachment->delete(); $attachment->delete();
@@ -231,7 +230,7 @@ class PostController extends ApiController
break; break;
} }
} }
if ($deleted) { if ($deleted) {
// Attachment was deleted successfully // Attachment was deleted successfully
return $this->respondNoContent(); return $this->respondNoContent();

View File

@@ -29,7 +29,7 @@ class AttachmentController extends ApiController
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(Request $request) public function store(Request $request)
@@ -40,7 +40,7 @@ class AttachmentController extends ApiController
/** /**
* Display the specified resource. * Display the specified resource.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show(Attachment $attachment) public function show(Attachment $attachment)
@@ -51,7 +51,7 @@ class AttachmentController extends ApiController
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function edit(Attachment $attachment) public function edit(Attachment $attachment)
@@ -62,11 +62,11 @@ class AttachmentController extends ApiController
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(Request $request, Attachment $attachment) public function update(Request $request, Attachment $attachment)
{ {
// //
} }
@@ -74,10 +74,10 @@ class AttachmentController extends ApiController
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param \App\Models\Attachment $attachment * @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy(Attachment $attachment) public function destroy(Attachment $attachment)
{ {
// //
} }

View File

@@ -31,9 +31,8 @@ class AuthController extends ApiController
* Current User details * Current User details
* *
* @param Request $request Current request data. * @param Request $request Current request data.
* @return JsonResponse
*/ */
public function me(Request $request) public function me(Request $request): JsonResponse
{ {
$user = $request->user()->makeVisible(['permissions']); $user = $request->user()->makeVisible(['permissions']);
return $this->respondAsResource($user); return $this->respondAsResource($user);
@@ -47,18 +46,18 @@ class AuthController extends ApiController
*/ */
public function login(AuthLoginRequest $request) public function login(AuthLoginRequest $request)
{ {
$user = User::where('username', '=', $request->input('username'))->first(); $user = User::where('email', '=', $request->input('email'))->first();
if ($user !== null && Hash::check($request->input('password'), $user->password) === true) { if ($user !== null && strlen($user->password) > 0 && Hash::check($request->input('password'), $user->password) === true) {
if ($user->email_verified_at === null) { if ($user->email_verified_at === null) {
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Email address has not been verified.' 'email' => 'Email address has not been verified.'
]); ]);
} }
if ($user->disabled === true) { if ($user->disabled === true) {
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Account has been disabled.' 'email' => 'Account has been disabled.'
]); ]);
} }
@@ -78,8 +77,8 @@ class AuthController extends ApiController
}//end if }//end if
return $this->respondWithErrors([ return $this->respondWithErrors([
'username' => 'Invalid username or password', 'email' => 'Invalid email or password',
'password' => 'Invalid username or password', 'password' => 'Invalid email or password',
]); ]);
} }
@@ -87,9 +86,8 @@ class AuthController extends ApiController
* Logout current user * Logout current user
* *
* @param Request $request Current request data. * @param Request $request Current request data.
* @return JsonResponse
*/ */
public function logout(Request $request) public function logout(Request $request): JsonResponse
{ {
$user = $request->user(); $user = $request->user();

View File

@@ -6,8 +6,10 @@ use App\Enum\HttpResponseCodes;
use App\Models\Event; use App\Models\Event;
use App\Conductors\EventConductor; use App\Conductors\EventConductor;
use App\Conductors\MediaConductor; use App\Conductors\MediaConductor;
use App\Conductors\UserConductor;
use App\Http\Requests\EventRequest; use App\Http\Requests\EventRequest;
use App\Models\Media; use App\Models\Media;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class EventController extends ApiController class EventController extends ApiController
@@ -18,7 +20,7 @@ class EventController extends ApiController
public function __construct() public function __construct()
{ {
$this->middleware('auth:sanctum') $this->middleware('auth:sanctum')
->only(['store','update','destroy']); ->only(['store','update','destroy', 'userAdd', 'userUpdate', 'userDelete']);
} }
/** /**
@@ -111,13 +113,10 @@ class EventController extends ApiController
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Event $event The event model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the event attachments.
* @throws InvalidFormatException
* @throws BindingResolutionException
* @throws InvalidCastException
*/ */
public function getAttachments(Request $request, Event $event) public function getAttachments(Request $request, Event $event): JsonResponse
{ {
if (EventConductor::viewable($event) === true) { if (EventConductor::viewable($event) === true) {
$medium = $event->attachments->map(function ($attachment) { $medium = $event->attachments->map(function ($attachment) {
@@ -134,15 +133,13 @@ class EventController extends ApiController
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Event $event The event model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException
* @throws MassAssignmentException
*/ */
public function storeAttachment(Request $request, Event $event) public function storeAttachment(Request $request, Event $event): JsonResponse
{ {
if (EventConductor::updatable($event) === true) { if (EventConductor::updatable($event) === true) {
if ($request->has("medium") && Media::find($request->medium)) { if ($request->has("medium") === true && Media::find($request->medium) !== null) {
$event->attachments()->create(['media_id' => $request->medium]); $event->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated(); return $this->respondCreated();
} }
@@ -157,12 +154,9 @@ class EventController extends ApiController
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Event $event The related model.
* @return JsonResponse
* @throws BindingResolutionException
* @throws MassAssignmentException
*/ */
public function updateAttachments(Request $request, Event $event) public function updateAttachments(Request $request, Event $event): JsonResponse
{ {
if (EventConductor::updatable($event) === true) { if (EventConductor::updatable($event) === true) {
$mediaIds = $request->attachments; $mediaIds = $request->attachments;
@@ -175,23 +169,23 @@ class EventController extends ApiController
// Delete attachments that are not in $mediaIds // Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if (!in_array($attachment->media_id, $mediaIds)) { if (in_array($attachment->media_id, $mediaIds) === false) {
$attachment->delete(); $attachment->delete();
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id == $mediaId) { if ($attachment->media_id === $mediaId) {
$found = true; $found = true;
break; break;
} }
} }
if (!$found) { if ($found === false) {
$event->attachments()->create(['media_id' => $mediaId]); $event->attachments()->create(['media_id' => $mediaId]);
} }
} }
@@ -204,13 +198,12 @@ class EventController extends ApiController
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
*
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Event $event The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse
* @throws BindingResolutionException
*/ */
public function deleteAttachment(Request $request, Event $event, Media $medium) public function deleteAttachment(Request $request, Event $event, Media $medium): JsonResponse
{ {
if (EventConductor::updatable($event) === true) { if (EventConductor::updatable($event) === true) {
$attachments = $event->attachments; $attachments = $event->attachments;
@@ -224,7 +217,7 @@ class EventController extends ApiController
} }
} }
if ($deleted) { if ($deleted === true) {
// Attachment was deleted successfully // Attachment was deleted successfully
return $this->respondNoContent(); return $this->respondNoContent();
} else { } else {
@@ -235,4 +228,82 @@ class EventController extends ApiController
return $this->respondForbidden(); return $this->respondForbidden();
} }
public function userList(Request $request, Event $event)
{
$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();
}
return $this->respondForbidden();
}
public function userAdd(Request $request, Event $event)
{
$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();
}
public function userUpdate(Request $request, Event $event)
{
// only admin/events permitted
}
public function userDelete(Request $request, Event $event, User $user)
{
$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

@@ -8,6 +8,7 @@ use App\Http\Requests\MediaRequest;
use App\Models\Media; use App\Models\Media;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken; use Laravel\Sanctum\PersonalAccessToken;
class MediaController extends ApiController class MediaController extends ApiController
@@ -34,7 +35,11 @@ class MediaController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
); );
} }
@@ -80,31 +85,23 @@ class MediaController extends ApiController
} }
} }
if ($file->getSize() > Media::maxUploadSize()) { if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge(); return $this->respondTooLarge();
} }
$title = $file->getClientOriginalName(); try {
$mime = $file->getMimeType(); $media = Media::createFromUploadedFile($request, $file);
$fileInfo = Media::store($file, empty($request->input('permission'))); } catch (\Exception $e) {
if ($fileInfo === null) { if ($e->getCode() === Media::FILE_SIZE_EXCEEDED_ERROR) {
return $this->respondWithErrors( return $this->respondTooLarge();
['file' => 'The file could not be stored on the server'], } else {
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR return $this->respondWithErrors(['file' => $e->getMessage()]);
); }
} }
$request->merge([
'title' => $title,
'mime' => $mime,
'name' => $fileInfo['name'],
'size' => filesize($fileInfo['path'])
]);
$media = $request->user()->media()->create($request->all());
return $this->respondAsResource( return $this->respondAsResource(
MediaConductor::model($request, $media), MediaConductor::model($request, $media),
['respondCode' => HttpResponseCodes::HTTP_CREATED] ['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
); );
}//end if }//end if
@@ -123,32 +120,36 @@ class MediaController extends ApiController
if (MediaConductor::updatable($medium) === true) { if (MediaConductor::updatable($medium) === true) {
$file = $request->file('file'); $file = $request->file('file');
if ($file !== null) { if ($file !== null) {
if ($file->getSize() > Media::maxUploadSize()) { if ($file->isValid() !== true) {
return $this->respondTooLarge(); 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.']);
}
} }
$oldPath = $medium->path(); if ($file->getSize() > Media::getMaxUploadSize()) {
$fileInfo = Media::store($file, empty($request->input('permission'))); return $this->respondTooLarge();
if ($fileInfo === null) { }
}
$medium->update($request->all());
if ($file !== null) {
try {
$medium->updateWithUploadedFile($file);
} catch (\Exception $e) {
return $this->respondWithErrors( return $this->respondWithErrors(
['file' => 'The file could not be stored on the server'], ['file' => $e->getMessage()],
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
); );
} }
}
if (file_exists($oldPath) === true) {
unlink($oldPath);
}
$request->merge([
'title' => $file->getClientOriginalName(),
'mime' => $file->getMimeType(),
'name' => $fileInfo['name'],
'size' => filesize($fileInfo['path'])
]);
}//end if
$medium->update($request->all());
return $this->respondAsResource(MediaConductor::model($request, $medium)); return $this->respondAsResource(MediaConductor::model($request, $medium));
}//end if }//end if
@@ -164,10 +165,6 @@ class MediaController extends ApiController
public function destroy(Media $medium) public function destroy(Media $medium)
{ {
if (MediaConductor::destroyable($medium) === true) { if (MediaConductor::destroyable($medium) === true) {
if (file_exists($medium->path()) === true) {
unlink($medium->path());
}
$medium->delete(); $medium->delete();
return $this->respondNoContent(); return $this->respondNoContent();
} }

View File

@@ -32,7 +32,7 @@ class OCRController extends ApiController
$data = ['ocr' => []]; $data = ['ocr' => []];
$filters = $request->get('filters', ['tesseract']); $filters = $request->get('filters', ['tesseract']);
if(is_array($filters) === false) { if (is_array($filters) === false) {
$filters = explode(',', $filters); $filters = explode(',', $filters);
} }
@@ -52,9 +52,12 @@ class OCRController extends ApiController
// We need progress updates to break the connection mid-way // We need progress updates to break the connection mid-way
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // more progress info
curl_setopt($ch, CURLOPT_NOPROGRESS, false); curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function( curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function (
$downloadSize, $downloaded, $uploadSize, $uploaded $downloadSize,
) use($maxDownloadSize) { $downloaded,
$uploadSize,
$uploaded
) use ($maxDownloadSize) {
return ($downloaded > $maxDownloadSize) ? 1 : 0; return ($downloaded > $maxDownloadSize) ? 1 : 0;
}); });
@@ -62,9 +65,9 @@ class OCRController extends ApiController
$curlError = curl_errno($ch); $curlError = curl_errno($ch);
$curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); $curlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch); curl_close($ch);
if($curlError !== 0) { if ($curlError !== 0) {
$error = 'File size is larger then allowed'; $error = 'File size is larger then allowed';
if($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) { if ($curlError !== CurlErrorCodes::CURLE_ABORTED_BY_CALLBACK) {
$error = CurlErrorCodes::getMessage($curlError); $error = CurlErrorCodes::getMessage($curlError);
} }
@@ -77,8 +80,8 @@ class OCRController extends ApiController
// tesseract (overall) // tesseract (overall)
$ocr = null; $ocr = null;
foreach($filters as $filterItem) { foreach ($filters as $filterItem) {
if(str_starts_with($filterItem, 'tesseract') === true) { if (str_starts_with($filterItem, 'tesseract') === true) {
$ocr = new TesseractOCR(); $ocr = new TesseractOCR();
$ocr->image($urlDownloadFilePath); $ocr->image($urlDownloadFilePath);
if ($tesseractOEM !== null) { if ($tesseractOEM !== null) {
@@ -95,11 +98,10 @@ class OCRController extends ApiController
} }
// Image Filter Function // Image Filter Function
$tesseractImageFilterFunc = function($filter, $options = null) use($curlResult, $curlSize, $ocr) { $tesseractImageFilterFunc = function ($filter, $options = null) use ($curlResult, $curlSize, $ocr) {
$result = ''; $result = '';
$img = imagecreatefromstring($curlResult); $img = imagecreatefromstring($curlResult);
if ($img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) || ($options === null && imagefilter($img, $filter) === true))) { if ($img !== false && (($options !== null && imagefilter($img, $filter, $options) === true) || ($options === null && imagefilter($img, $filter) === true))) {
ob_start(); ob_start();
imagepng($img); imagepng($img);
$imgData = ob_get_contents(); $imgData = ob_get_contents();
@@ -108,7 +110,7 @@ class OCRController extends ApiController
$ocr->imageData($imgData, $imgDataSize); $ocr->imageData($imgData, $imgDataSize);
imagedestroy($img); imagedestroy($img);
$result = $ocr->run(500); $result = $ocr->run(500);
} }
@@ -116,7 +118,7 @@ class OCRController extends ApiController
}; };
// Image Scale Function // Image Scale Function
$tesseractImageScaleFunc = function($scaleFunc) use ($curlResult, $ocr) { $tesseractImageScaleFunc = function ($scaleFunc) use ($curlResult, $ocr) {
$result = ''; $result = '';
$srcImage = imagecreatefromstring($curlResult); $srcImage = imagecreatefromstring($curlResult);
$srcWidth = imagesx($srcImage); $srcWidth = imagesx($srcImage);
@@ -133,7 +135,7 @@ class OCRController extends ApiController
$imgData = ob_get_contents(); $imgData = ob_get_contents();
ob_end_clean(); ob_end_clean();
$imgDataSize = strlen($imgData); $imgDataSize = strlen($imgData);
imagedestroy($srcImage); imagedestroy($srcImage);
imagedestroy($dstImage); imagedestroy($dstImage);
@@ -143,7 +145,7 @@ class OCRController extends ApiController
}; };
// filter: tesseract // filter: tesseract
if(in_array('tesseract', $filters) === true) { if (in_array('tesseract', $filters) === true) {
$data['ocr']['tesseract'] = $ocr->run(500); $data['ocr']['tesseract'] = $ocr->run(500);
} }
@@ -154,14 +156,14 @@ class OCRController extends ApiController
// filter: tesseract.double_scale // filter: tesseract.double_scale
if (in_array('tesseract.double_scale', $filters) === true) { if (in_array('tesseract.double_scale', $filters) === true) {
$data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function($size) { $data['ocr']['tesseract.double_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size * 2; return $size * 2;
}); });
} }
// filter: tesseract.half_scale // filter: tesseract.half_scale
if (in_array('tesseract.half_scale', $filters) === true) { if (in_array('tesseract.half_scale', $filters) === true) {
$data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function($size) { $data['ocr']['tesseract.half_scale'] = $tesseractImageScaleFunc(function ($size) {
return $size / 2; return $size / 2;
}); });
} }
@@ -187,12 +189,12 @@ class OCRController extends ApiController
} }
// filter: keras // filter: keras
if(in_array('keras', $filters) === true) { if (in_array('keras', $filters) === true) {
$cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url); $cmd = '/usr/bin/python3 ' . base_path() . '/scripts/keras_oc.py ' . urlencode($url);
$command = escapeshellcmd($cmd); $command = escapeshellcmd($cmd);
$output = shell_exec($cmd); $output = shell_exec($cmd);
if ($output !== null && strlen($output) > 0) { if ($output !== null && strlen($output) > 0) {
$output = substr($output, strpos($output, '----------START----------') + 25); $output = substr($output, (strpos($output, '----------START----------') + 25));
} else { } else {
$output = ''; $output = '';
} }

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\ShortlinkConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\MediaRequest;
use App\Http\Requests\ShortlinkRequest;
use App\Models\Media;
use App\Models\Shortlink;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
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 $medium 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 $medium 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

@@ -35,7 +35,8 @@ class SubscriptionController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }

View File

@@ -2,10 +2,10 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Conductors\EventConductor;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Requests\UserRequest; use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest; use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest; use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest; use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest; use App\Http\Requests\UserResetPasswordRequest;
@@ -14,7 +14,6 @@ use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail; use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword; use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify; use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword; use App\Mail\ForgotPassword;
use App\Mail\EmailVerify; use App\Mail\EmailVerify;
use App\Models\User; use App\Models\User;
@@ -22,6 +21,8 @@ use App\Models\UserCode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use App\Conductors\UserConductor; use App\Conductors\UserConductor;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
class UserController extends ApiController class UserController extends ApiController
{ {
@@ -37,10 +38,10 @@ class UserController extends ApiController
'register', 'register',
'exists', 'exists',
'forgotPassword', 'forgotPassword',
'forgotUsername',
'resetPassword', 'resetPassword',
'verifyEmail', 'verifyEmail',
'resendVerifyEmailCode' 'resendVerifyEmailCode',
'eventList',
]); ]);
} }
@@ -57,7 +58,8 @@ class UserController extends ApiController
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }
@@ -97,14 +99,14 @@ class UserController extends ApiController
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \App\Http\Requests\UserRequest $request The user update request. * @param \App\Http\Requests\UserRequest $request The user update request.
* @param \App\Models\User $user The specified user. * @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(UserRequest $request, User $user) public function update(UserRequest $request, User $user)
{ {
if (UserConductor::updatable($user) === true) { if (UserConductor::updatable($user) === true) {
$input = []; $input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; $updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) { if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']); $updatable = array_merge($updatable, ['email_verified_at']);
@@ -143,20 +145,32 @@ class UserController extends ApiController
* Register a new user * Register a new user
* *
* @param \App\Http\Requests\UserRegisterRequest $request The register user request. * @param \App\Http\Requests\UserRegisterRequest $request The register user request.
* @return \Illuminate\Http\Response
*/ */
public function register(UserRegisterRequest $request) public function register(UserRegisterRequest $request): JsonResponse
{ {
try { try {
$user = User::create([ $userData = $request->only([
'first_name' => $request->input('first_name'), 'first_name',
'last_name' => $request->input('last_name'), 'last_name',
'username' => $request->input('username'), 'email',
'email' => $request->input('email'), 'phone',
'phone' => $request->input('phone'), 'password',
'password' => Hash::make($request->input('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([ $code = $user->codes()->create([
'action' => 'verify-email', 'action' => 'verify-email',
]); ]);
@@ -173,26 +187,6 @@ class UserController extends ApiController
}//end try }//end try
} }
/**
* Sends an email with all the usernames registered at that address
*
* @param \App\Http\Requests\UserForgotUsernameRequest $request The forgot username request.
* @return \Illuminate\Http\Response
*/
public function forgotUsername(UserForgotUsernameRequest $request)
{
$users = User::where('email', $request->input('email'))->whereNotNull('email_verified_at')->get();
if ($users->count() > 0) {
dispatch((new SendEmailJob(
$users->first()->email,
new ForgotUsername($users->pluck('username')->toArray())
)))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondJson(['message' => 'Username send to the email address if registered']);
}
/** /**
* Generates a new reset password code * Generates a new reset password code
* *
@@ -201,7 +195,7 @@ class UserController extends ApiController
*/ */
public function forgotPassword(UserForgotPasswordRequest $request) public function forgotPassword(UserForgotPasswordRequest $request)
{ {
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete(); $user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([ $code = $user->codes()->create([
@@ -291,13 +285,12 @@ class UserController extends ApiController
* Resend a new verify email * Resend a new verify email
* *
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request. * @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request.
* @return \Illuminate\Http\Response
*/ */
public function resendVerifyEmail(UserResendVerifyEmailRequest $request) public function resendVerifyEmail(UserResendVerifyEmailRequest $request): JsonResponse
{ {
UserCode::clearExpired(); UserCode::clearExpired();
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first(); $code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate(); $code->regenerate();
@@ -322,7 +315,7 @@ class UserController extends ApiController
*/ */
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request) public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{ {
$user = User::where('username', $request->input('username'))->first(); $user = User::where('email', $request->input('email'))->first();
if ($user !== null) { if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete(); $user->codes()->where('action', 'verify-email')->delete();
@@ -339,4 +332,28 @@ class UserController extends ApiController
return $this->respondNotFound(); return $this->respondNotFound();
} }
/**
* Return a JSON event list of a user.
*
* @param Request $request The http request.
* @param User $user The specified user.
*/
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

@@ -3,13 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests; use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests; use ValidatesRequests;
} }

View File

@@ -40,7 +40,7 @@ class Kernel extends HttpKernel
'api' => [ 'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api', \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\ForceJsonResponse::class, // \App\Http\Middleware\ForceJsonResponse::class,
'useSanctumGuard', 'useSanctumGuard',
@@ -49,13 +49,13 @@ class Kernel extends HttpKernel
]; ];
/** /**
* The application's route middleware. * The application's middleware aliases.
* *
* These middleware may be assigned to groups or used individually. * Aliases may be used to conveniently assign middleware to routes and groups.
* *
* @var array<string, class-string|string> * @var array<string, class-string|string>
*/ */
protected $routeMiddleware = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,

View File

@@ -10,9 +10,8 @@ class Authenticate extends Middleware
* Get the path the user should be redirected to when they are not authenticated. * Get the path the user should be redirected to when they are not authenticated.
* *
* @param mixed $request Request. * @param mixed $request Request.
* @return string|null
*/ */
protected function redirectTo(mixed $request) protected function redirectTo(mixed $request): ?string
{ {
if ($request->expectsJson() === false) { if ($request->expectsJson() === false) {
return route('login'); return route('login');

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -10,11 +11,9 @@ class ForceJsonResponse
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
$request->headers->set('Accept', 'application/json'); $request->headers->set('Accept', 'application/json');
return $next($request); return $next($request);

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Analytics; use App\Models\Analytics;
@@ -11,18 +12,16 @@ class LogRequest
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
// Make it an after middleware // Make it an after middleware
$response = $next($request); $response = $next($request);
try { try {
Analytics::create([ Analytics::createWithSession([
'type' => 'pageview', 'type' => 'apirequest',
'attribute' => $request->path(), 'attribute' => $request->path(),
'useragent' => $request->userAgent(), 'useragent' => $request->userAgent(),
'ip' => $request->ip(), 'ip' => $request->ip(),

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -12,12 +13,11 @@ class RedirectIfAuthenticated
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param Request $request Request. * @param Request $request Request.
* @param Closure(Request): (Response|RedirectResponse) $next Next. * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards Guards. * @param string|null ...$guards Guards.
* @return Response|RedirectResponse
*/ */
public function handle(Request $request, Closure $next, ...$guards) public function handle(Request $request, Closure $next, string ...$guards): Response
{ {
$guards = empty($guards) === true ? [null] : $guards; $guards = empty($guards) === true ? [null] : $guards;

View File

@@ -11,7 +11,7 @@ class TrustHosts extends Middleware
* *
* @return array<int, string|null> * @return array<int, string|null>
*/ */
public function hosts() public function hosts(): array
{ {
return [ return [
$this->allSubdomainsOfApplicationUrl(), $this->allSubdomainsOfApplicationUrl(),

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -11,11 +12,9 @@ class UseSanctumGuard
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
Auth::shouldUse('sanctum'); Auth::shouldUse('sanctum');
return $next($request); 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

@@ -4,17 +4,17 @@ namespace App\Http\Requests;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class PostRequest extends BaseRequest class ArticleRequest extends BaseRequest
{ {
/** /**
* Get the validation rules that apply to POST requests. * Get the validation rules that apply to POST requests.
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function postRules() public function postRules(): array
{ {
return [ return [
'slug' => 'required|string|min:6|unique:posts', 'slug' => 'required|string|min:6|unique:articles',
'title' => 'required|string|min:6|max:255', 'title' => 'required|string|min:6|max:255',
'publish_at' => 'required|date', 'publish_at' => 'required|date',
'user_id' => 'required|uuid|exists:users,id', 'user_id' => 'required|uuid|exists:users,id',
@@ -28,13 +28,13 @@ class PostRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function putRules() public function putRules(): array
{ {
return [ return [
'slug' => [ 'slug' => [
'string', 'string',
'min:6', 'min:6',
Rule::unique('posts')->ignoreModel($this->post), Rule::unique('articles')->ignoreModel($this->article),
], ],
'title' => 'string|min:6|max:255', 'title' => 'string|min:6|max:255',
'publish_at' => 'date', 'publish_at' => 'date',

View File

@@ -11,10 +11,10 @@ class AuthLoginRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'username' => 'required|string|min:6|max:255', 'email' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6', 'password' => 'required|string|min:6',
]; ];
} }

View File

@@ -9,10 +9,8 @@ class BaseRequest extends FormRequest
{ {
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
*
* @return boolean
*/ */
public function authorize() public function authorize(): bool
{ {
if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) { if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) {
return $this->postAuthorize(); return $this->postAuthorize();
@@ -30,7 +28,7 @@ class BaseRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
$rules = []; $rules = [];
@@ -54,9 +52,8 @@ class BaseRequest extends FormRequest
* *
* @param array $collection1 The first collection of rules. * @param array $collection1 The first collection of rules.
* @param array $collection2 The second collection of rules to merge. * @param array $collection2 The second collection of rules to merge.
* @return array
*/ */
private function mergeRules(array $collection1, array $collection2) private function mergeRules(array $collection1, array $collection2): array
{ {
$rules = []; $rules = [];

View File

@@ -12,13 +12,13 @@ class ContactSendRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'name' => 'required|max:255', 'name' => 'required|max:255',
'email' => 'required|email|max:255', 'email' => 'required|email|max:255',
'content' => 'required|max:2000', 'content' => 'required|max:2000',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -11,7 +11,7 @@ class EventRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function baseRules() public function baseRules(): array
{ {
return [ return [
'title' => 'min:6', 'title' => 'min:6',
@@ -42,7 +42,7 @@ class EventRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
protected function postRules() protected function postRules(): array
{ {
return [ return [
'title' => 'required', 'title' => 'required',

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

@@ -11,11 +11,11 @@ class SubscriptionRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function postRules() public function postRules(): array
{ {
return [ return [
'email' => 'required|email|unique:subscriptions', 'email' => 'required|email|unique:subscriptions',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
@@ -24,20 +24,18 @@ class SubscriptionRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function destroyRules() public function destroyRules(): array
{ {
return [ return [
'email' => 'required|email', 'email' => 'required|email',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
/** /**
* Get the custom error messages. * Get the custom error messages.
*
* @return array
*/ */
public function messages() public function messages(): array
{ {
return [ return [
'email.unique' => 'This email address has already subscribed', 'email.unique' => 'This email address has already subscribed',

View File

@@ -12,11 +12,11 @@ class UserForgotPasswordRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'username' => 'required|exists:users,username', 'email' => 'required|exists:users,email',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\Uniqueish;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest class UserRegisterRequest extends FormRequest
@@ -11,13 +12,11 @@ class UserRegisterRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'first_name' => 'required|string|max:255', 'display_name' => ['required','string','max:255', new Uniqueish('users')],
'last_name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users',
'email' => 'required|string|email|max:255',
'username' => 'required|string|min:4|max:255|unique:users',
'password' => 'required|string|min:8', 'password' => 'required|string|min:8',
]; ];
} }

View File

@@ -2,7 +2,11 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\RequiredIfAny;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\RequiredIf;
use App\Rules\Uniqueish;
use Illuminate\Support\Arr;
class UserRequest extends BaseRequest class UserRequest extends BaseRequest
{ {
@@ -11,13 +15,21 @@ class UserRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function postRules() public function postRules(): array
{ {
$user = auth()->user();
$isAdminUser = $user->hasPermission('admin/users');
return [ return [
'username' => 'required|string|max:255|min:4|unique:users', 'first_name' => ($isAdminUser === true ? 'required_with:last_name,display_name,phone' : 'required') . '|string|max:255|min:2',
'first_name' => 'required|string|max:255|min:2', 'last_name' => ($isAdminUser === true ? 'required_with:first_name,display_name,phone' : 'required') . '|string|max:255|min:2',
'last_name' => 'required|string|max:255|min:2', 'display_name' => [
'email' => 'required|string|email|max:255', $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}$/'], 'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date' 'email_verified_at' => 'date'
]; ];
@@ -28,27 +40,68 @@ class UserRequest extends BaseRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function putRules() public function putRules(): array
{ {
$user = $this->route('user'); $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 [ return [
'username' => [ 'first_name' => [
'sometimes',
$isAdminUser === true ? $requiredIfFieldsPresent(['last_name', 'display_name', 'phone']) : 'required',
'string', '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', 'max:255',
'min:4', Rule::unique('users')->ignore($ruleUser->id)->when(
Rule::unique('users')->ignore($user->id)->when( $this->email !== $ruleUser->email,
$this->username !== $user->username,
function ($query) { function ($query) {
return $query->where('username', $this->username); return $query->where('email', $this->email);
} }
), ),
], ],
'first_name' => 'string|max:255|min:2', 'phone' => ['nullable', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'last_name' => 'string|max:255|min:2', 'password' => "nullable|string|min:8"
'email' => 'string|email|max:255',
'phone' => ['nullable','regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => 'string|min:8'
]; ];
} }
} }

View File

@@ -12,11 +12,11 @@ class UserResendVerifyEmailRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'username' => 'required|exists:users,username', 'email' => 'required|exists:users,email',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -12,12 +12,12 @@ class UserResetPasswordRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'code' => 'required|digits:6', 'code' => 'required|digits:6',
'password' => 'required|string|min:8', 'password' => 'required|string|min:8',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

View File

@@ -12,11 +12,11 @@ class UserVerifyEmailRequest extends FormRequest
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function rules() public function rules(): array
{ {
return [ return [
'code' => 'required|digits:6', 'code' => 'required|digits:6',
'captcha_token' => [new Recaptcha()], // 'captcha_token' => [new Recaptcha()],
]; ];
} }
} }

82
app/Jobs/MoveMediaJob.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class MoveMediaJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Media item
*
* @var Media
*/
public $media;
/**
* New storage ID
*
* @var string
*/
protected $newStorage;
/**
* Create a new job instance.
*
* @param Media $media The media model.
* @param string $newStorage The new storage ID.
* @return void
*/
public function __construct(Media $media, string $newStorage)
{
$this->media = $media;
$this->newStorage = $newStorage;
}
/**
* Execute the job.
*/
public function handle(): void
{
// Don't continue if the media is already on the new storage disk
if ($this->media->storage === $this->newStorage) {
return;
}
$this->media->status = 'Moving file';
$this->media->save();
$files = ["/{$this->media->name}"];
if (empty($this->media->variants) === false) {
foreach ($this->media->variants as $variant => $name) {
$files[] = "/{$name}";
}
}
$this->media->invalidateCFCache();
// Move the files from the old storage disk to the new storage disk
foreach ($files as $file) {
Storage::disk($this->newStorage)->put($file, Storage::disk($this->media->storage)->get($file));
Storage::disk($this->media->storage)->delete($file);
}
// Update the media model with the new storage and save it to the database
$this->media->storage = $this->newStorage;
$this->media->status = 'OK';
$this->media->save();
}
}

View File

@@ -47,10 +47,8 @@ class SendEmailJob implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*
* @return void
*/ */
public function handle() public function handle(): void
{ {
Mail::to($this->to)->send($this->mailable); Mail::to($this->to)->send($this->mailable);
} }

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Jobs;
use App\Models\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use SplFileInfo;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Intervention\Image\Facades\Image;
use Spatie\ImageOptimizer\OptimizerChainFactory;
class StoreUploadedFileJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Media item
*
* @var Media
*/
protected $media;
/**
* Uploaded file item
*
* @var string
*/
protected $uploadedFilePath;
/**
* Replace existing files
*
* @var string
*/
protected $replaceExisting;
/**
* Create a new job instance.
*
* @param Media $media The media model.
* @param string $filePath The uploaded file.
* @param boolean $replaceExisting Replace existing files.
* @return void
*/
public function __construct(Media $media, string $filePath, bool $replaceExisting = true)
{
$this->media = $media;
$this->uploadedFilePath = $filePath;
$this->replaceExisting = $replaceExisting;
}
/**
* Execute the job.
*/
public function handle(): void
{
$storageDisk = $this->media->storage;
$fileName = $this->media->name;
try {
$this->media->status = "Uploading to CDN";
$this->media->save();
if (strlen($this->uploadedFilePath) > 0) {
if (Storage::disk($storageDisk)->exists($fileName) === false || $this->replaceExisting === true) {
Storage::disk($storageDisk)->putFileAs('/', new SplFileInfo($this->uploadedFilePath), $fileName);
Log::info("uploading file {$storageDisk} / {$fileName} / {$this->uploadedFilePath}");
} else {
Log::info("file {$fileName} already exists in {$storageDisk} / {$this->uploadedFilePath}. Not replacing file and using local {$fileName} for variants.");
}
} else {
if (Storage::disk($storageDisk)->exists($fileName) === true) {
Log::info("file {$fileName} already exists in {$storageDisk} / {$this->uploadedFilePath}. No local {$fileName} for variants, downloading from CDN.");
$readStream = Storage::disk($storageDisk)->readStream($fileName);
$tempFilePath = tempnam(sys_get_temp_dir(), 'download-');
$writeStream = fopen($tempFilePath, 'w');
while (feof($readStream) !== true) {
fwrite($writeStream, fread($readStream, 8192));
}
fclose($readStream);
fclose($writeStream);
$this->uploadedFilePath = $tempFilePath;
} else {
$errorStr = "cannot upload file {$storageDisk} / {$fileName} / {$this->uploadedFilePath} as temp file is empty";
Log::info($errorStr);
throw new \Exception($errorStr);
}
}//end if
if (strpos($this->media->mime_type, 'image/') === 0) {
$this->media->status = "Optimizing image";
$this->media->save();
// Generate additional image sizes
$sizes = Media::getTypeVariants('image');
$originalImage = Image::make($this->uploadedFilePath);
$dimensions = [$originalImage->getWidth(), $originalImage->getHeight()];
$this->media->dimensions = implode('x', $dimensions);
foreach ($sizes as $variantName => $size) {
$postfix = "{$size['width']}x{$size['height']}";
if ($variantName === 'scaled') {
$postfix = 'scaled';
}
if (is_array($this->media->variants) === true && array_key_exists($postfix, $this->media->variants) === true && Storage::disk($storageDisk)->exists($this->media->variants[$postfix]) === true && $this->replaceExisting === true) {
Storage::disk($storageDisk)->delete($this->media->variants[$postfix]);
}
$newFilename = pathinfo($this->media->name, PATHINFO_FILENAME) . "-$postfix.webp";
if (Storage::disk($storageDisk)->exists($newFilename) === false || $this->replaceExisting === true) {
// Get the largest available variant
if ($dimensions[0] >= $size['width'] && $dimensions[1] >= $size['height']) {
// Store the variant in the variants array
$variants[$variantName] = $newFilename;
// Resize the image to the variant size if its dimensions are greater than the specified size
$image = clone $originalImage;
$imageSize = $image->getSize();
if ($imageSize->getWidth() > $size['width'] || $imageSize->getHeight() > $size['height']) {
$image->resize($size['width'], $size['height'], function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$image->resizeCanvas($size['width'], $size['height'], 'center', false, '#FFFFFF');
}
// Optimize and store image
$tempImagePath = tempnam(sys_get_temp_dir(), 'optimize');
$image->encode('webp', 75)->save($tempImagePath);
Storage::disk($storageDisk)->putFileAs('/', new SplFileInfo($tempImagePath), $newFilename);
unlink($tempImagePath);
}//end if
} else {
Log::info("variant {$variantName} already exists for file {$fileName}");
}//end if
}//end foreach
// Set missing variants to the largest available variant
foreach ($sizes as $variantName => $size) {
if (isset($variants[$variantName]) === false) {
$variants[$variantName] = $this->media->name;
}
}
$this->media->variants = $variants;
}//end if
if (strlen($this->uploadedFilePath) > 0) {
unlink($this->uploadedFilePath);
}
$this->media->status = 'OK';
$this->media->save();
} catch (\Exception $e) {
Log::error($e->getMessage());
$this->media->status = "Failed";
$this->media->save();
$this->fail($e);
}//end try
}
}

View File

@@ -54,10 +54,8 @@ class ChangeEmailVerify extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '👋🏻 Lets change your email!', subject: '👋🏻 Lets change your email!',
@@ -66,10 +64,8 @@ class ChangeEmailVerify extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.change_email_verify', view: 'emails.user.change_email_verify',

View File

@@ -54,10 +54,8 @@ class ChangedEmail extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '👍 Your email has been changed!', subject: '👍 Your email has been changed!',
@@ -66,10 +64,8 @@ class ChangedEmail extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.changed_email', view: 'emails.user.changed_email',

View File

@@ -36,10 +36,8 @@ class ChangedPassword extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '👍 Your password has been changed!', subject: '👍 Your password has been changed!',
@@ -48,10 +46,8 @@ class ChangedPassword extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.changed_password', view: 'emails.user.changed_password',

View File

@@ -53,10 +53,8 @@ class Contact extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: config('contact.contact_subject'), subject: config('contact.contact_subject'),
@@ -65,10 +63,8 @@ class Contact extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.contact', view: 'emails.user.contact',

View File

@@ -45,10 +45,8 @@ class EmailVerify extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '👋🏻 Welcome to STEMMechanics!', subject: '👋🏻 Welcome to STEMMechanics!',
@@ -57,10 +55,8 @@ class EmailVerify extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.email_verify', view: 'emails.user.email_verify',

View File

@@ -45,10 +45,8 @@ class ForgotPassword extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '🤦 Forgot your password?', subject: '🤦 Forgot your password?',
@@ -57,10 +55,8 @@ class ForgotPassword extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.forgot_password', view: 'emails.user.forgot_password',

View File

@@ -1,60 +0,0 @@
<?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 ForgotUsername extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The list of usernames
*
* @var string[]
*/
public $usernames;
/**
* Create a new message instance.
*
* @param array $usernames The usernames.
* @return void
*/
public function __construct(array $usernames)
{
$this->usernames = $usernames;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '🤦 Forgot your username?',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.forgot_username',
text: 'emails.user.forgot_username_plain',
);
}
}

View File

@@ -36,10 +36,8 @@ class SubscriptionConfirm extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: '🗞️ You\'re on the mailing list!', subject: '🗞️ You\'re on the mailing list!',
@@ -48,10 +46,8 @@ class SubscriptionConfirm extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.subscription_confirm', view: 'emails.user.subscription_confirm',

View File

@@ -36,10 +36,8 @@ class SubscriptionUnsubscribed extends Mailable
/** /**
* Get the message envelope. * Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/ */
public function envelope() public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: 'You have been unsubscribed', subject: 'You have been unsubscribed',
@@ -48,10 +46,8 @@ class SubscriptionUnsubscribed extends Mailable
/** /**
* Get the message content definition. * Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/ */
public function content() public function content(): Content
{ {
return new Content( return new Content(
view: 'emails.user.subscription_unsubscribed', view: 'emails.user.subscription_unsubscribed',

View File

@@ -15,4 +15,30 @@ class Analytics extends Model
* @var array * @var array
*/ */
protected $guarded = []; protected $guarded = [];
/**
* Create a new row in the analytics table with the given attributes,
* automatically assigning a session value based on previous rows.
*
* @param array $attributes Model attributes.
*/
public static function createWithSession(array $attributes): static
{
$previousRow = self::where('useragent', $attributes['useragent'])
->where('ip', $attributes['ip'])
->where('created_at', '>=', now()->subMinutes(30))
->whereNotNull('session')
->orderBy('created_at', 'desc')
->first();
if ($previousRow !== null) {
$attributes['session'] = $previousRow->session;
} else {
$lastSession = self::max('session');
$attributes['session'] = ($lastSession + 1);
}
return static::create($attributes);
}
} }

View File

@@ -5,8 +5,9 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model class Article extends Model
{ {
use HasFactory; use HasFactory;
use Uuids; use Uuids;
@@ -27,20 +28,18 @@ class Post extends Model
/** /**
* Get the post user * Get the article user
*
* @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
*/ */
public function attachments() public function attachments(): MorphMany
{ {
return $this->morphMany('App\Models\Attachment', 'attachable'); return $this->morphMany(\App\Models\Attachment::class, 'attachable');
} }
} }

View File

@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Attachment extends Model class Attachment extends Model
{ {
@@ -16,13 +18,23 @@ class Attachment extends Model
*/ */
protected $fillable = [ protected $fillable = [
'media_id', 'media_id',
'private',
];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'private' => 'false',
]; ];
/** /**
* Get attachments attachable * Get attachments attachable
*/ */
public function attachable() public function attachable(): MorphTo
{ {
return $this->morphTo(); return $this->morphTo();
} }
@@ -30,8 +42,8 @@ class Attachment extends Model
/** /**
* Get the media for this attachment. * Get the media for this attachment.
*/ */
public function media() public function media(): BelongsTo
{ {
return $this->belongsTo(Media::class); return $this->belongsTo(Media::class);
} }
} }

View File

@@ -5,6 +5,8 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Event extends Model class Event extends Model
{ {
@@ -19,6 +21,7 @@ class Event extends Model
protected $fillable = [ protected $fillable = [
'title', 'title',
'location', 'location',
'location_url',
'address', 'address',
'start_at', 'start_at',
'end_at', 'end_at',
@@ -34,10 +37,18 @@ class Event extends Model
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
*/ */
public function attachments() public function attachments(): MorphMany
{ {
return $this->morphMany('App\Models\Attachment', 'attachable'); return $this->morphMany(\App\Models\Attachment::class, 'attachable');
}
/**
* Get all the associated users.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'event_user', 'event_id', 'user_id');
} }
} }

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

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
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.
*/
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
/**
* Get the user for this attachment.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -2,19 +2,37 @@
namespace App\Models; namespace App\Models;
use App\Enum\HttpResponseCodes;
use App\Jobs\MoveMediaJob;
use App\Jobs\OptimizeMediaJob;
use App\Jobs\StoreUploadedFileJob;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\UploadedFile; 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\Facades\Storage;
use Intervention\Image\Facades\Image; use Illuminate\Support\Str;
use Spatie\ImageOptimizer\OptimizerChainFactory; use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Media extends Model class Media extends Model
{ {
use HasFactory; use HasFactory;
use Uuids; use Uuids;
use DispatchesJobs;
public const INVALID_FILE_ERROR = 1;
public const FILE_SIZE_EXCEEDED_ERROR = 2;
public const FILE_NAME_EXISTS_ERROR = 3;
public const TEMP_FILE_ERROR = 4;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -23,20 +41,14 @@ class Media extends Model
*/ */
protected $fillable = [ protected $fillable = [
'title', 'title',
'name',
'mime',
'user_id', 'user_id',
'mime_type',
'permission',
'storage',
'description',
'name',
'size', 'size',
'permission' 'status',
];
/**
* The attributes that are hidden.
*
* @var array<int, string>
*/
protected $hidden = [
'path',
]; ];
/** /**
@@ -48,13 +60,48 @@ class Media extends Model
'url', 'url',
]; ];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'storage' => 'cdn',
'variants' => '[]',
'description' => '',
'dimensions' => '',
'permission' => '',
];
/**
* The storage file list cache.
*
* @var array
*/
protected static $storageFileListCache = [];
/**
* The variant types.
*
* @var int[][][]
*/
protected static $variantTypes = [
'image' => [
'thumb' => ['width' => 150, 'height' => 150],
'small' => ['width' => 300, 'height' => 225],
'medium' => ['width' => 768, 'height' => 576],
'large' => ['width' => 1024, 'height' => 768],
'xlarge' => ['width' => 1536, 'height' => 1152],
'xxlarge' => ['width' => 2048, 'height' => 1536],
'scaled' => ['width' => 2560, 'height' => 1920]
]
];
/** /**
* Model Boot * Model Boot
*
* @return void
*/ */
protected static function boot() protected static function boot(): void
{ {
parent::boot(); parent::boot();
@@ -63,153 +110,379 @@ class Media extends Model
$origPermission = $media->getOriginal()['permission']; $origPermission = $media->getOriginal()['permission'];
$newPermission = $media->permission; $newPermission = $media->permission;
$origPath = Storage::disk(Media::getStorageId(empty($origPermission)))->path($media->name); $newPermissionLen = strlen($newPermission);
$newPath = Storage::disk(Media::getStorageId(empty($newPermission)))->path($media->name);
if ($origPath !== $newPath) { if ($newPermissionLen !== strlen($origPermission)) {
if (file_exists($origPath) === true) { if ($newPermissionLen === 0) {
if (file_exists($newPath) === true) { $this->moveToStorage('cdn');
$fileParts = pathinfo($newPath); } else {
$newName = ''; $this->moveToStorage('private');
}
// need a new name! }
$tmpPath = $newPath; }
while (file_exists($tmpPath) === true) {
$newName = uniqid('', true) . $fileParts['extension'];
$tmpPath = $fileParts['dirname'] . '/' . $newName;
}
$media->name = $newName;
}
rename($origPath, $newPath);
}//end if
}//end if
}//end if
}); });
static::deleting(function ($media) {
$media->deleteFile();
});
}
/**
* Get Type Variants.
*
* @param string $type The variant type to get.
* @return array The variant data.
*/
public static function getTypeVariants(string $type): array
{
if (isset(self::$variantTypes[$type]) === true) {
return self::$variantTypes[$type];
}
return [];
}
/**
* Variants Get Mutator.
*
* @param mixed $value The value to mutate.
* @return array The mutated value.
*/
public function getVariantsAttribute(mixed $value): array
{
if (is_string($value) === true) {
return json_decode($value, true);
}
return [];
}
/**
* Variants Set Mutator.
*
* @param mixed $value The value to mutate.
*/
public function setVariantsAttribute(mixed $value): void
{
if (is_array($value) !== true) {
$value = [];
}
$this->attributes['variants'] = json_encode(($value ?? []));
}
/**
* Get previous variant.
*
* @param string $type The variant type.
* @param string $variant The initial variant.
* @return string The previous variant name (or '').
*/
public function getPreviousVariant(string $type, string $variant): string
{
if (isset(self::$variantTypes[$type]) === false) {
return '';
}
$variants = self::$variantTypes[$type];
$keys = array_keys($variants);
$currentIndex = array_search($variant, $keys);
if ($currentIndex === false || $currentIndex === 0) {
return '';
}
return $keys[($currentIndex - 1)];
}
/**
* Get next variant.
*
* @param string $type The variant type.
* @param string $variant The initial variant.
* @return string The next variant name (or '').
*/
public function getNextVariant(string $type, string $variant): string
{
if (isset(self::$variantTypes[$type]) === false) {
return '';
}
$variants = self::$variantTypes[$type];
$keys = array_keys($variants);
$currentIndex = array_search($variant, $keys);
if ($currentIndex === false || $currentIndex === (count($keys) - 1)) {
return '';
}
return $keys[($currentIndex + 1)];
}
/**
* Get variant URL.
*
* @param string $variant The variant to find.
* @param boolean $returnNearest Return the nearest variant if request is not found.
* @return string The URL.
*/
public function getVariantURL(string $variant, bool $returnNearest = true): string
{
$variants = $this->variants;
if (isset($variants[$variant]) === true) {
return self::getUrlPath() . $variants[$variant];
}
if ($returnNearest === true) {
$variantType = explode('/', $this->mime_type)[0];
$previousVariant = $variant;
while (empty($previousVariant) === false) {
$previousVariant = $this->getPreviousVariant($variantType, $previousVariant);
if (empty($previousVariant) === false && isset($variants[$previousVariant]) === true) {
return self::getUrlPath() . $variants[$previousVariant];
}
}
}
return '';
}
/**
* Delete file and associated files with the modal.
*/
public function deleteFile(): void
{
$fileName = $this->name;
$baseName = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$files = Storage::disk($this->storage)->files();
foreach ($files as $file) {
if (preg_match("/{$baseName}(-[a-zA-Z0-9]+)?\.{$extension}/", $file) === 1) {
Storage::disk($this->storage)->delete($file);
}
}
$this->invalidateCFCache();
}
/**
* Invalidate Cloudflare Cache.
*
* @throws InvalidArgumentException Exception.
*/
private function invalidateCFCache(): void
{
$zone_id = env("CLOUDFLARE_ZONE_ID");
$api_key = env("CLOUDFLARE_API_KEY");
if ($zone_id !== null && $api_key !== null && $this->url !== "") {
$urls = [$this->url];
foreach ($this->variants as $variant => $name) {
$urls[] = str_replace($this->name, $name, $this->url);
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cloudflare.com/client/v4/zones/" . $zone_id . "/purge_cache",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_POSTFIELDS => json_encode(["files" => $urls]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $api_key
],
]);
curl_exec($curl);
curl_close($curl);
}//end if
}
/**
* Get URL path
*/
public function getUrlPath(): string
{
$url = config("filesystems.disks.$this->storage.url");
return "$url/";
} }
/** /**
* Return the file URL * Return the file URL
*
* @return string
*/ */
public function getUrlAttribute() public function getUrlAttribute(): string
{ {
$url = config('filesystems.disks.' . Media::getStorageId($this) . '.url'); if (isset($this->attributes['name']) === true) {
if (empty($url) === false) { return self::getUrlPath() . $this->name;
$replace = [ }
'id' => $this->id,
'name' => $this->name
];
$url = str_ireplace(array_map(function ($item) {
return '%' . $item . '%';
}, array_keys($replace)), array_values($replace), $url);
return $url;
}//end if
return ''; return '';
} }
/** /**
* Return the file owner * Return the file owner
*
* @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
/** /**
* Get the file full local path * Move files to new storage device.
* *
* @return string * @param string $storage The storage ID to move to.
*/ */
public function path() public function moveToStorage(string $storage): void
{ {
return Storage::disk(Media::getStorageId($this))->path($this->name); if ($storage !== $this->storage && Config::has("filesystems.disks.$storage") === true) {
$this->status = "Processing media";
MoveMediaJob::dispatch($this, $storage)->onQueue('media');
$this->save();
}
} }
/** /**
* Get Storage ID * Create new Media from UploadedFile data.
* *
* @param mixed $mediaOrPublic Media object or if file is public. * @param App\Models\Request $request The request data.
* @return string * @param Illuminate\Http\UploadedFile $file The file.
* @return null|Media The result or null if not successful.
*/ */
public static function getStorageId(mixed $mediaOrPublic) public static function createFromUploadedFile(Request $request, UploadedFile $file): ?Media
{ {
$isPublic = true; $request->merge([
'title' => $request->get('title', ''),
'name' => '',
'size' => 0,
'mime_type' => '',
'status' => '',
]);
if ($mediaOrPublic instanceof Media) { if ($request->get('storage') === null) {
$isPublic = empty($mediaOrPublic->permission); // We store images by default locally
} else { if (strpos($file->getMimeType(), 'image/') === 0) {
$isPublic = boolval($mediaOrPublic); $request->merge([
'storage' => 'local',
]);
} else {
$request->merge([
'storage' => 'cdn',
]);
}
} }
return $isPublic === true ? 'public' : 'local'; $mediaItem = $request->user()->media()->create($request->all());
$mediaItem->updateWithUploadedFile($file);
return $mediaItem;
} }
/** /**
* Place uploaded file into storage. Return full path or null * Update Media with UploadedFile data.
* *
* @param UploadedFile $file File to put into storage. * @param Illuminate\Http\UploadedFile $file The file.
* @param boolean $public Is the file available to the public. * @return null|Media The media item.
* @return array|null
*/ */
public static function store(UploadedFile $file, bool $public = true) public function updateWithUploadedFile(UploadedFile $file): ?Media
{ {
$storage = Media::getStorageId($public); if ($file === null || $file->isValid() !== true) {
$name = $file->store('', ['disk' => $storage]); throw new \Exception('The file is invalid.', self::INVALID_FILE_ERROR);
}
if ($file->getSize() > static::getMaxUploadSize()) {
throw new \Exception('The file size is larger then permitted.', self::FILE_SIZE_EXCEEDED_ERROR);
}
$name = static::generateUniqueFileName($file->getClientOriginalName());
if ($name === false) { if ($name === false) {
return null; throw new \Exception('The file name already exists in storage.', self::FILE_NAME_EXISTS_ERROR);
} }
$path = Storage::disk($storage)->path($name); // remove file if there is an existing entry in this medium item
if (in_array($file->getClientOriginalExtension(), ['jpg', 'jpeg', 'png', 'gif']) === true) { if (strlen($this->name) > 0 && strlen($this->storage) > 0) {
// Generate additional image sizes Storage::disk($this->storage)->delete($this->name);
$sizes = [ foreach ($this->variants as $variantName => $fileName) {
'thumb' => [150, 150], Storage::disk($this->storage)->delete($fileName);
'small' => [300, 300],
'medium' => [640, 640],
'large' => [1024, 1024],
'xlarge' => [1536, 1536],
'xxlarge' => [2560, 2560],
];
$images = ['full' => $path];
foreach ($sizes as $sizeName => $size) {
$image = Image::make($path);
$image->resize($size[0], $size[1], function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$newPath = pathinfo($path, PATHINFO_DIRNAME) . '/' . pathinfo($path, PATHINFO_FILENAME) . "-$sizeName." . pathinfo($path, PATHINFO_EXTENSION);
$image->save($newPath);
$images[$sizeName] = $newPath;
} }
// Optimize all images $this->name = '';
$optimizerChain = OptimizerChainFactory::create(); $this->variants = [];
foreach ($images as $imagePath) { }
$optimizerChain->optimize($imagePath);
}
}//end if
return [ if (strlen($this->title) === 0) {
'name' => $name, $this->title = $name;
'path' => $path }
];
$this->name = $name;
$this->size = $file->getSize();
$this->mime_type = $file->getMimeType();
$this->status = 'Processing media';
$this->save();
$temporaryFilePath = generateTempFilePath();
copy($file->path(), $temporaryFilePath);
try {
StoreUploadedFileJob::dispatch($this, $temporaryFilePath)->onQueue('media');
} catch (\Exception $e) {
$this->status = 'Error';
$this->save();
throw $e;
}//end try
return $this;
}
/**
* Download the file from the storage to the user.
*
* @param string $variant The variant to download or null if none.
* @param boolean $fallback Fallback to the original file if the variant is not found.
* @return JsonResponse|StreamedResponse The response.
* @throws BindingResolutionException The Exception.
*/
public function download(string $variant = null, bool $fallback = true)
{
$path = $this->name;
if ($variant !== null) {
if (array_key_exists($variant, $this->variant) === true) {
$path = $this->variant[$variant];
} else {
return response()->json(['message' => 'The resource was not found.'], HttpResponseCodes::HTTP_NOT_FOUND);
}
}
$disk = Storage::disk($this->storage);
if ($disk->exists($path) === true) {
$stream = $disk->readStream($path);
$response = response()->stream(
function () use ($stream) {
fpassthru($stream);
},
200,
[
'Content-Type' => $this->mime_type,
'Content-Length' => $disk->size($path),
'Content-Disposition' => 'attachment; filename="' . basename($path) . '"',
]
);
return $response;
}
return response()->json(['message' => 'The resource was not found.'], HttpResponseCodes::HTTP_NOT_FOUND);
} }
/** /**
* Get the server maximum upload size * Get the server maximum upload size
*
* @return integer
*/ */
public static function maxUploadSize() public static function getMaxUploadSize(): int
{ {
$sizes = [ $sizes = [
ini_get('upload_max_filesize'), ini_get('upload_max_filesize'),
@@ -237,31 +510,122 @@ class Media extends Model
} }
/** /**
* Sanitize filename for upload * Generate a file name that is available within storage.
* *
* @param string $filename Filename to sanitize. * @param string $fileName The proposed file name.
* @return string * @return string|boolean The available file name or false if failed.
*/ */
public static function sanitizeFilename(string $filename) public static function generateUniqueFileName(string $fileName)
{
$index = 1;
$maxTries = 100;
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$fileName = static::sanitizeFilename(pathinfo($fileName, PATHINFO_FILENAME));
if (static::fileNameHasSuffix($fileName) === true || static::fileExistsInStorage("$fileName.$extension") === true || Media::where('name', "$fileName.$extension")->where('status', 'not like', 'failed%')->exists() === true) {
$fileName .= '-';
for ($i = 1; $i < $maxTries; $i++) {
$fileNameIndex = $fileName . $index;
if (static::fileExistsInStorage("$fileNameIndex.$extension") !== true && Media::where('name', "$fileNameIndex.$extension")->where('status', 'not like', 'Failed%')->exists() !== true) {
return "$fileNameIndex.$extension";
}
++$index;
}
return false;
}
return "$fileName.$extension";
}
/**
* Determines if the file name exists in any of the storage disks.
*
* @param string $fileName The file name to check.
* @param boolean $ignoreCache Ignore the file list cache.
* @return boolean If the file exists on any storage disks.
*/
public static function fileExistsInStorage(string $fileName, bool $ignoreCache = false): bool
{
$disks = array_keys(Config::get('filesystems.disks'));
if ($ignoreCache === false) {
if (count(static::$storageFileListCache) === 0) {
$disks = array_keys(Config::get('filesystems.disks'));
foreach ($disks as $disk) {
try {
static::$storageFileListCache[$disk] = Storage::disk($disk)->allFiles();
} catch (\Exception $e) {
Log::error($e->getMessage());
throw new \Exception("Cannot get a file list for storage device '$disk'");
}
}
}
foreach (static::$storageFileListCache as $disk => $files) {
if (in_array($fileName, $files) === true) {
return true;
}
}
} else {
$disks = array_keys(Config::get('filesystems.disks'));
foreach ($disks as $disk) {
try {
if (Storage::disk($disk)->exists($fileName) === true) {
return true;
}
} catch (\Exception $e) {
Log::error($e->getMessage());
throw new \Exception("Cannot verify if file '$fileName' already exists in storage device '$disk'");
}
}
}//end if
return false;
}
/**
* Test if the file name contains a special suffix.
*
* @param string $fileName The file name to test.
* @return boolean If the file name contains the special suffix.
*/
public static function fileNameHasSuffix(string $fileName): bool
{
$suffix = '/(-\d+x\d+|-scaled)$/i';
$fileNameWithoutExtension = pathinfo($fileName, PATHINFO_FILENAME);
return preg_match($suffix, $fileNameWithoutExtension) === 1;
}
/**
* Sanitize fileName for upload
*
* @param string $fileName Filename to sanitize.
*/
private static function sanitizeFilename(string $fileName): string
{ {
/* /*
# file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[<>:"/\\\|?*]| [<>:"/\\\|?*]|
# control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x00-\x1F]| [\x00-\x1F]|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[\x7F\xA0\xAD]| [\x7F\xA0\xAD]|
# URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2 # URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
[#\[\]@!$&\'()+,;=]| [#\[\]@!$&\'()+,;=]|
# URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
[{}^\~`] [{}^\~`]
*/ */
$filename = preg_replace( $fileName = preg_replace(
'~ '~
[<>:"/\\\|?*]| [<>:"/\\\|?*]|
[\x00-\x1F]| [\x00-\x1F]|
@@ -270,37 +634,37 @@ class Media extends Model
[{}^\~`] [{}^\~`]
~x', ~x',
'-', '-',
$filename $fileName
); );
$filename = ltrim($filename, '.-'); $fileName = ltrim($fileName, '.-');
$filename = preg_replace([ $fileName = preg_replace([
// "file name.zip" becomes "file-name.zip" // "file name.zip" becomes "file-name.zip"
'/ +/', '/ +/',
// "file___name.zip" becomes "file-name.zip" // "file___name.zip" becomes "file-name.zip"
'/_+/', '/_+/',
// "file---name.zip" becomes "file-name.zip" // "file---name.zip" becomes "file-name.zip"
'/-+/' '/-+/'
], '-', $filename); ], '-', $fileName);
$filename = preg_replace([ $fileName = preg_replace([
// "file--.--.-.--name.zip" becomes "file.name.zip" // "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/', '/-*\.-*/',
// "file...name..zip" becomes "file.name.zip" // "file...name..zip" becomes "file.name.zip"
'/\.{2,}/' '/\.{2,}/'
], '.', $filename); ], '.', $fileName);
// lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 // lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625
$filename = mb_strtolower($filename, mb_detect_encoding($filename)); $fileName = mb_strtolower($fileName, mb_detect_encoding($fileName));
// ".file-name.-" becomes "file-name" // ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-'); $fileName = trim($fileName, '.-');
$ext = pathinfo($filename, PATHINFO_EXTENSION); $ext = pathinfo($fileName, PATHINFO_EXTENSION);
$filename = mb_strcut( $fileName = mb_strcut(
pathinfo($filename, PATHINFO_FILENAME), pathinfo($fileName, PATHINFO_FILENAME),
0, 0,
(255 - ($ext !== '' ? strlen($ext) + 1 : 0)), (255 - ($ext !== '' ? strlen($ext) + 1 : 0)),
mb_detect_encoding($filename) mb_detect_encoding($fileName)
) . ($ext !== '' ? '.' . $ext : ''); ) . ($ext !== '' ? '.' . $ext : '');
return $filename; return $fileName;
} }
} }

View File

@@ -24,10 +24,8 @@ class Permission extends Model
/** /**
* Get the User associated with this model * Get the User associated with this model
*
* @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }

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

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Enum\HttpResponseCodes;
use App\Jobs\MoveMediaJob;
use App\Jobs\OptimizeMediaJob;
use App\Jobs\StoreUploadedFileJob;
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

@@ -6,6 +6,7 @@ namespace App\Models;
use App\Traits\Uuids; use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
@@ -25,12 +26,12 @@ class User extends Authenticatable implements Auditable
* @var array<int, string> * @var array<int, string>
*/ */
protected $fillable = [ protected $fillable = [
'username',
'first_name', 'first_name',
'last_name', 'last_name',
'email', 'email',
'phone', 'phone',
'password', 'password',
'display_name',
]; ];
/** /**
@@ -66,28 +67,28 @@ class User extends Authenticatable implements Auditable
'permissions' 'permissions'
]; ];
/**
// public function getPermissionsAttribute() { * The default attributes.
// return $this->permissions()->pluck('permission')->toArray(); *
// } * @var string[]
*/
protected $attributes = [
'phone' => '',
];
/** /**
* Get the list of files of the user * Get the list of files of the user
*
* @return HasMany
*/ */
public function permissions() public function permissions(): HasMany
{ {
return $this->hasMany(Permission::class); return $this->hasMany(Permission::class);
} }
/** /**
* Get the permission attribute * Get the permission attribute
*
* @return array
*/ */
public function getPermissionsAttribute() public function getPermissionsAttribute(): array
{ {
return $this->permissions()->pluck('permission')->toArray(); return $this->permissions()->pluck('permission')->toArray();
} }
@@ -96,9 +97,8 @@ class User extends Authenticatable implements Auditable
* Test if user has permission * Test if user has permission
* *
* @param string $permission Permission to test. * @param string $permission Permission to test.
* @return boolean
*/ */
public function hasPermission(string $permission) public function hasPermission(string $permission): bool
{ {
return ($this->permissions()->where('permission', $permission)->first() !== null); return ($this->permissions()->where('permission', $permission)->first() !== null);
} }
@@ -107,11 +107,10 @@ class User extends Authenticatable implements Auditable
* Give permissions to the user * Give permissions to the user
* *
* @param string|array $permissions The permission(s) to give. * @param string|array $permissions The permission(s) to give.
* @return Collection
*/ */
public function givePermission($permissions) public function givePermission($permissions): Collection
{ {
if (!is_array($permissions)) { if (is_array($permissions) === false) {
$permissions = [$permissions]; $permissions = [$permissions];
} }
@@ -132,11 +131,10 @@ class User extends Authenticatable implements Auditable
* Revoke permissions from the user * Revoke permissions from the user
* *
* @param string|array $permissions The permission(s) to revoke. * @param string|array $permissions The permission(s) to revoke.
* @return int
*/ */
public function revokePermission($permissions) public function revokePermission($permissions): int
{ {
if (!is_array($permissions)) { if (is_array($permissions) === false) {
$permissions = [$permissions]; $permissions = [$permissions];
} }
@@ -147,41 +145,41 @@ class User extends Authenticatable implements Auditable
/** /**
* Get the list of files of the user * Get the list of files of the user
*
* @return HasMany
*/ */
public function media() public function media(): HasMany
{ {
return $this->hasMany(Media::class); return $this->hasMany(Media::class);
} }
/** /**
* Get the list of files of the user * Get the list of files of the user
*
* @return HasMany
*/ */
public function posts() public function articles(): HasMany
{ {
return $this->hasMany(Post::class); return $this->hasMany(Article::class);
} }
/** /**
* Get associated user codes * Get associated user codes
*
* @return HasMany
*/ */
public function codes() public function codes(): HasMany
{ {
return $this->hasMany(UserCode::class); return $this->hasMany(UserCode::class);
} }
/** /**
* Get the list of logins of the user * Get the list of logins of the user
*
* @return HasMany
*/ */
public function logins() public function logins(): HasMany
{ {
return $this->hasMany(UserLogins::class); return $this->hasMany(UserLogins::class);
} }
/**
* Get the events associated with the user.
*/
public function events(): BelongsToMany
{
return $this->belongsToMany(Event::class, 'event_user', 'user_id', 'event_id');
}
} }

View File

@@ -23,10 +23,8 @@ class UserCode extends Model
/** /**
* Boot function from Laravel. * Boot function from Laravel.
*
* @return void
*/ */
protected static function boot() protected static function boot(): void
{ {
parent::boot(); parent::boot();
static::creating(function ($model) { static::creating(function ($model) {
@@ -46,10 +44,8 @@ class UserCode extends Model
/** /**
* Generate new code * Generate new code
*
* @return void
*/ */
public function regenerate() public function regenerate(): void
{ {
while (true) { while (true) {
$code = random_int(100000, 999999); $code = random_int(100000, 999999);
@@ -62,20 +58,16 @@ class UserCode extends Model
/** /**
* Clear expired user codes * Clear expired user codes
*
* @return void
*/ */
public static function clearExpired() public static function clearExpired(): void
{ {
UserCode::where('updated_at', '<=', now()->subDays(5))->delete(); UserCode::where('updated_at', '<=', now()->subDays(5))->delete();
} }
/** /**
* Get associated user * Get associated user
*
* @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }

View File

@@ -28,10 +28,8 @@ class UserLogins extends Model
/** /**
* Get the file user * Get the file user
*
* @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }

View File

@@ -2,30 +2,34 @@
namespace App\Providers; namespace App\Providers;
use App\Rules\RequiredIfAny;
use App\Rules\Uniqueish;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Exception; use Exception;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use PDOException; use PDOException;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/** /**
* Register any application services. * Register any application services.
*
* @return void
*/ */
public function register() public function register(): void
{ {
// //
} }
/** /**
* Bootstrap any application services. * Bootstrap any application services.
*
* @return void
*/ */
public function boot() public function boot(): void
{ {
// Storage::macro('public', function ($diskName) {
$public = config("filesystems.disks.{$diskName}.public", false);
return $public;
});
} }
} }

View File

@@ -19,13 +19,9 @@ class AuthServiceProvider extends ServiceProvider
/** /**
* Register any authentication / authorization services. * Register any authentication / authorization services.
*
* @return void
*/ */
public function boot() public function boot(): void
{ {
$this->registerPolicies();
// //
} }
} }

View File

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

View File

@@ -26,35 +26,16 @@ class EventServiceProvider extends ServiceProvider
/** /**
* Register any events for your application. * Register any events for your application.
*
* @return void
*/ */
public function boot() public function boot(): void
{ {
Queue::after(function (JobProcessed $event) { //
// Log::info($event->connectionName);
// Log::info('ID: ' . $event->job->getJobId());
// Log::info('Attempts: ' . $event->job->attempts());
// Log::info('Name: ' . $event->job->getName());
// Log::info('ResolveNAme: ' . $event->job->resolveName());
// Log::info('Queue: ' . $event->job->getQueue());
// Log::info('Body: ' . $event->job->getRawBody());
// Log::info(print_r($event->job->payload(), true));
// $payload = $event->job->payload();
// $data = unserialize($payload['data']['command']);
// Log::info('MAIL: ' . $data->to);
// Log::info('MAIL: ' . get_class($data->mailable));
});
} }
/** /**
* Determine if events and listeners should be automatically discovered. * Determine if events and listeners should be automatically discovered.
*
* @return boolean
*/ */
public function shouldDiscoverEvents() public function shouldDiscoverEvents(): bool
{ {
return false; return false;
} }

View File

@@ -7,6 +7,7 @@ use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvi
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@@ -22,29 +23,8 @@ class RouteServiceProvider extends ServiceProvider
/** /**
* Define your route model bindings, pattern filters, and other route configuration. * Define your route model bindings, pattern filters, and other route configuration.
*
* @return void
*/ */
public function boot() public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{ {
// RateLimiter::for('api', function (Request $request) { // RateLimiter::for('api', function (Request $request) {
// return Limit::perMinute(60)->by($request->user()?->id !== null ?: $request->ip()); // return Limit::perMinute(60)->by($request->user()?->id !== null ?: $request->ip());
@@ -52,14 +32,14 @@ class RouteServiceProvider extends ServiceProvider
$rateLimitEnabled = true; $rateLimitEnabled = true;
$user = auth()->user(); $user = auth()->user();
if (app()->environment('testing')) { if (app()->environment('testing')) {
$rateLimitEnabled = false; $rateLimitEnabled = false;
} elseif ($user !== null && $user->hasPermission('admin/ratelimit') === true) { } elseif ($user !== null && $user->hasPermission('admin/ratelimit') === true) {
// Admin users with the "admin/ratelimit" permission are not rate limited // Admin users with the "admin/ratelimit" permission are not rate limited
$rateLimitEnabled = false; $rateLimitEnabled = false;
} }
if ($rateLimitEnabled === true) { if ($rateLimitEnabled === true) {
RateLimiter::for('api', function (Request $request) { RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(180)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(180)->by($request->user()?->id ?: $request->ip());
@@ -69,5 +49,30 @@ class RouteServiceProvider extends ServiceProvider
return Limit::none(); return Limit::none();
}); });
} }
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
Route::macro('apiAttachmentResource', function ($uri, $controller) {
$singularUri = Str::singular($uri);
Route::get("$uri/{{$singularUri}}/attachments", [$controller, 'getAttachments'])
->name("{{$singularUri}}.attachments.index");
Route::post("$uri/{{$singularUri}}/attachments", [$controller, 'storeAttachment'])
->name("{{$singularUri}}.attachments.store");
Route::match(['put', 'patch'], "$uri/{{$singularUri}}/attachments", [$controller, 'updateAttachments'])
->name("{{$singularUri}}.attachments.update");
Route::delete("$uri/{{$singularUri}}/attachments/{medium}", [$controller, 'deleteAttachment'])
->name("{{$singularUri}}.attachments.destroy");
});
} }
} }

View File

@@ -22,9 +22,8 @@ class Recaptcha implements Rule
* *
* @param mixed $attribute Attribute name. * @param mixed $attribute Attribute name.
* @param mixed $value Attribute value. * @param mixed $value Attribute value.
* @return boolean
*/ */
public function passes(mixed $attribute, mixed $value) public function passes(mixed $attribute, mixed $value): bool
{ {
$endpoint = config('services.google_recaptcha'); $endpoint = config('services.google_recaptcha');
@@ -42,10 +41,8 @@ class Recaptcha implements Rule
/** /**
* Get the validation error message. * Get the validation error message.
*
* @return string
*/ */
public function message() public function message(): string
{ {
return 'Captcha failed. Refresh the page and try again'; return 'Captcha failed. Refresh the page and try again';
} }

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Rules;
use App\Models\Media;
use Illuminate\Contracts\Validation\Rule;
class UniqueFileName implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param mixed $value
*/
public function passes(string $attribute, $value): bool
{
return (Media::fileExists($value) === false);
}
/**
* Get the validation error message.
*/
public function message(): string
{
return 'The file name already exists.';
}
}

103
app/Rules/Uniqueish.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\DB;
use PDO;
class Uniqueish implements Rule
{
/**
* The table name to compare.
*
* @var string
*/
protected $table;
/**
* The column name to compare.
*
* @var string|null
*/
protected $column;
/**
* The ID of the record to be ignored.
*
* @var mixed
*/
protected $ignoreId;
/**
* Create a new rule instance.
*
* @param string $table The table name to compare.
* @param string $column The column name to compare.
* @return void
*/
public function __construct(string $table, string $column = null)
{
$this->table = $table;
$this->column = $column;
}
/**
* Set the ID of the record to be ignored.
*
* @param mixed $id The ID to ignore.
*/
public function ignore(mixed $id): static
{
$this->ignoreId = $id;
return $this;
}
/**
* Determine if the validation rule passes.
*
* @param mixed $attribute Not used.
* @param mixed $value The value to compare.
*/
public function passes(mixed $attribute, mixed $value): bool
{
$columnName = ($this->column ?? $attribute);
$similarValue = preg_replace('/[^A-Za-z]/', '', strtolower($value));
try {
$query = DB::table($this->table)
->where($columnName, 'like', '%' . $similarValue . '%');
if ($this->ignoreId !== null) {
$query->where('id', '<>', $this->ignoreId);
}
$query->whereRaw('LOWER(REGEXP_REPLACE(' . $columnName . ', \'[^A-Za-z0-9]\', \'\')) = ?', [$similarValue]);
$result = $query->first();
} catch (\Illuminate\Database\QueryException $e) {
// Fall back to performing the regex replace in PHP
// $results = $query->get();
$query = DB::table($this->table);
$results = $query->get();
foreach ($results as $result) {
$resultValue = preg_replace('/[^A-Za-z0-9]/', '', strtolower($result->{$columnName}));
if ($resultValue === $similarValue && $result->id != $this->ignoreId) {
return false; // Value already exists in the table
}
}
return true; // Value does not exist in the table
}//end try
return $result === null;
}
/**
* Get the validation error message.
*/
public function message(): string
{
return 'The :attribute is similar to one that already exists. Please choose another.';
}
}

View File

@@ -11,7 +11,7 @@ class AnimatedGifService
* @param integer $dataSize GIF blob size. * @param integer $dataSize GIF blob size.
* @return boolean GIF file/blob is animated. * @return boolean GIF file/blob is animated.
*/ */
public static function isAnimatedGif(string $filenameOrBlob, int $dataSize = 0) public static function isAnimatedGif(string $filenameOrBlob, int $dataSize = 0): bool
{ {
$regex = '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s'; $regex = '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s';
$count = 0; $count = 0;
@@ -41,10 +41,8 @@ class AnimatedGifService
* @param string $filenameOrBlob GIF filename path * @param string $filenameOrBlob GIF filename path
* @param integer $dataSize GIF blob size. * @param integer $dataSize GIF blob size.
* @param boolean $originalFrames Get original frames (with transparent background) * @param boolean $originalFrames Get original frames (with transparent background)
*
* @return array
*/ */
public function extract(string $filenameOrBlob, int $dataSize = 0, $originalFrames = false) public function extract(string $filenameOrBlob, int $dataSize = 0, bool $originalFrames = false): array
{ {
if (self::isAnimatedGif($filenameOrBlob) === false) { if (self::isAnimatedGif($filenameOrBlob) === false) {
return []; return [];
@@ -198,7 +196,7 @@ class GifFrameExtractor
* *
* @param string $filename GIF filename path * @param string $filename GIF filename path
*/ */
private function parseFramesInfo($filename) private function parseFramesInfo(string $filename)
{ {
$this->openFile($filename); $this->openFile($filename);
$this->parseGifHeader(); $this->parseGifHeader();
@@ -275,10 +273,8 @@ class GifFrameExtractor
/** /**
* Parse the graphic extension of the frames (old: get_graphics_extension) * Parse the graphic extension of the frames (old: get_graphics_extension)
*
* @param integer $type
*/ */
private function parseGraphicsExtension($type) private function parseGraphicsExtension(int $type)
{ {
$startdata = $this->readByte(2); $startdata = $this->readByte(2);
@@ -303,10 +299,8 @@ class GifFrameExtractor
/** /**
* Get the full frame string block (old: get_image_block) * Get the full frame string block (old: get_image_block)
*
* @param integer $type
*/ */
private function getFrameString($type) private function getFrameString(int $type)
{ {
if ($this->checkByte(0x2c)) { if ($this->checkByte(0x2c)) {
$start = $this->pointer; $start = $this->pointer;
@@ -400,14 +394,8 @@ class GifFrameExtractor
/** /**
* Get the image data byte (old: get_imagedata_byte) * Get the image data byte (old: get_imagedata_byte)
*
* @param string $type
* @param integer $start
* @param integer $length
*
* @return string
*/ */
private function getImageDataByte($type, $start, $length) private function getImageDataByte(string $type, int $start, int $length): string
{ {
if ($type == "ext") { if ($type == "ext") {
return substr($this->frameSources[$this->frameNumber]["graphicsextension"], $start, $length); return substr($this->frameSources[$this->frameNumber]["graphicsextension"], $start, $length);
@@ -419,15 +407,8 @@ class GifFrameExtractor
/** /**
* Get the image data bit (old: get_imagedata_bit) * Get the image data bit (old: get_imagedata_bit)
*
* @param string $type
* @param integer $byteIndex
* @param integer $bitStart
* @param integer $bitLength
*
* @return number
*/ */
private function getImageDataBit($type, $byteIndex, $bitStart, $bitLength) private function getImageDataBit(string $type, int $byteIndex, int $bitStart, int $bitLength): number
{ {
if ($type == "ext") { if ($type == "ext") {
return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["graphicsextension"], $byteIndex, 1)), $bitStart, $bitLength); return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["graphicsextension"], $byteIndex, 1)), $bitStart, $bitLength);
@@ -439,12 +420,8 @@ class GifFrameExtractor
/** /**
* Return the value of 2 ASCII chars (old: dualbyteval) * Return the value of 2 ASCII chars (old: dualbyteval)
*
* @param string $s
*
* @return integer
*/ */
private function dualByteVal($s) private function dualByteVal(string $s): int
{ {
$i = (ord($s[1]) * 256 + ord($s[0])); $i = (ord($s[1]) * 256 + ord($s[0]));
@@ -453,10 +430,8 @@ class GifFrameExtractor
/** /**
* Read the data stream (old: read_data_stream) * Read the data stream (old: read_data_stream)
*
* @param integer $firstLength
*/ */
private function readDataStream($firstLength) private function readDataStream(int $firstLength)
{ {
$this->pointerForward($firstLength); $this->pointerForward($firstLength);
$length = $this->readByteInt(); $length = $this->readByteInt();
@@ -471,10 +446,8 @@ class GifFrameExtractor
/** /**
* Open the gif file (old: loadfile) * Open the gif file (old: loadfile)
*
* @param string $filename
*/ */
private function openFile($filename) private function openFile(string $filename)
{ {
$this->handle = fopen($filename, "rb"); $this->handle = fopen($filename, "rb");
$this->pointer = 0; $this->pointer = 0;
@@ -495,12 +468,8 @@ class GifFrameExtractor
/** /**
* Read the file from the beginning to $byteCount in binary (old: readbyte) * Read the file from the beginning to $byteCount in binary (old: readbyte)
*
* @param integer $byteCount
*
* @return string
*/ */
private function readByte($byteCount) private function readByte(int $byteCount): string
{ {
$data = fread($this->handle, $byteCount); $data = fread($this->handle, $byteCount);
$this->pointer += $byteCount; $this->pointer += $byteCount;
@@ -510,10 +479,8 @@ class GifFrameExtractor
/** /**
* Read a byte and return ASCII value (old: readbyte_int) * Read a byte and return ASCII value (old: readbyte_int)
*
* @return integer
*/ */
private function readByteInt() private function readByteInt(): int
{ {
$data = fread($this->handle, 1); $data = fread($this->handle, 1);
$this->pointer++; $this->pointer++;
@@ -523,14 +490,8 @@ class GifFrameExtractor
/** /**
* Convert a $byte to decimal (old: readbits) * Convert a $byte to decimal (old: readbits)
*
* @param string $byte
* @param integer $start
* @param integer $length
*
* @return number
*/ */
private function readBits($byte, $start, $length) private function readBits(string $byte, int $start, int $length): number
{ {
$bin = str_pad(decbin($byte), 8, "0", STR_PAD_LEFT); $bin = str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
$data = substr($bin, $start, $length); $data = substr($bin, $start, $length);
@@ -540,10 +501,8 @@ class GifFrameExtractor
/** /**
* Rewind the file pointer reader (old: p_rewind) * Rewind the file pointer reader (old: p_rewind)
*
* @param integer $length
*/ */
private function pointerRewind($length) private function pointerRewind(int $length)
{ {
$this->pointer -= $length; $this->pointer -= $length;
fseek($this->handle, $this->pointer); fseek($this->handle, $this->pointer);
@@ -551,10 +510,8 @@ class GifFrameExtractor
/** /**
* Forward the file pointer reader (old: p_forward) * Forward the file pointer reader (old: p_forward)
*
* @param integer $length
*/ */
private function pointerForward($length) private function pointerForward(int $length)
{ {
$this->pointer += $length; $this->pointer += $length;
fseek($this->handle, $this->pointer); fseek($this->handle, $this->pointer);
@@ -562,13 +519,8 @@ class GifFrameExtractor
/** /**
* Get a section of the data from $start to $start + $length (old: datapart) * Get a section of the data from $start to $start + $length (old: datapart)
*
* @param integer $start
* @param integer $length
*
* @return string
*/ */
private function dataPart($start, $length) private function dataPart(int $start, int $length): string
{ {
fseek($this->handle, $start); fseek($this->handle, $start);
$data = fread($this->handle, $length); $data = fread($this->handle, $length);
@@ -579,12 +531,8 @@ class GifFrameExtractor
/** /**
* Check if a character if a byte (old: checkbyte) * Check if a character if a byte (old: checkbyte)
*
* @param integer $byte
*
* @return boolean
*/ */
private function checkByte($byte) private function checkByte(int $byte): bool
{ {
if (fgetc($this->handle) == chr($byte)) { if (fgetc($this->handle) == chr($byte)) {
fseek($this->handle, $this->pointer); fseek($this->handle, $this->pointer);
@@ -598,10 +546,8 @@ class GifFrameExtractor
/** /**
* Check the end of the file (old: checkEOF) * Check the end of the file (old: checkEOF)
*
* @return boolean
*/ */
private function checkEOF() private function checkEOF(): bool
{ {
if (fgetc($this->handle) === false) { if (fgetc($this->handle) === false) {
return true; return true;
@@ -628,70 +574,56 @@ class GifFrameExtractor
/** /**
* Get the total of all added frame duration * Get the total of all added frame duration
*
* @return integer
*/ */
public function getTotalDuration() public function getTotalDuration(): int
{ {
return $this->totalDuration; return $this->totalDuration;
} }
/** /**
* Get the number of extracted frames * Get the number of extracted frames
*
* @return integer
*/ */
public function getFrameNumber() public function getFrameNumber(): int
{ {
return $this->frameNumber; return $this->frameNumber;
} }
/** /**
* Get the extracted frames (images and durations) * Get the extracted frames (images and durations)
*
* @return array
*/ */
public function getFrames() public function getFrames(): array
{ {
return $this->frames; return $this->frames;
} }
/** /**
* Get the extracted frame positions * Get the extracted frame positions
*
* @return array
*/ */
public function getFramePositions() public function getFramePositions(): array
{ {
return $this->framePositions; return $this->framePositions;
} }
/** /**
* Get the extracted frame dimensions * Get the extracted frame dimensions
*
* @return array
*/ */
public function getFrameDimensions() public function getFrameDimensions(): array
{ {
return $this->frameDimensions; return $this->frameDimensions;
} }
/** /**
* Get the extracted frame images * Get the extracted frame images
*
* @return array
*/ */
public function getFrameImages() public function getFrameImages(): array
{ {
return $this->frameImages; return $this->frameImages;
} }
/** /**
* Get the extracted frame durations * Get the extracted frame durations
*
* @return array
*/ */
public function getFrameDurations() public function getFrameDurations(): array
{ {
return $this->frameDurations; return $this->frameDurations;
} }

View File

@@ -8,10 +8,8 @@ trait Uuids
{ {
/** /**
* Boot function from Laravel. * Boot function from Laravel.
*
* @return void
*/ */
protected static function bootUuids() protected static function bootUuids(): void
{ {
static::creating(function ($model) { static::creating(function ($model) {
if (empty($model->{$model->getKeyName()}) === true) { if (empty($model->{$model->getKeyName()}) === true) {
@@ -22,20 +20,16 @@ trait Uuids
/** /**
* Get the value indicating whether the IDs are incrementing. * Get the value indicating whether the IDs are incrementing.
*
* @return boolean
*/ */
public function getIncrementing() public function getIncrementing(): bool
{ {
return false; return false;
} }
/** /**
* Get the auto-incrementing key type. * Get the auto-incrementing key type.
*
* @return string
*/ */
public function getKeyType() public function getKeyType(): string
{ {
return 'string'; return 'string';
} }

View File

@@ -8,30 +8,33 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.0.2", "php": "^8.1",
"doctrine/dbal": "^3.5", "doctrine/dbal": "^3.5",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7", "intervention/image": "^2.7",
"laravel/framework": "^9.19", "laravel/framework": "^10.12",
"laravel/sanctum": "^3.0", "laravel/sanctum": "^3.2",
"laravel/tinker": "^2.7", "laravel/tinker": "^2.8",
"owen-it/laravel-auditing": "^13.0", "league/flysystem-aws-s3-v3": "^3.12",
"owen-it/laravel-auditing": "^13.1",
"php-ffmpeg/php-ffmpeg": "^1.1", "php-ffmpeg/php-ffmpeg": "^1.1",
"spatie/image-optimizer": "^1.6", "sunspikes/clamav-validator": "*",
"thiagoalessio/tesseract_ocr": "^2.12" "thiagoalessio/tesseract_ocr": "^2.12",
"vlucas/phpdotenv": "^5.5"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.0.1", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^6.1", "nunomaduro/collision": "^7.1",
"phpunit/phpunit": "^9.5.10", "phpunit/phpunit": "^10.1.3",
"spatie/laravel-ignition": "^1.0" "spatie/laravel-ignition": "^2.0"
}, },
"autoload": { "autoload": {
"files": [ "files": [
"app/Helpers/Array.php" "app/Helpers/Array.php",
"app/Helpers/Temp.php"
], ],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",
@@ -74,6 +77,6 @@
"pestphp/pest-plugin": true "pestphp/pest-plugin": true
} }
}, },
"minimum-stability": "dev", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

2046
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
return [ return [
@@ -154,34 +155,7 @@ return [
| |
*/ */
'providers' => [ 'providers' => ServiceProvider::defaultProviders()->merge([
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/* /*
* Package Service Providers... * Package Service Providers...
*/ */
@@ -196,8 +170,7 @@ return [
// App\Providers\BroadcastServiceProvider::class, // App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class, App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class, App\Providers\RouteServiceProvider::class,
])->toArray(),
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -80,16 +80,20 @@ return [
| than one user table or model in the application and you want to have | than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types. | separate password reset settings based on the specific user types.
| |
| The expire time is the number of minutes that each reset token will be | The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so | considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed. | they have less time to be guessed. You may change this as needed.
| |
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/ */
'passwords' => [ 'passwords' => [
'users' => [ 'users' => [
'provider' => 'users', 'provider' => 'users',
'table' => 'password_resets', 'table' => 'password_reset_tokens',
'expire' => 60, 'expire' => 60,
'throttle' => 60, 'throttle' => 60,
], ],

View File

@@ -36,7 +36,7 @@ return [
'secret' => env('PUSHER_APP_SECRET'), 'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'), 'app_id' => env('PUSHER_APP_ID'),
'options' => [ 'options' => [
'host' => env('PUSHER_HOST') === true ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com', 'host' => env('PUSHER_HOST') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
'port' => env('PUSHER_PORT', 443), 'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'), 'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true, 'encrypted' => true,

68
config/clamav.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Preferred socket
|--------------------------------------------------------------------------
|
| This option controls the socket which is used, which is unix_socket or tcp_socket.
|
| Please note if the unix_socket is used and the socket-file is not found the tcp socket will be
| used as fallback.
*/
'preferred_socket' => env('CLAMAV_PREFERRED_SOCKET', 'unix_socket'),
/*
|--------------------------------------------------------------------------
| Unix Socket
|--------------------------------------------------------------------------
| This option defines the location to the unix socket-file. For example
| /var/run/clamav/clamd.ctl
*/
'unix_socket' => env('CLAMAV_UNIX_SOCKET', '/var/run/clamav/clamd.ctl'),
/*
|--------------------------------------------------------------------------
| TCP Socket
|--------------------------------------------------------------------------
| This option defines the TCP socket to the ClamAV instance.
*/
'tcp_socket' => env('CLAMAV_TCP_SOCKET', 'tcp://127.0.0.1:3310'),
/*
|--------------------------------------------------------------------------
| Socket connect timeout
|--------------------------------------------------------------------------
| This option defines the maximum time to wait in seconds for socket connection attempts before failure or timeout, default null = no limit.
*/
'socket_connect_timeout' => env('CLAMAV_SOCKET_CONNECT_TIMEOUT', null),
/*
|--------------------------------------------------------------------------
| Socket read timeout
|--------------------------------------------------------------------------
| This option defines the maximum time to wait in seconds for a read.
*/
'socket_read_timeout' => env('CLAMAV_SOCKET_READ_TIMEOUT', 30),
/*
|--------------------------------------------------------------------------
| Throw exceptions instead of returning failures when scan fails.
|--------------------------------------------------------------------------
| This makes it easier for a developer to find the source of a clamav
| failure, but an end user may only see a 500 error for the user
| if exceptions are not displayed.
*/
'client_exceptions' => env('CLAMAV_CLIENT_EXCEPTIONS', false),
/*
|--------------------------------------------------------------------------
| Skip validation
|--------------------------------------------------------------------------
| This skips the virus validation for current environment.
|
| Please note when true it won't connect to ClamAV and will skip the virus validation.
*/
'skip_validation' => env('CLAMAV_SKIP_VALIDATION', false),
];

View File

@@ -58,9 +58,8 @@ return [
'prefix_indexes' => true, 'prefix_indexes' => true,
'strict' => true, 'strict' => true,
'engine' => null, 'engine' => null,
'options' => extension_loaded('pdo_mysql') === true ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::ATTR_TIMEOUT => env('DB_TIMEOUT', 30),
]) : [], ]) : [],
], ],

View File

@@ -32,16 +32,50 @@ return [
'local' => [ 'local' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app/uploads'), 'root' => storage_path('app/public'),
'url' => env('APP_URL') . "/storage",
'public' => true,
'throw' => false, 'throw' => false,
'url' => env('STORAGE_LOCAL_URL'), ],
'cdn' => [
'driver' => 's3',
'key' => env('AWS_PUBLIC_ACCESS_KEY_ID'),
'secret' => env('AWS_PUBLIC_SECRET_ACCESS_KEY'),
'region' => env('AWS_PUBLIC_DEFAULT_REGION'),
'bucket' => env('AWS_PUBLIC_BUCKET'),
'url' => env('AWS_PUBLIC_URL'),
'endpoint' => env('AWS_PUBLIC_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_PUBLIC_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'public' => true,
'options' => [
'ACL' => '',
]
],
'private' => [
'driver' => 's3',
'key' => env('AWS_PRIVATE_ACCESS_KEY_ID'),
'secret' => env('AWS_PRIVATE_SECRET_ACCESS_KEY'),
'region' => env('AWS_PRIVATE_DEFAULT_REGION'),
'bucket' => env('AWS_PRIVATE_BUCKET'),
'url' => env('AWS_PRIVATE_URL'),
'endpoint' => env('AWS_PRIVATE_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_PRIVATE_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'public' => false,
'options' => [
'ACL' => '',
]
], ],
'public' => [ 'public' => [
'driver' => 'local', 'driver' => 'local',
'root' => public_path('uploads'), 'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false, 'throw' => false,
'url' => env('STORAGE_PUBLIC_URL'),
], ],
's3' => [ 's3' => [

View File

@@ -3,6 +3,7 @@
use Monolog\Handler\NullHandler; use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler; use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [ return [
@@ -61,6 +62,7 @@ return [
'driver' => 'single', 'driver' => 'single',
'path' => storage_path('logs/laravel.log'), 'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
], ],
'daily' => [ 'daily' => [
@@ -68,6 +70,7 @@ return [
'path' => storage_path('logs/laravel.log'), 'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),
'days' => 14, 'days' => 14,
'replace_placeholders' => true,
], ],
'slack' => [ 'slack' => [
@@ -76,6 +79,7 @@ return [
'username' => 'Laravel Log', 'username' => 'Laravel Log',
'emoji' => ':boom:', 'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'), 'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
], ],
'papertrail' => [ 'papertrail' => [
@@ -87,6 +91,7 @@ return [
'port' => env('PAPERTRAIL_PORT'), 'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
], ],
'processors' => [PsrLogMessageProcessor::class],
], ],
'stderr' => [ 'stderr' => [
@@ -97,16 +102,20 @@ return [
'with' => [ 'with' => [
'stream' => 'php://stderr', 'stream' => 'php://stderr',
], ],
'processors' => [PsrLogMessageProcessor::class],
], ],
'syslog' => [ 'syslog' => [
'driver' => 'syslog', 'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),
'facility' => LOG_USER,
'replace_placeholders' => true,
], ],
'errorlog' => [ 'errorlog' => [
'driver' => 'errorlog', 'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
], ],
'null' => [ 'null' => [

View File

@@ -28,7 +28,7 @@ return [
| sending an e-mail. You will specify which one you are using for your | sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required. | mailers below. You are free to add additional mailers as required.
| |
| Supported: "smtp", "sendmail", "mailgun", "ses", | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "log", "array", "failover" | "postmark", "log", "array", "failover"
| |
*/ */
@@ -51,10 +51,16 @@ return [
'mailgun' => [ 'mailgun' => [
'transport' => 'mailgun', 'transport' => 'mailgun',
// 'client' => [
// 'timeout' => 5,
// ],
], ],
'postmark' => [ 'postmark' => [
'transport' => 'postmark', 'transport' => 'postmark',
// 'client' => [
// 'timeout' => 5,
// ],
], ],
'sendmail' => [ 'sendmail' => [

View File

@@ -30,10 +30,14 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'), 'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
], ],
<<<<<<< HEAD
'google_recaptcha' => [ 'google_recaptcha' => [
'url' => 'https://www.google.com/recaptcha/api/siteverify', 'url' => 'https://www.google.com/recaptcha/api/siteverify',
'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'), 'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'),
'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_SITE_KEY'), 'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_SITE_KEY'),
], ],
=======
>>>>>>> 086cf9e (removed obsolete recaptcha)
]; ];

View File

@@ -8,14 +8,14 @@ use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event> * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/ */
class PostFactory extends Factory class ArticleFactory extends Factory
{ {
/** /**
* Define the model's default state. * Define the model's default state.
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function definition() public function definition(): array
{ {
$publishDate = Carbon::parse($this->faker->dateTimeBetween('-1 month', '+1 month')); $publishDate = Carbon::parse($this->faker->dateTimeBetween('-1 month', '+1 month'));
@@ -24,8 +24,8 @@ class PostFactory extends Factory
'slug' => $this->faker->slug(), 'slug' => $this->faker->slug(),
'publish_at' => $publishDate, 'publish_at' => $publishDate,
'content' => $this->faker->paragraphs(3, true), 'content' => $this->faker->paragraphs(3, true),
'user_id' => $this->faker->uuid, 'user_id' => $this->faker->uuid(),
'hero' => $this->faker->uuid, 'hero' => $this->faker->uuid(),
]; ];
} }
} }

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