Compare commits

...

978 Commits

Author SHA1 Message Date
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
eb32c99764 added event attachments support 2023-04-01 07:24:09 +10:00
8305f16dae added function docs 2023-04-01 07:24:09 +10:00
a74ace3bbd updated api.put to use params 2023-04-01 07:24:09 +10:00
96c8774e31 added attachments array to Event type 2023-04-01 07:24:09 +10:00
45895bddae added update event attachments route 2023-04-01 07:24:09 +10:00
e29e443078 alignment fixws 2023-03-31 13:51:05 +10:00
f213aeb93a added download option 2023-03-29 20:42:36 +10:00
b68b0f1583 added file types 2023-03-29 19:57:03 +10:00
52fc4c5c34 dont optimize non-images 2023-03-29 19:53:27 +10:00
d7f81c2f03 actually delete events 2023-03-29 19:17:08 +10:00
9b6e9aeb1c bug fixes 2023-03-29 19:06:40 +10:00
ea3bb13661 debug 2023-03-29 18:56:49 +10:00
ec12679426 debug 2023-03-29 18:52:57 +10:00
25a6d60e73 add debug 2023-03-29 18:50:24 +10:00
24c3a1ef30 prevent user select 2023-03-29 18:29:01 +10:00
fbaef5392f test foreach exists 2023-03-29 16:43:15 +10:00
f52cd448e2 remove debug 2023-03-29 16:43:09 +10:00
7f3eada0c1 debug 2023-03-29 16:02:57 +10:00
181cd2fce1 debug 2023-03-29 15:56:51 +10:00
a5f600e73b remove debug 2023-03-29 15:56:42 +10:00
c96b3d8349 updated returnAsResource parameters 2023-03-29 15:46:06 +10:00
06c9d48126 rules are not merged correctly 2023-03-29 15:21:52 +10:00
0d29fbce45 added attachments support 2023-03-29 15:01:43 +10:00
dc7fd81fc5 fix attachment model path 2023-03-29 14:58:41 +10:00
f7503d1f20 belongs to media 2023-03-29 14:58:31 +10:00
56673fceaa attachments support 2023-03-29 14:58:14 +10:00
3dccc56d16 reload component when attachments change outside 2023-03-29 14:57:59 +10:00
75566e27fa remove debugging 2023-03-29 14:57:46 +10:00
a8627ca89e launch attachments in new window 2023-03-29 14:57:39 +10:00
f49b6fef0f post supports attachments 2023-03-29 14:57:30 +10:00
8b83b0c212 added support for new responseAsResource format 2023-03-29 14:57:18 +10:00
57292ab8de responseAsResource suppports group name override 2023-03-29 14:56:45 +10:00
a26b522356 includes attachments data 2023-03-29 14:56:28 +10:00
e1468e82e4 added collection processing 2023-03-29 14:56:03 +10:00
76102637dd update dates of test 2023-03-29 11:59:20 +10:00
a55fac1bfa bugfix drop zone 2023-03-29 11:56:28 +10:00
052a256422 touch support 2023-03-29 09:44:40 +10:00
3bfcb0c0d4 bug fixes 2023-03-29 09:25:21 +10:00
e18292e352 fix when to purify so not conflict with components 2023-03-29 09:25:09 +10:00
d71a1f5940 moved gallery button to media group 2023-03-29 07:51:59 +10:00
d63f180030 added styling of component 2023-03-29 07:51:07 +10:00
aebdb0b599 improvements to the gallery 2023-03-28 22:40:25 +10:00
3a562005e5 urlMatches now supports an array 2023-03-28 22:40:02 +10:00
320516fd8d bug fixes 2023-03-28 20:45:53 +10:00
971074777d remove debug 2023-03-28 20:26:05 +10:00
22c3b5800d updated image library selector 2023-03-28 20:25:48 +10:00
b0a1197e22 fix scope to apply after filters 2023-03-28 20:25:21 +10:00
79704e2f2b encode url params 2023-03-28 19:53:01 +10:00
9f9faf5554 updated imageLibrary 2023-03-28 16:59:36 +10:00
a927334e06 added urlMatches 2023-03-28 16:59:36 +10:00
74c4c5d2bc fixed large tox height 2023-03-28 16:59:36 +10:00
2bd8acc00f updated packages 2023-03-28 16:59:36 +10:00
22ea843f60 default sorting to starts_at desc 2023-03-28 16:59:36 +10:00
c7fb636ab5 type is link not url 2023-03-27 19:15:23 +10:00
59761e8eac updated component 2023-03-24 21:28:34 +10:00
4f8efa9c90 start of image gallery 2023-03-24 16:55:29 +10:00
132ece89c1 remove obsolete DialogWrapper 2023-03-24 14:36:05 +10:00
d0d5cc6841 update tests to support new promise validation 2023-03-24 12:02:45 +10:00
498fd89239 updated env for npm test 2023-03-24 11:58:53 +10:00
0f99d3e83b updated autoload paths 2023-03-24 11:54:57 +10:00
d0fa4d649b added faker provider path 2023-03-24 11:52:58 +10:00
7d1d74e48a added faker autoload to prod 2023-03-24 11:50:12 +10:00
65e81eaae7 upgraded webpack 2023-03-24 11:46:33 +10:00
85cfdfd24f replaced vue3-promise-dialog for internal component 2023-03-24 11:45:12 +10:00
ad5b47f2a5 updated tests for new SMDate object 2023-03-24 09:31:27 +10:00
238189fd9a call CustomInternetProvider directly 2023-03-24 09:24:56 +10:00
5691a051a6 updated test to run vue tests 2023-03-24 09:24:41 +10:00
d11ac9240c added custom provider for min username length for faker 2023-03-24 09:00:16 +10:00
7c9f901a7a show load errors and run api calls simultaneously 2023-03-24 09:00:16 +10:00
55fffef5cb added getApiResultData helper 2023-03-24 09:00:16 +10:00
eb1c475fd9 added configurable timeout option 2023-03-24 09:00:16 +10:00
7cfeea9641 removed automatic deployment 2023-03-24 09:00:16 +10:00
59a7f02893 Change SMDialog to SMFormCard 2023-03-23 18:14:30 +10:00
ab4ef89c87 fix formatted sort 2023-03-23 18:05:59 +10:00
3d8b85dcf4 added duplicate option 2023-03-14 18:44:34 +10:00
4b9867bd16 fix incorrect parsing of some dates 2023-03-14 18:44:29 +10:00
b45dd84f0f support duplication 2023-03-14 11:44:49 +10:00
b21468f265 added event duplication 2023-03-14 07:57:30 +10:00
19c5bd5c25 use new xxlarge size 2023-03-13 22:33:24 +10:00
64fd34ff1c added helper 2023-03-13 22:33:18 +10:00
c0e7adcc42 improve imageLoad methiod 2023-03-13 22:33:13 +10:00
0f48f21dde support xxlarge size 2023-03-13 22:33:00 +10:00
5f960cca71 fix disposition filename extension 2023-03-13 22:10:15 +10:00
630418cf02 request thumbnail instead of custom size 2023-03-13 22:09:57 +10:00
dd5ac3a2b4 use better image sizes 2023-03-13 21:42:44 +10:00
0385672364 added content-disposition headers 2023-03-13 21:42:32 +10:00
c2a0f04cc0 updated sizes 2023-03-13 21:27:53 +10:00
9be9f4329b add descriptors 2023-03-13 21:24:58 +10:00
667972f05e added image size helper methods 2023-03-13 21:23:11 +10:00
0bbb1f0eba automagic image optimization 2023-03-13 21:15:54 +10:00
a7219861f4 fix progress option to not throw error if http status is any value below 300 2023-03-13 21:15:28 +10:00
09376e8f98 change const form to let form 2023-03-13 20:01:30 +10:00
ac45a6b5ef remove commented line 2023-03-13 20:01:19 +10:00
James Collins
ea732301fd Merge pull request #18 from STEMMechanics/useScopes
Use scopes
2023-03-13 19:49:41 +10:00
d21d1b6993 fix registration_type case check 2023-03-13 19:48:39 +10:00
655c003969 typo 2023-03-13 19:48:10 +10:00
23288e15e0 support value = null 2023-03-13 19:48:01 +10:00
cf3c35ffa3 cleanup before deamalgmation 2023-03-13 19:18:16 +10:00
85c37ba748 fix input fields not being used as an array 2023-03-13 19:02:14 +10:00
154dffeee4 use API path in .env 2023-03-13 19:02:00 +10:00
2cea90c2c8 added tests, bug fixes and cleanup 2023-03-13 16:03:32 +10:00
44b123307a fix class name 2023-03-13 13:14:45 +10:00
7ecec70520 Added registration type of message 2023-03-13 13:13:40 +10:00
7605a826d6 added test 2023-03-13 13:06:59 +10:00
3126991e8f added testing env 2023-03-13 13:06:52 +10:00
7b58303cde added test 2023-03-13 12:31:20 +10:00
58d302fc38 using postRules instead of putRules on PUT request 2023-03-13 12:23:25 +10:00
af4b9b95e7 combine UserRequest to BaseRequest 2023-03-13 12:19:39 +10:00
dc56edf486 added tests 2023-03-13 12:13:02 +10:00
14dd2bb336 allow same username on update 2023-03-13 12:12:55 +10:00
7a1499a0b3 fix missing response 2023-03-13 12:12:32 +10:00
be8ccbd41a remove obsolete uses 2023-03-13 12:12:24 +10:00
970618f561 fix givePermission relationship 2023-03-13 11:25:25 +10:00
8a3d9eec03 rename arrayOnlyItems to arrayLimitKeys 2023-03-13 11:25:13 +10:00
46de5cc0c9 fix transform to show all fields for admin users 2023-03-13 11:24:51 +10:00
0c2ac5d0a5 added give and revoke helper methods 2023-03-13 10:38:50 +10:00
c18b740f46 cleanup 2023-03-12 15:39:43 +10:00
615abcc8e3 added new conductors 2023-03-12 13:51:23 +10:00
3d13fc6864 use new filter option 2023-03-12 13:51:12 +10:00
8244230268 added array helper functions 2023-03-12 13:51:01 +10:00
3f48f11838 support quotes and embed quotes in filter 2023-03-12 13:50:51 +10:00
e16ba2d096 remove filters 2023-03-12 13:50:35 +10:00
874339348c added docs and cleanup 2023-03-11 22:53:16 +10:00
d1833d7b8d added raw "filter" support as well as <> between 2023-03-11 21:38:36 +10:00
b658e96425 fix errors in spliting 2023-03-10 19:27:49 +10:00
0ab92d95ea cleanup comparitors 2023-03-10 17:59:53 +10:00
a9b480994a support not equals 2023-03-10 15:35:15 +10:00
3bd5c064c3 converted from filters to conductors 2023-03-10 13:46:30 +10:00
6bee6b1ba7 added controllers 2023-03-10 13:46:16 +10:00
e11211fcc7 remove unnecessary guarded 2023-03-10 13:46:06 +10:00
d5a703026a fix attachment event endpoints 2023-03-10 12:40:29 +10:00
44481fe107 added guarded properties 2023-03-10 12:39:39 +10:00
57092e1b26 catch promise 2023-03-07 13:03:51 +10:00
b62a3b9d63 updated java/bedrock 2023-03-07 09:26:40 +10:00
7fd65ede5f update merging strign 2023-03-05 20:23:21 +10:00
d3e7938231 improved meta processing and support seo tags 2023-03-05 20:23:05 +10:00
2afb59d4a2 added 2023-03-05 20:22:21 +10:00
d54f2159d9 dont run on PR 2023-03-05 19:17:26 +10:00
e032fb666f use correct components 2023-03-05 16:58:46 +10:00
dca56831af return empty body correctly 2023-03-05 16:58:34 +10:00
937b70e1fd remove console.log 2023-03-05 16:58:25 +10:00
3b0c7d8388 directly set _messsage to empty on creation 2023-03-05 16:39:47 +10:00
d5093110f7 upodate to strongly recommended 2023-03-05 16:24:07 +10:00
25ddcef978 fix button link 2023-03-02 15:45:54 +10:00
5c613df087 fix processing results 2023-03-02 15:10:58 +10:00
0aa44e70de added progress support 2023-03-02 14:08:29 +10:00
2b69d9985e cleanup 2023-03-01 20:14:00 +10:00
86f44c8bab dont attempt to decode body on DELETE method 2023-03-01 20:13:53 +10:00
fa66ee14ee dynamic height 2023-03-01 20:13:40 +10:00
40ed36e0e2 wrap text 2023-03-01 20:13:25 +10:00
b7c8a9ece5 ensure control prop is string 2023-03-01 20:13:15 +10:00
29dfb852c8 dont hide overflow 2023-03-01 20:12:47 +10:00
9de83c3436 default publish_at to now on new items 2023-03-01 19:39:56 +10:00
9a0768a2d5 fix label not rolling back when no value is set 2023-03-01 19:39:42 +10:00
07aa82e7d9 add ages 2023-03-01 19:32:25 +10:00
bc79951700 swap price and ages 2023-03-01 19:32:21 +10:00
155a83cfdd improve ages strings 2023-03-01 18:16:07 +10:00
1ffa7cdbcf added event ages 2023-03-01 17:59:30 +10:00
0f2e1478a8 fix content saving 2023-03-01 17:38:31 +10:00
3efe31c91f event status can now include 'soon' 2023-03-01 17:21:37 +10:00
22ebefc46a show toast on apiErrors 2023-03-01 17:20:58 +10:00
1f8646438d create unique defaultFormControlValidation objects 2023-03-01 17:20:01 +10:00
9ac7769dbc dont show invalid icon on select 2023-03-01 17:09:23 +10:00
5f3cc1a3c6 fix incorrect endpoint 2023-03-01 15:39:15 +10:00
7b882d6705 compression options 2023-03-01 14:17:38 +10:00
33aa01fc78 remove options 2023-03-01 14:11:27 +10:00
542bf9c189 set compression options 2023-03-01 14:09:37 +10:00
d39d599570 use compression 2023-03-01 14:05:56 +10:00
082e2fe2bd updated 2023-03-01 13:36:27 +10:00
9600d1e7d1 updated css 2023-03-01 13:36:18 +10:00
6ab68df4a0 set dynamic titles 2023-03-01 09:46:37 +10:00
9ae8fa4055 fix page errors 2023-03-01 09:44:55 +10:00
52d40cb8a1 updated error pages 2023-03-01 09:38:37 +10:00
30e51dddd7 fix styling 2023-03-01 08:43:21 +10:00
ffbe78fdc1 improve slider design on mobile 2023-03-01 07:54:37 +10:00
adae8888f8 prefix page-error class 2023-03-01 07:43:52 +10:00
a3641828fc cleanup 2023-03-01 07:42:14 +10:00
b91d079787 move scss from app to component 2023-03-01 07:42:06 +10:00
9e2bb48f13 add inital loading indicators 2023-02-28 20:33:43 +10:00
22495294fc margin right on carousel item to prevent overlap of arrow 2023-02-28 19:50:16 +10:00
c83cfff556 added new fail validation callback 2023-02-28 19:42:25 +10:00
35c3108db7 add price line to panel 2023-02-28 19:38:07 +10:00
6155a8768f fix redirect after save 2023-02-28 19:37:58 +10:00
9d16889216 lighten help text 2023-02-28 19:17:47 +10:00
9fd46b9fd9 added event pricing 2023-02-28 19:16:10 +10:00
041dd4b314 update control object with media change 2023-02-28 18:35:26 +10:00
13b2c448b5 fix redirect 2023-02-28 18:35:14 +10:00
a41e209123 only show loading icon if there is an image to load 2023-02-28 18:29:28 +10:00
c24c1f54be support FormData for body 2023-02-28 15:42:16 +10:00
96076af037 remove progress code 2023-02-28 15:42:10 +10:00
1c9bce3b33 add console.log 2023-02-28 15:37:34 +10:00
ef048f77bb fix push on submit 2023-02-28 15:34:59 +10:00
361ead1351 fix upload bug 2023-02-28 15:33:33 +10:00
f031c51ce4 fix 404 on posts create 2023-02-28 15:11:10 +10:00
ce7ba83fff cleanup 2023-02-28 13:41:52 +10:00
eb692de59a fix media-list name 2023-02-28 13:41:44 +10:00
fe66dedecf remove console log 2023-02-28 13:33:53 +10:00
79eea9ea25 panel should show start time 2023-02-28 12:50:46 +10:00
83c25f20bb improve rules text 2023-02-28 12:39:40 +10:00
95c5ed6a32 fix strpos looking at the wrong variable 2023-02-28 12:17:58 +10:00
3f0ebca4da cleanup 2023-02-28 11:45:04 +10:00
03ec852648 relative now works for future dates 2023-02-28 11:34:42 +10:00
6ccc33f762 cleanup 2023-02-28 11:30:35 +10:00
d1bc5c2fe5 load media on mount 2023-02-28 11:25:03 +10:00
ee0d5c363c remove whitespace 2023-02-28 11:24:53 +10:00
86d5706aac change button to use SMButton 2023-02-28 10:16:19 +10:00
2ca199d98f cleanup 2023-02-28 10:16:08 +10:00
562d7a603e disable import ordering as breaks tinymce 2023-02-28 09:56:01 +10:00
c078189f44 fix import order 2023-02-28 09:55:47 +10:00
d060b1f56c improve logic 2023-02-28 09:45:35 +10:00
d91d51a60a added support for ISO 8601 formats 2023-02-28 09:44:31 +10:00
4677400b4f fixed template and logic 2023-02-28 08:56:34 +10:00
f7bac335db added LogsDiscordResponse type 2023-02-28 08:56:24 +10:00
ad3e6e59f2 fix logic 2023-02-28 08:51:37 +10:00
cc38a45fd0 move and refactor scss 2023-02-28 08:49:54 +10:00
7320094a87 improve logic handling 2023-02-28 08:48:09 +10:00
f05691d911 expand user type 2023-02-28 08:47:59 +10:00
3f9e40dbc8 fix the margin bottom for container > input-group 2023-02-28 08:42:55 +10:00
d399427d7f updated text 2023-02-28 08:37:24 +10:00
bf53784135 fix class prefixes 2023-02-28 08:30:24 +10:00
988d48996a fix padding on social links 2023-02-28 08:27:13 +10:00
037873778d add descriptive text 2023-02-28 08:27:04 +10:00
835e8be4dc fix fullscreen layout on mobiles 2023-02-28 08:21:09 +10:00
e384229d7c fix footer link alignment 2023-02-28 08:14:58 +10:00
efcb61781a fix template 2023-02-28 08:08:20 +10:00
a20551db7a fix template 2023-02-28 08:07:16 +10:00
26a3191226 fix typo 2023-02-27 23:55:30 +10:00
James Collins
f6a390595a Moved date checks to computed 2023-02-27 23:45:11 +10:00
e0ac364dff style changes 2023-02-27 23:24:21 +10:00
1b6c5a57e4 add prefix to class 2023-02-27 23:22:20 +10:00
62a3180e53 move date/month to computed 2023-02-27 23:18:58 +10:00
91a4a2c2a4 revert 2023-02-27 23:18:49 +10:00
c40f448aa3 now 2023-02-27 23:05:13 +10:00
7bc89f4463 fixed? 2023-02-27 23:03:24 +10:00
30b487e48c remove formatting 2023-02-27 23:01:06 +10:00
db7ee08937 fix terms 2023-02-27 22:58:42 +10:00
d810340425 fix datetime? 2023-02-27 22:56:42 +10:00
48192240c4 ignore banner 2023-02-27 22:53:59 +10:00
5c6b0085c8 hide courses 2023-02-27 22:52:10 +10:00
c6b088fdf9 fix promise loading 2023-02-27 22:51:36 +10:00
b1769a3326 remove bad import 2023-02-27 22:51:28 +10:00
61f9efe32d override a:visited color 2023-02-27 22:49:04 +10:00
f940278f4d fix post view 2023-02-27 22:45:08 +10:00
James Collins
0665286657 Merge pull request #17 from STEMMechanics/dependency-refactor
Dependency refactor
2023-02-27 22:30:56 +10:00
eb0064e477 cleanup 2023-02-27 22:29:34 +10:00
955c06f5aa cleanup 2023-02-27 19:40:03 +10:00
1ee2a1189d cleanup 2023-02-27 16:08:41 +10:00
c8e49ba49c cleanup 2023-02-27 14:52:01 +10:00
aeb7939c6e cleanup 2023-02-27 09:39:56 +10:00
910330c7dd added types 2023-02-27 09:39:40 +10:00
1315d6368c sort imports on save 2023-02-27 08:45:30 +10:00
c9e2350155 support params in url surrounded by curly braces 2023-02-27 08:43:44 +10:00
35f5b382fa ignore tempCodeRunnerFiles 2023-02-27 08:43:07 +10:00
a530b98fe2 addn stackable addKeyUpListener 2023-02-27 08:27:20 +10:00
26b93442b1 center paragraph 2023-02-26 22:07:43 +10:00
1142033d57 reduce p font-size to 90% 2023-02-26 20:04:16 +10:00
808304303a cleanup 2023-02-26 20:02:01 +10:00
2b298b7c76 fix box shadow clipping 2023-02-26 20:01:52 +10:00
3231063bc2 default attachments to empty array 2023-02-26 19:25:10 +10:00
212c5410e1 support control object 2023-02-26 19:14:12 +10:00
34a90bf218 removed .prevent from button 2023-02-26 19:14:03 +10:00
56f786cd2b cleanup types and added isValid function to controls 2023-02-26 19:13:37 +10:00
cff787f541 added toasts 2023-02-26 17:48:43 +10:00
55ec88e11f improved progress bar design 2023-02-26 13:53:46 +10:00
b718212702 replace progressbar with inhouse 2023-02-26 13:21:01 +10:00
c1dbde2fb7 add clamp method 2023-02-26 13:20:49 +10:00
2eac3f8b0b remove obsolete component 2023-02-26 11:35:50 +10:00
05cc7767bf pop progress on redirect 2023-02-26 11:35:41 +10:00
7c897a7e12 tinymce copy now npm run prepare 2023-02-26 11:34:31 +10:00
06655e2378 removed font-awesome 2023-02-26 11:30:53 +10:00
aad927fb96 added rollup-plugin-analyzer 2023-02-26 11:30:41 +10:00
533fdab150 fix SMDate() to SMDate("now") 2023-02-24 22:18:32 +10:00
93eb6da68d bugfixes with isBefore and isAfter 2023-02-24 22:18:07 +10:00
cc89d45690 begun adding attachment support 2023-02-24 22:05:21 +10:00
f993913438 added 2023-02-24 22:05:11 +10:00
158da60922 cleanup 2023-02-24 22:05:05 +10:00
bb0be1dadf fix z-index so loading cover covers all 2023-02-24 22:04:57 +10:00
c3379f2796 improved 2023-02-24 22:04:40 +10:00
531e1f53fd added file icon functions 2023-02-24 22:04:10 +10:00
62f09f738f added ion-icon visibility 2023-02-24 22:01:56 +10:00
79d5218b16 added file type icons 2023-02-24 22:01:47 +10:00
a068516aa0 cleanup display 2023-02-24 16:04:53 +10:00
9f92bc710c update media type 2023-02-24 16:04:46 +10:00
716cc0eb58 cleanup types 2023-02-24 16:04:37 +10:00
c3bb2179b1 remove unused label 2023-02-24 15:05:57 +10:00
a82e8df06d package updates 2023-02-24 14:41:41 +10:00
40dce90aa0 allow download of any files in uploads or img dirs 2023-02-24 14:41:25 +10:00
37e59bb8a5 updated types 2023-02-24 14:23:18 +10:00
982c124d3b add attachments support 2023-02-24 14:23:12 +10:00
6ebb915c68 add attachments structure 2023-02-24 12:53:26 +10:00
4ee8fd2400 only request images from media endpoint 2023-02-24 12:22:48 +10:00
19b40d50c1 use pixel sizing for indicators 2023-02-24 10:15:25 +10:00
6b14a4bb24 update colours 2023-02-24 10:14:11 +10:00
25790d1c45 add loader bar 2023-02-24 10:11:32 +10:00
4261a35ca7 added soon option 2023-02-23 14:20:57 +10:00
66e2783d97 remove cached content 2023-02-23 10:00:21 +10:00
55bf78497f cleanup of Editor 2023-02-23 09:56:55 +10:00
ac35ce6c47 more completions 2023-02-22 20:23:41 +10:00
3b443aafc5 use new uuid helper library 2023-02-22 20:23:35 +10:00
a2ab077325 seperate uuid helpers 2023-02-22 20:23:23 +10:00
4adb0c953b sm-form-columns no longer required 2023-02-22 20:23:11 +10:00
72a78ba27b clean up meta 2023-02-22 20:22:56 +10:00
e4cdd25922 added sortProperties method 2023-02-22 20:22:49 +10:00
24e2637408 support file uploading 2023-02-22 20:22:38 +10:00
b538eb1929 fix sm prefixes 2023-02-22 20:22:29 +10:00
4e40e8b1db fix mediacollection type 2023-02-22 20:21:52 +10:00
e6aef415f5 give login route a name 2023-02-22 20:21:43 +10:00
0f8f18943b fix response errors 2023-02-22 20:21:34 +10:00
f0accc2e33 update tinymce-copy to include emoticons 2023-02-22 20:21:26 +10:00
14ea157b7a added prism support 2023-02-22 07:20:15 +10:00
686b0a167d add case insentive and force download support 2023-02-22 07:19:59 +10:00
bde47e1251 updated toolbar options 2023-02-21 16:13:58 +10:00
9aaa2e5926 improve the design of the dropdown button 2023-02-21 15:18:47 +10:00
ad74d7bdeb remove fontawesome 2023-02-21 15:06:18 +10:00
ab42eeb370 fix icon and to 2023-02-21 15:04:35 +10:00
a25fc2e7e8 Fix Snyk errors 2023-02-21 14:53:51 +10:00
ca40db79f7 fix Snyk issues 2023-02-21 14:51:49 +10:00
fad2f82b6b add snyk ignore for eTag 2023-02-21 14:39:57 +10:00
1b68387865 fix no posts found while loading 2023-02-21 14:35:57 +10:00
b2ef15d45c rename loadingicon file 2023-02-21 14:21:36 +10:00
998e69085e updated types 2023-02-21 14:19:46 +10:00
77d2786937 Update loading icon 2023-02-21 14:10:11 +10:00
b7814ac2a4 Update loader 2023-02-21 14:09:16 +10:00
516410a649 update loaders 2023-02-21 14:09:13 +10:00
3018048140 use new types 2023-02-21 13:48:27 +10:00
8e4044dffb improve layout 2023-02-21 13:10:57 +10:00
4123845783 added delayed router loader 2023-02-21 12:59:13 +10:00
fd9dcc182f lighten label color 2023-02-21 12:42:03 +10:00
a3a6fbcaa4 place form footer in column 2023-02-21 12:41:01 +10:00
d9e4ccfca8 set active if value exists 2023-02-21 12:40:52 +10:00
ca288a088a improve dashboard box color 2023-02-21 12:36:39 +10:00
a63e73d692 fix api response 2023-02-21 10:43:22 +10:00
5eda87b1ef improve dropdown button design 2023-02-21 10:43:13 +10:00
2ee1bd3658 validation now async 2023-02-21 09:06:28 +10:00
cd689ea1a8 fix DebounceCallback typehinting 2023-02-21 09:06:06 +10:00
3ca5db394a added signal 2023-02-21 09:05:53 +10:00
5ee6d7ba78 fix template to use smpage 2023-02-21 09:05:43 +10:00
d15a44e7d7 move background to page-outer 2023-02-21 09:05:30 +10:00
4bb557e055 fix spacing 2023-02-21 09:05:14 +10:00
f57466b243 added npm run tinymce-copy script 2023-02-21 08:10:57 +10:00
bdceb6d774 disable emoticons plugin 2023-02-20 21:23:12 +10:00
fadf6a3d21 ignore tinymce public 2023-02-20 21:22:26 +10:00
3cd03499c9 cleanup tinymce 2023-02-20 21:22:16 +10:00
9406088f1f swapped trix with tinymce 2023-02-20 20:58:47 +10:00
a8f650d530 added api types 2023-02-20 19:45:29 +10:00
caffbb6c45 updated 2023-02-20 19:45:22 +10:00
4a6a65c5d2 improved matching 2023-02-20 19:11:40 +10:00
b50acd3b74 support date "now" 2023-02-20 18:46:52 +10:00
83f6bce923 cleanup 2023-02-20 18:42:40 +10:00
7c4f377e2b update value when control value changes 2023-02-20 18:42:26 +10:00
8851a7149f merge select type 2023-02-20 18:35:55 +10:00
ced5089caf added datetime type 2023-02-20 18:28:14 +10:00
ce53c94ea3 update to new components 2023-02-20 17:07:01 +10:00
5cfcbacb80 updated layout and use new dropdown button 2023-02-20 17:01:20 +10:00
5acf5e5297 add dropdown button 2023-02-20 16:50:02 +10:00
9337ee292c added multicolumn support 2023-02-20 16:23:46 +10:00
3de48f002c css didnt target ionicon 2023-02-20 16:23:25 +10:00
dad736b0d9 cleanup 2023-02-20 16:17:37 +10:00
7b2e88d089 update SMPanel to be more flexible 2023-02-20 16:12:30 +10:00
0db1bd50fd added 2023-02-20 15:48:28 +10:00
0fcc9b0d7e remove narrow dialog 2023-02-20 13:38:17 +10:00
0349225ede handle when response is not json 2023-02-20 13:37:36 +10:00
5ee55b298f apply style to page-outer 2023-02-20 12:22:48 +10:00
9877f3883a Password invalidation fix 2023-02-20 12:21:22 +10:00
7cfc24cf39 Fix required validation not returning array 2023-02-20 12:21:10 +10:00
9025581db8 fix css classes 2023-02-20 12:16:44 +10:00
731e33a986 improve <640px layout 2023-02-20 12:14:23 +10:00
336e7a7d41 remove narrow option of dialog 2023-02-20 12:12:00 +10:00
3906ef119b better test if image is loaded 2023-02-20 12:08:05 +10:00
4de4642c23 add page background 2023-02-20 11:59:15 +10:00
56eee105d5 fix loading icon 2023-02-20 11:53:23 +10:00
d7e530b2dd fix css layout 2023-02-20 11:53:18 +10:00
54855e2651 update common functions 2023-02-20 11:45:11 +10:00
7de134683d splitup common.js 2023-02-20 11:39:44 +10:00
1d0030e34f rename DFileLink 2023-02-20 10:24:25 +10:00
ccc30a8b7a updates 2023-02-20 10:20:12 +10:00
9961aba160 removed 2023-02-20 10:19:51 +10:00
5414c0b232 added media parser 2023-02-20 10:19:41 +10:00
584e146af0 dependency updates 2023-02-20 10:19:17 +10:00
e035128c30 rule updates 2023-02-20 10:19:04 +10:00
03f5c8d90f further refactoring 2023-02-17 15:38:53 +10:00
ff75f142b3 added filesize 2023-02-17 15:38:46 +10:00
094779a4fd shortcuts support ApiOptions or string 2023-02-17 15:31:09 +10:00
cfa84faab4 updated to new dependencies 2023-02-17 14:44:11 +10:00
c18d7c56c1 added put shortcut 2023-02-17 14:40:34 +10:00
127fea1dfd improve type safety on ApiResponse.data 2023-02-17 14:19:48 +10:00
ebe1069a7f added Url validation 2023-02-17 14:12:05 +10:00
9f42f92c2e support function type for date/time before/after options 2023-02-17 13:50:41 +10:00
c2542472e7 fix date/time options before and after to be optional 2023-02-17 12:39:49 +10:00
2fe0a8c3f0 added datetime validation 2023-02-17 12:38:15 +10:00
e97427c7d7 added form/control support 2023-02-17 12:37:21 +10:00
d1e586bb39 improve datetime regex and added parseAusDateTime 2023-02-17 12:37:12 +10:00
9e2c7c2565 initial dropdown support 2023-02-15 15:18:10 +10:00
29deea73d4 added number, date and time validation 2023-02-15 13:22:59 +10:00
b1e30a603c added aus format validation and other funcs 2023-02-15 13:22:49 +10:00
afc3c94b04 drop axios/date-fns/fontawesome 2023-02-14 15:01:06 +10:00
ac4d3d8ad0 added sm prefix to classes and recaptcha ack 2023-02-11 17:50:36 +10:00
2b939f417c top margin now handled by SMPage 2023-02-11 17:45:28 +10:00
57236d814d ignore dccache 2023-02-11 13:39:19 +10:00
839eaa6db9 add www subdomain if missing 2023-02-06 14:25:42 +10:00
78ccd558aa added drustcraft notice 2023-02-06 12:43:10 +10:00
a5a9db9669 fix scale data 2023-02-06 10:09:18 +10:00
0b70a52177 fix GDImage raw data 2023-02-06 10:06:37 +10:00
f9105577ce clean up 2023-02-06 09:49:21 +10:00
a7e06bef91 added 2023-02-06 09:49:13 +10:00
0bed3aa096 added messages get function 2023-02-06 09:49:10 +10:00
305c018d39 support keras 2023-02-05 14:21:12 +10:00
9404df9a1d update path 2023-02-05 13:31:49 +10:00
a2894791e2 added keras support 2023-02-05 13:30:06 +10:00
c671fb9bf6 remove ffmpeg 2023-02-05 13:01:01 +10:00
5f22142a6d probe correct file 2023-02-05 12:54:30 +10:00
cc1a389eba use FFMpeg 2023-02-05 12:51:03 +10:00
e6174a4456 update phpunit 2023-02-05 12:50:57 +10:00
145b5559ef use ffmpeg 2023-02-05 12:41:50 +10:00
09751208a1 cleanup 2023-02-05 12:32:46 +10:00
22ff92e3a7 added more outputs 2023-02-05 12:27:40 +10:00
8f7f1a35d5 added half scale 2023-02-05 12:23:27 +10:00
1452d6f340 provide double scale 2023-02-05 12:20:59 +10:00
6080c12a7a set default oem 2023-02-05 12:14:41 +10:00
d16231d338 added additional options 2023-02-05 12:08:45 +10:00
14e2e2da3e fix varible override 2023-02-05 12:00:40 +10:00
0fba7849b4 ocr on greyscale filter 2023-02-05 11:57:10 +10:00
6cf5cd1697 remove not required param 2023-02-05 11:39:31 +10:00
be39577466 added OCR support 2023-02-05 11:35:21 +10:00
fe56acd818 added ffmpeg and tesseract dependencies 2023-02-05 11:35:12 +10:00
e84dfcacd1 better styling between 1024 and 768 2023-02-03 20:53:25 +10:00
e639801e37 margin not padding 2023-02-03 20:24:41 +10:00
636ff8c4a9 cleanup 2023-02-03 20:21:11 +10:00
5da4cca80f fix options 2023-02-03 20:20:52 +10:00
c5cc91b075 added get options 2023-02-03 20:14:01 +10:00
e40a2bad16 log file not plural 2023-02-03 20:13:50 +10:00
4367fa4440 dependency updates 2023-02-03 19:31:29 +10:00
a2332451b0 remove obsolete file 2023-02-03 19:28:49 +10:00
f6377a6f3a remove margin top from h1 2023-02-03 19:28:16 +10:00
ba3846b374 ignore for linting 2023-02-03 19:26:08 +10:00
42651ec256 plural 2023-02-03 11:28:24 +10:00
d248204bf9 add plurals 2023-02-03 11:26:28 +10:00
8b84f1c02c code block should be block 2023-02-03 11:24:53 +10:00
3904fe784e dont show tabs/button if error 2023-02-03 11:24:47 +10:00
e5a68fd805 use correct variables 2023-02-03 11:21:34 +10:00
32d39cdc1c add split logs 2023-02-03 11:17:19 +10:00
9188461b1f length 2023-02-03 11:07:15 +10:00
44f6ef4a0b direct log file 2023-02-03 11:04:31 +10:00
b0ae6b0a3d cleanup 2023-02-03 10:41:31 +10:00
James Collins
b6268b3fab Merge pull request #16 from STEMMechanics/feature/discord-bot-logs
Feature/discord bot logs
2023-02-03 10:28:14 +10:00
ba436e5dcc include log controller 2023-02-03 10:25:43 +10:00
dd01a075d8 discord log view 2023-02-03 10:25:35 +10:00
89ebab5134 remove plural 2023-02-03 10:25:27 +10:00
a135c7e8fd style code block 2023-02-03 10:25:16 +10:00
1f8b83fc8c fix permissions and split lines 2023-02-03 10:25:08 +10:00
c378d683db update dependencies 2023-02-03 09:54:11 +10:00
0d1a0cf130 add discord log module 2023-02-03 09:39:47 +10:00
4ec4b5b10b add log controller 2023-02-03 09:39:37 +10:00
d60383211c rename components to match 2023-02-03 09:39:26 +10:00
900909aa5b finally fix multidates? 2023-02-02 17:42:58 +10:00
e33e8ad6f6 fixed array push 2023-02-02 17:34:53 +10:00
7369eb469e fix formatting 2023-02-02 17:31:40 +10:00
c3ebec38d7 bad variable 2023-02-02 17:29:06 +10:00
7f1d0b127d fix multidate display 2023-02-02 17:26:52 +10:00
cb8dc3cfa5 1 heart 2023-01-30 20:23:25 +10:00
ac68b4856a update actions/checkout to v3 2023-01-30 20:22:42 +10:00
26348d9e19 use latest shivammathur/setup-php 2023-01-30 20:21:58 +10:00
d8805a791e fake vite 2023-01-30 20:20:22 +10:00
8fdc7f8600 uncomment db lines 2023-01-30 20:17:57 +10:00
5681990112 require test completion before deploy 2023-01-30 20:07:54 +10:00
ab36df319b fix composer test 2023-01-30 20:06:07 +10:00
f48704804a fix composer test? 2023-01-30 20:04:15 +10:00
05e9315ef2 2 hearts 2023-01-30 19:52:58 +10:00
James Collins
a885d15341 Create laravel.yml 2023-01-30 19:51:09 +10:00
ad69e5c0e2 package updates 2023-01-30 19:46:13 +10:00
824ab331b4 sort from oldest to newest 2023-01-27 20:11:13 +10:00
1f21e22dce fix timestamp formatting 2023-01-27 10:55:26 +10:00
441 changed files with 55589 additions and 15020 deletions

View File

@@ -38,11 +38,24 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
AWS_PUBLIC_ACCESS_KEY_ID=
AWS_PUBLIC_SECRET_ACCESS_KEY=
AWS_PUBLIC_DEFAULT_REGION="us-west-002"
AWS_PUBLIC_BUCKET=
AWS_PUBLIC_USE_PATH_STYLE_ENDPOINT=false
AWS_PUBLIC_ENDPOINT=
AWS_PUBLIC_URL=
AWS_PRIVATE_ACCESS_KEY_ID=
AWS_PRIVATE_SECRET_ACCESS_KEY=
AWS_PRIVATE_DEFAULT_REGION="us-west-002"
AWS_PRIVATE_BUCKET=
AWS_PRIVATE_USE_PATH_STYLE_ENDPOINT=false
AWS_PRIVATE_ENDPOINT=
AWS_PRIVATE_URL=
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_API_KEY=
PUSHER_APP_ID=
PUSHER_APP_KEY=

59
.env.testing Normal file
View File

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

3
.eslintignore Normal file
View File

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

View File

@@ -4,14 +4,15 @@ module.exports = {
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"plugin:vue/vue3-strongly-recommended",
"prettier",
"plugin:jsdoc/recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"vue/multi-word-component-names": "off",
indent: ["error", 4],
indent: ["off", 4, { ignoredNodes: ["ConditionalExpression"] }],
"@typescript-eslint/no-inferrable-types": "off",
},
plugins: ["jsdoc", "@typescript-eslint"],
parser: "vue-eslint-parser",

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

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

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

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

9
.gitignore vendored
View File

@@ -237,4 +237,11 @@ dist/
### This Project ###
/public/uploads
/public/build
*.key
# /public/tinymce
*.key
### Synk ###
.dccache
### TempCodeRunner ###
tempCodeRunnerFile.*

View File

@@ -3,6 +3,7 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
// "source.organizeImports": true // <-- when enabled, breaks tinymce required import order
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
@@ -14,5 +15,6 @@
"[php]": {
// "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
"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));
}
}

View File

@@ -0,0 +1,979 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class Conductor
{
/**
* The Conductors Model class.
*
* @var string|null
*/
protected $class = null;
/**
* The default sorting fields of a collection. Can be an array. Supports - and + prefixes.
*
* @var string|array
*/
protected $sort = "id";
/**
* The default collection size limit per request.
*
* @var integer
*/
protected $limit = 50;
/**
* The maximum collection size limit per request.
*
* @var integer
*/
protected $maxLimit = 100;
/**
* The default includes to include in a request.
*
* @var array
*/
protected $includes = [];
/**
* The default filters to use in a request.
*
* @var array
*/
protected $defaultFilters = [];
/**
* The conductor collection.
*
* @var Collection
*/
protected $collection = null;
/**
* The collection filter to apply.
*
* @var array
*/
protected $filterArray = [];
/**
* The conductor query.
*
* @var Builder
*/
private $query = null;
/**
* Split a string on commas, keeping quotes intact.
*
* @param string $string The string to split.
* @return array The split string.
*/
private function splitString(string $string): array
{
$parts = [];
$start = 0;
$len = strlen($string);
while ($start < $len) {
$commaPos = strpos($string, ',', $start);
$singlePos = strpos($string, '\'', $start);
$doublePos = strpos($string, '"', $start);
// Find the smallest position that is not false
$minPos = false;
if ($commaPos !== false) {
$minPos = $commaPos;
}
if ($singlePos !== false && ($minPos === false || $singlePos < $minPos)) {
$minPos = $singlePos;
}
if ($doublePos !== false && ($minPos === false || $doublePos < $minPos)) {
$minPos = $doublePos;
}
if ($minPos === false) {
// No more commas, single quotes, or double quotes found
$part = substr($string, $start);
$parts[] = trim($part);
break;
} else {
// Add the current part to the parts array
$part = substr($string, $start, ($minPos - $start));
$parts[] = trim($part);
// Update the start position to the next character after the comma, single quote, or double quote
if ($string[$minPos] === ',') {
$start = ($minPos + 1);
} else {
$quoteChar = $string[$minPos];
$endPos = strpos($string, $quoteChar, ($minPos + 1));
if ($endPos === false) {
$part = substr($string, ($minPos + 1));
$parts[] = trim($part);
break;
} else {
$part = substr($string, ($minPos + 1), ($endPos - $minPos - 1));
$parts[] = trim($part);
$start = ($endPos + 1);
}
}
}//end if
}//end while
return array_filter($parts, function ($value) {
return $value !== '';
});
}
/**
* Filter Collection based on the Request.
*
* @param Request $request The user request.
* @param array|null $limitFields A list of fields to limit the filter request to.
*/
private function filter(Request $request, array|null $limitFields = null): void
{
if (is_array($limitFields) === true && count($limitFields) === 0) {
$limitFields = null;
}
$filterFields = $request->all();
if ($limitFields !== null) {
$filterFields = array_intersect_key($filterFields, array_flip($limitFields));
}
$filterFields += $this->defaultFilters;
foreach ($filterFields as $field => $value) {
if (
is_array($limitFields) === false ||
in_array(strtolower($field), array_map('strtolower', $limitFields)) !== false
) {
$value = trim($value);
$operator = '';
$join = 'OR';
// Check if value has a operator and remove it if it's a number
if (preg_match('/^(!?=|[<>]=?|<>|!)([^=!<>].*)*$/', $value, $matches) > 0) {
$operator = $matches[1];
$value = ($matches[2] ?? '');
}
switch ($operator) {
case '=':
$operator = '==';
break;
case '!':
$operator = 'NOT LIKE';
$value = "%{$value}%";
break;
case '>':
case '<':
case '>=':
case '<=':
case '!=':
break;
case '<>':
$separatorPos = strpos($value, '|');
if ($separatorPos === false) {
$operator = '!=';
}
break;
default:
$operator = 'LIKE';
$value = "%{$value}%";
break;
}//end switch
$this->appendFilter($field, $operator, $value, $join);
}//end if
}//end foreach
if ($request->has('filter') === true) {
$this->appendFilterString($request->input('filter', ''), $limitFields);
}
$this->applyFilters();
}
/**
* Apple the filter array to the collection.
*/
final public function applyFilters(): void
{
$parseFunc = function ($filterArray, $query) use (&$parseFunc) {
$item = null;
$result = null;
$join = 'AND';
if (gettype($query) === 'array') {
$item = $query;
}
foreach ($filterArray as $condition) {
$currentResult = false;
if (is_array($condition) === true) {
if (isset($condition[0]) === true && is_array($condition[0]) === true) {
if ($item !== null) {
$currentResult = $parseFunc($condition, $item);
} else {
if ($join === 'OR') {
$query->orWhere(function ($subQuery) use ($parseFunc, $condition) {
$parseFunc($condition, $subQuery);
});
} else {
$query->where(function ($subQuery) use ($parseFunc, $condition) {
$parseFunc($condition, $subQuery);
});
}
}
} else {
list($field, $operator, $value) = $condition;
if ($item !== null) {
if (array_key_exists($field, $item) === true) {
switch ($operator) {
case '==':
$currentResult = ($item[$field] == $value);
break;
case 'NOT LIKE':
$currentResult = (stripos($item[$field], substr($value, 1, -1)) === false);
break;
case '>':
$currentResult = ($item[$field] > $value);
break;
case '<':
$currentResult = ($item[$field] < $value);
break;
case '>=':
$currentResult = ($item[$field] >= $value);
break;
case '<=':
$currentResult = ($item[$field] <= $value);
break;
case '!=':
$currentResult = ($item[$field] != $value);
break;
case '<>':
$separatorPos = strpos($value, '|');
if ($separatorPos !== false) {
$fieldInt = intval($item[$field]);
$currentResult = (
$fieldInt > intVal(
substr($value, 0, $separatorPos)
) && $fieldInt < intVal(substr($value, ($separatorPos + 1))));
} else {
$currentResult = ($item[$field] != $value);
}
break;
case 'LIKE':
$currentResult = (stripos($item[$field], substr($value, 1, -1)) !== false);
break;
}//end switch
}//end if
} else {
if ($operator === '==') {
$operator = '=';
}
if ($join === 'OR') {
if ($operator === '<>') {
$separatorPos = strpos($value, '|');
if ($separatorPos !== false) {
$query->orWhereBetween(
$field,
[substr($value, 0, $separatorPos), substr($value, ($separatorPos + 1))]
);
} else {
$query->orWhere($field, '!=', $value);
}
} else {
$query->orWhere($field, $operator, $value);
}
} else {
if ($operator === '<>') {
$separatorPos = strpos($value, '|');
if ($separatorPos !== false) {
$query->whereBetween(
$field,
[substr($value, 0, $separatorPos), substr($value, ($separatorPos + 1))]
);
} else {
$query->where($field, '!=', $value);
}
} else {
$query->where($field, $operator, $value);
}
}//end if
}//end if
}//end if
if ($item !== null) {
if ($result === null) {
$result = $currentResult;
} else {
if ($join === 'OR') {
$result = $result || $currentResult;
} else {
$result = $result && $currentResult;
}
}
}
$join = 'OR';
} else {
$join = $condition;
}//end if
}//end foreach
return $result;
};
$filterArray = $this->filterArray;
if (count($filterArray) === 0) {
$filterArray = $this->defaultFilters;
}
if (count($filterArray) !== 0) {
if ($this->collection !== null) {
$this->collection = $this->collection->filter(function ($item) use ($parseFunc) {
return $parseFunc($this->filterArray, $item);
});
} else {
$parseFunc($this->filterArray, $this->query);
}
}
}
/**
* Run the conductor on a Request to generate a collection and total.
*
* @param Request $request The request data.
* @return array The processed and transformed collection | the total rows found.
*/
final public static function request(Request $request): array
{
$conductor_class = get_called_class();
$conductor = new $conductor_class();
$total = 0;
try {
$conductor->query = $conductor->class::query();
} catch (\Throwable $e) {
throw new \Exception('Failed to create query builder instance for ' . $conductor->class . '.', 0, $e);
}
// Filter request
$limitFields = $conductor->fields(new $conductor->class());
if (is_array($limitFields) === false) {
$limitFields = [];
}
$conductor->filter($request, $limitFields);
// After Scope query
$conductor->query->where(function ($query) use ($conductor) {
$conductor->scope($query);
});
// Sort request
$sort = $request->input('sort', $conductor->sort);
if (strlen($sort) === 0) {
if (strlen($conductor->sort) > 0) {
$conductor->sort($conductor->sort);
}
} else {
$conductor->sort($sort);
}
// Get total
$total = $conductor->count();
// Paginate
$conductor->paginate($request->input('page', 1), $request->input('limit', -1), $request->input('offset', 0));
// Filter request
$fields = $conductor->fields(new $conductor->class());
if (is_array($fields) === false) {
$fields = [];
}
// Limit fields
$limitFields = array_map(function ($field) {
if (strpos($field, '.') !== false) {
return substr($field, 0, strpos($field, '.'));
}
return $field;
}, explode(',', $request->input('fields')));
if ($limitFields === null) {
$limitFields = $fields;
} else {
$limitFields = array_intersect($limitFields, $fields);
}
$conductor->limitFields($limitFields);
$conductor->collection = $conductor->query->get();
// Transform and Includes
$includes = $conductor->includes;
if (count($limitFields) > 0) {
$includes = array_intersect($limitFields, $conductor->includes);
}
$conductor->collection = $conductor->collection->map(
function ($model) use ($conductor, $includes, $limitFields) {
$conductor->applyIncludes($model, $includes);
if (count($limitFields) > 0) {
$model->setAppends(array_intersect($model->getAppends(), $limitFields));
}
$model = $conductor->transformModel($model);
return $model;
}
);
return [$conductor->collection, $total];
}
/**
* Run the conductor on a collection with the data stored in a Request.
*
* @param Request $request The request data.
* @param Collection $collection The collection.
* @return array The processed and transformed model data.
*/
final public static function collection(Request $request, Collection $collection): array
{
$conductor_class = get_called_class();
$conductor = new $conductor_class();
$conductor->collection = collect();
foreach ($collection as $item) {
if ($conductor->viewable($item) === true) {
$conductor->collection->push($conductor->transformModel($item));
}
}
// Filter request
$limitFields = $conductor->fields(new $conductor->class());
if (is_array($limitFields) === false) {
$limitFields = [];
}
$conductor->filter($request, $limitFields);
// Get total
$total = $conductor->collection->count();
// Sort request
$sort = $request->input('sort', $conductor->sort);
if (strlen($sort) === 0) {
if (strlen($conductor->sort) > 0) {
$conductor->sort($sort);
}
} else {
$conductor->sort($sort);
}
// Paginate
$conductor->paginate($request->input('page', 1), $request->input('limit', -1), $request->input('offset', 0));
return [$conductor->collection, $total];
}
/**
* Filter a custom query on a user request.
*
* @param Builder $query The custom query.
* @param Request $request The request.
* @param array|null $limitFields Limit the request to these fields.
*/
public static function filterQuery(Builder $query, Request $request, array|null $limitFields = null): Builder
{
$conductor_class = get_called_class();
$conductor = new $conductor_class();
$conductor->query = $query;
$conductor->filter($request, $limitFields);
return $conductor->query;
}
/**
* Run the conductor on a Model with the data stored in a Request.
*
* @param Request $request The request data.
* @param string $key The key prefix to use.
* @param Model|null $model The model.
* @return array The processed and transformed model data.
*/
final public static function includeModel(Request $request, string $key, mixed $model): array
{
$fields = [];
if ($request !== null && $request->has('fields') === true) {
$requestFields = $request->input('fields');
if ($requestFields !== null) {
$requestFields = explode(',', $requestFields);
if (in_array($key, $requestFields) === false) {
foreach ($requestFields as $field) {
if (strpos($field, $key . '.') === 0) {
$fields[] = substr($field, (strlen($key) + 1));
}
}
}
}
}
return static::model($fields, $model);
}
/**
* Run the conductor on a Model with the data stored in a Request.
*
* @param mixed $fields The fields to show.
* @param Model|null $model The model.
* @return array The processed and transformed model data.
*/
final public static function model(mixed $fields, mixed $model): array
{
if ($model === null) {
return null;
}
$conductor_class = get_called_class();
$conductor = new $conductor_class();
$modelFields = $conductor->fields(new $conductor->class());
// Limit fields
$limitFields = $modelFields;
if ($fields instanceof Request) {
if ($fields !== null && $fields->has('fields') === true) {
$requestFields = $fields->input('fields');
if ($requestFields !== null) {
$limitFields = array_intersect(explode(',', $requestFields), $modelFields);
}
}
} elseif (is_array($fields) === true && count($fields) > 0) {
$limitFields = array_intersect($fields, $modelFields);
}
if (empty($limitFields) === false) {
$modelAppends = $model->getAppends();
foreach (array_diff($modelFields, $limitFields) as $attribute) {
$key = array_search($attribute, $modelAppends);
if ($key !== false) {
unset($modelAppends[$key]);
} else {
unset($model[$attribute]);
}
}
$model->setAppends($modelAppends);
}
// Includes
$includes = array_intersect($limitFields, $conductor->includes);
$conductor->applyIncludes($model, $includes);
// Transform
$model = $conductor->transformModel($model);
return $model;
}
/**
* Return the current conductor collection count.
*
* @return integer The current collection count.
*/
final public function count(): int
{
if ($this->query !== null) {
return $this->query->count();
}
return 0;
}
/**
* Sort the conductor collection.
*
* @param mixed $fields A field name or array of field names to sort. Supports prefix of +/- to change direction.
*/
final public function sort(mixed $fields = null): void
{
$collectionSort = [];
if (is_string($fields) === true) {
$fields = explode(',', $fields);
} elseif ($fields === null) {
$fields = $this->sort;
}
if (is_array($fields) === true) {
foreach ($fields as $orderByField) {
$direction = 'asc';
$directionChar = substr($orderByField, 0, 1);
if (in_array($directionChar, ['-', '+']) === true) {
$orderByField = substr($orderByField, 1);
if ($directionChar === '-') {
$direction = 'desc';
}
}
if ($this->collection !== null) {
$collectionSort[] = [trim($orderByField), $direction];
} else {
$this->query->orderBy(trim($orderByField), $direction);
}
}
} else {
throw new \InvalidArgumentException('Expected string or array, got ' . gettype($fields));
}//end if
if ($this->collection !== null) {
$this->collection = $this->collection->sortBy($collectionSort)->values();
}
}
/**
* Paginate the conductor collection.
*
* @param integer $page The current page to return.
* @param integer $limit The limit of items to include or use default.
* @param integer $offset Offset the page count after this count of rows.
* @return mixed
*/
final public function paginate(int $page = 1, int $limit = -1, int $offset = 0)
{
// Limit
if ($limit < 1) {
$limit = $this->limit;
} else {
$limit = min($limit, $this->maxLimit);
}
// Page
if ($page < 1) {
$page = 1;
}
// After
if ($offset < 0) {
$offset = 0;
}
if ($this->collection !== null) {
$this->collection = $this->collection->splice(((($page - 1) * $limit) + $offset), $limit);
} else {
$this->query->limit($limit);
$this->query->offset((($page - 1) * $limit) + $offset);
}
}
/**
* Apply a list of includes to the model.
*
* @param Model $model The model to append.
* @param array $includes The list of includes to include.
*/
final public function applyIncludes(Model $model, array $includes): void
{
foreach ($includes as $include) {
$includeMethodName = 'include' . Str::studly($include);
if (method_exists($this, $includeMethodName) === true) {
$attributeName = Str::snake($include);
$attributeValue = $this->{$includeMethodName}($model);
if ($attributeValue !== null) {
$model->$attributeName = $this->{$includeMethodName}($model);
}
}
}
}
/**
* Limit the returned fields in the conductor collection.
*
* @param array $fields An array of field names.
*/
final public function limitFields(array $fields): void
{
if (empty($fields) !== true) {
$this->query->select(array_diff($fields, $this->includes));
}
}
/**
* Filter the conductor collection using raw data.
*
* @param string $rawFilter The raw filter string to parse.
* @param array|null $limitFields The fields to allow in the filter string.
* @param string $outerJoin The join for this filter group.
*/
final public function appendFilterString(string $rawFilter, array|null $limitFields = null, string $outerJoin = 'OR'): void
{
if ($rawFilter === '') {
return;
}
if (substr($rawFilter, -1) !== ',') {
$rawFilter .= ',';
}
$parseFunc = function ($string, &$i = 0) use (&$parseFunc, $limitFields) {
$tokens = [];
$ignoreUntil = '';
$skipUntil = '';
$field = '';
$value = null;
$set = &$field;
for (; $i < strlen($string); $i++) {
$char = $string[$i];
if ($skipUntil !== '' && $char !== $skipUntil) {
continue;
}
if ($ignoreUntil === '') {
if ($char === '\'' || $char === '"') {
$ignoreUntil = $char;
} elseif ($char === ':') {
if ($field === '') {
$skipUntil = ',';
continue;
}
if ($field[0] === '\'' || $field[0] === '"') {
$field = substr($field, 1, -1);
}
$set = &$value;
continue;
} elseif (($char === ')' && $string[($i + 1)] === ',') || $char === ',') {
if ($value === null) {
$tokens[] = $field;
} else {
$value = trim($value);
$operator = 'LIKE';
// Check if value has a operator and remove it if it's a number
if (preg_match('/^(!?=|[<>]=?|<>|!)([^=!<>].*)*$/', $value, $matches) > 0) {
$operator = $matches[1];
$value = ($matches[2] ?? '');
}
if ($value[0] === '\'' || $value[0] === '"') {
$value = substr($value, 1, -1);
}
if ($operator === 'LIKE') {
$value = "%{$value}%";
}
if (
is_array($limitFields) === false ||
in_array(strtolower($field), array_map('strtolower', $limitFields)) !== false
) {
$tokens[] = [$field, $operator, $value];
}
}//end if
$field = '';
$value = null;
$set = &$field;
if ($char === ')') {
$i++;
return $tokens;
}
continue;
} elseif ($char === '(') {
if ($field === '') {
$i++;
$tokens[] = $parseFunc($string, $i);
continue;
}
}//end if
} elseif ($char === $ignoreUntil) {
$ignoreUntil = '';
}//end if
$set .= $char;
}//end for
return $tokens;
};
$i = 0;
$filterArray = $parseFunc($rawFilter, $i);
if (count($this->filterArray) !== 0) {
$this->filterArray[] = $outerJoin;
}
$this->filterArray[] = $filterArray;
}
/**
* Append a field to the filter array.
*
* @param string $field The field name to append.
* @param string $operator The operator to append.
* @param string $value The value to append.
* @param string $join The join to append.
*/
final public function appendFilter(string $field, string $operator, string $value, string $join = 'OR'): void
{
if (count($this->filterArray) !== 0) {
$this->filterArray[] = $join;
}
$this->filterArray[] = [$field, $operator, $value];
}
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
*/
public function scope(Builder $builder): void
{
}
/**
* Return an array of model fields visible to the current user.
*
* @param Model $model The model in question.
* @return array The array of field names.
*/
public function fields(Model $model): array
{
$visibleFields = $model->getVisible();
if (empty($visibleFields) === true) {
$visibleFields = $model->getConnection()
->getSchemaBuilder()
->getColumnListing($model->getTable());
}
$appends = $model->getAppends();
if (is_array($appends) === true) {
$visibleFields = array_merge($visibleFields, $appends);
}
if (is_array($this->includes) === true) {
$visibleFields = array_merge($visibleFields, $this->includes);
}
return $visibleFields;
}
/**
* Transform the passed Model to an array
*
* @param Model $model The model to transform.
* @return array The transformed model.
*/
protected function transformModel(Model $model): array
{
$result = $this->transform($model);
foreach ($result as $key => $value) {
$transformFunction = 'transform' . Str::studly($key);
if (method_exists($this, $transformFunction) === true) {
$result[$key] = $this->$transformFunction($value);
}
}
$result = $this->transformFinal($result);
return $result;
}
/**
* Transform the passed Model to an array
*
* @param Model $model The model to transform.
* @return array The transformed model.
*/
public function transform(Model $model): array
{
$result = $model->toArray();
$fields = $this->fields($model);
if (is_array($fields) === true) {
$result = array_intersect_key($result, array_flip($fields));
}
return $result;
}
/**
* Final Transform of the model array
*
* @param array $data The model array to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
return $data;
}
/**
* Is the passed model viewable by the current user?
*
* @param Model $model The model in question.
* @return boolean Is the model viewable.
*/
public static function viewable(Model $model): bool
{
return true;
}
/**
* Is the model creatable by the current user?
*
* @return boolean Is the model creatable.
*/
public static function creatable(): bool
{
return true;
}
/**
* Is the passed model updatable by the current user?
*
* @param Model $model The model in question.
* @return boolean Is the model updatable.
*/
public static function updatable(Model $model): bool
{
return true;
}
/**
* Is the passed model destroyable by the current user?
*
* @param Model $model The model in question.
* @return boolean Is the model destroyable.
*/
public static function destroyable(Model $model): bool
{
return true;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Conductors;
use App\Models\Media;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\Model;
class EventConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Event::class;
/**
* The default sorting field
* @var string
*/
protected $sort = '-start_at';
/**
* The included fields
* @var string[]
*/
protected $includes = ['attachments'];
/**
* Run a scope query on the collection before anything else.
*
* @param Builder $builder The builder in use.
*/
public function scope(Builder $builder): void
{
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
$builder
->where('status', '!=', 'draft')
->where('publish_at', '<=', now());
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === false) {
return false;
}
}
return true;
}
/**
* Return if the current model is creatable.
*
* @return boolean Allow creating model.
*/
public static function creatable(): bool
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is updatable.
*
* @param Model $model The model.
* @return boolean Allow updating model.
*/
public static function updatable(Model $model): bool
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
$user = auth()->user();
return ($user !== null && $user->hasPermission('admin/events') === true);
}
/**
* Include Attachments Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeAttachments(Model $model)
{
$user = auth()->user();
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

@@ -0,0 +1,148 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
class MediaConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = \App\Models\Media::class;
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* The included fields
*
* @var string[]
*/
protected $includes = ['user'];
/**
* The default filters to use in a request.
*
* @var array
*/
protected $defaultFilters = [
'status' => 'OK'
];
/**
* Return an array of model fields visible to the current user.
*
* @param Model $model The model in question.
* @return array The array of field names.
*/
public function fields(Model $model): array
{
$fields = parent::fields($model);
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/media') === false) {
$fields = arrayRemoveItem($fields, ['permission', 'storage']);
}
return $fields;
}
/**
* 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) {
$builder->where('permission', '');
} else {
$builder->where('permission', '')->orWhereIn('permission', $user->permissions);
}
}
/**
* Return if the current model is visible.
*
* @param Model $model The model.
* @return boolean Allow model to be visible.
*/
public static function viewable(Model $model): bool
{
if ($model->permission !== '') {
$user = auth()->user();
if ($user === null || $user->hasPermission($model->permission) === 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);
}
/**
* 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 && (strcasecmp($model->user_id, $user->id) === 0 || $user->hasPermission('admin/media') === true));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model): bool
{
$user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
}
/**
* Transform the final model data
*
* @param array $data The model data to transform.
* @return array The transformed model.
*/
public function transformFinal(array $data): array
{
unset($data['user_id']);
return $data;
}
/**
* Include User Field.
*
* @param Model $model Them model.
* @return mixed The model result.
*/
public function includeUser(Model $model)
{
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
}
}

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

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

View File

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

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

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

View File

@@ -6,6 +6,13 @@ use ReflectionClass;
class Enum
{
/**
* Message list
*
* @var array<string<static>>
*/
public static $messages = [];
/**
* Caches reflections of enum subclasses.
*
@@ -47,4 +54,18 @@ class Enum
{
return array_values(static::getReflection()->getConstants());
}
/**
* Returns a message from the enum subclass
*
* @return string
*/
public static function getMessage(int $messageIndex, string $defaultMessage = 'Unknown'): string
{
if (array_key_exists($messageIndex, self::$messages) === true) {
return self::$messages[$messageIndex];
}
return $defaultMessage;
}
}

View File

@@ -12,24 +12,6 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
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.
*
@@ -44,10 +26,8 @@ class Handler extends ExceptionHandler
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
public function register(): void
{
// $this->renderable(function (HttpException $e, $request) {
// if ($request->is('api/*')) {

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Filters;
use Illuminate\Support\Collection;
class AuditFilter
{
// public static function filter(Collection $collection): array
// {
// $collection->transform(function ($item, $key) {
// $row = $item->toArray();
// unset($row['user_type']);
// unset($row['auditable_type']);
// if (array_key_exists('password', $row['old_values'])) {
// $row['old_values']['password'] = '###';
// }
// if (array_key_exists('password', $row['new_values'])) {
// $row['new_values']['password'] = '###';
// }
// return $row;
// });
// return $collection->toArray();
// }
}

View File

@@ -1,65 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Event;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\Builder;
class EventFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Event';
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = '-start_at';
/**
* Filter columns for q param
*
* @var string|array
*/
protected $q = [
'_' => ['title','content'],
'location' => ['location','address'],
];
/**
* Determine if the user can view the media model
*
* @param Event $event The event instance.
* @param mixed $user The current logged in user.
* @return boolean
*/
protected function viewable(Event $event, mixed $user)
{
return (strcasecmp($event->status, 'draft') !== 0 && $event->publish_at <= now())
|| $user?->hasPermission('admin/events') === true;
}
/**
* Determine the prebuild query to limit results
*
* @param EloquentBuilder $builder The builder instance.
* @param mixed $user The current logged in user.
* @return EloquentBuilder|null
*/
protected function prebuild(Builder $builder, mixed $user)
{
if (
$user?->hasPermission('admin/events') !== true
) {
return $builder
->where('status', '!=', 'draft')
->where('publish_at', '<=', now());
}
}
}

View File

@@ -1,596 +0,0 @@
<?php
namespace App\Filters;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\SchemaException;
use ReflectionClass;
use RuntimeException;
use InvalidArgumentException;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Schema;
abstract class FilterAbstract
{
/**
* The model class to filter
*
* @var mixed
*/
protected $class;
/**
* The filter request
*
* @var \Illuminate\Http\Request
*/
protected $request;
/**
* The models table
*
* @var string
*/
protected $table = '';
/**
* Array of columns that can be filtered by the api
*
* @var array
*/
protected $filterable = null;
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = 'id';
/**
* Default collection result limit
*
* @var integer
*/
protected $defaultLimit = 50;
/**
* Found records from query
* @var integer
*/
protected $foundTotal = 0;
/**
* Maximum collection result limit
*
* @var integer
*/
protected $maxLimit = 100;
/**
* Only return these attributes in the results
* (minus any excludes)
*
* @var array
*/
protected $only = [];
/**
* Exclude these attributes from the results
*
* @var array
*/
protected $exclude = [];
/**
* Filter columns for q param
*
* @var string|array
*/
protected $q = [];
/**
* Filter constructor.
*
* @param \Illuminate\Http\Request $request Request object.
*/
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Only include the specified attributes in the results.
*
* @param string|array $only Only return these attributes.
* @return void
*/
public function only(mixed $only)
{
if (is_array($only) === true) {
$this->only = $only;
} else {
$this->only = [$only];
}
}
/**
* Exclude the specified attributes in the results.
*
* @param string|array $exclude Attributes to exclude.
* @return void
*/
public function exclude(mixed $exclude)
{
if (is_array($exclude) === true) {
$this->exclude = $exclude;
} else {
$this->exclude = [$exclude];
}
}
/**
* Check if the model is viewable by the user
*
* @param mixed $model Model instance.
* @param mixed $user Current user.
* @return boolean
*/
// protected function viewable(mixed $model, mixed $user)
// {
// return true;
// }
/**
* Prepend action to the builder to limit the results
*
* @param Builder $builder Builder instance.
* @param mixed $user Current user.
* @return Builder|null
*/
// protected function prebuild(Builder $builder, mixed $user)
// {
// return $builder;
// }
/**
* Return an array of attributes visible in the results
*
* @param array $attributes Attributes currently visible.
* @param User|null $user Current logged in user or null.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user)
{
return $attributes;
}
/**
* Apply all the requested filters if available.
*
* @param Model $model Model object to filter. If null create query.
* @return Builder|Model
*/
public function filter(Model $model = null)
{
$this->foundTotal = 0;
$builder = $this->class::query();
/* Get the related model */
$classModel = $model;
if ($model === null) {
$classModel = $builder->getModel();
}
/* Get table name */
if ($this->table === '') {
if ($model === null) {
$this->table = $classModel->getTable();
} else {
$this->table = $model->getTable();
}
}
/* Run query prebuilder or viewable */
if ($model === null) {
if (method_exists($this, 'prebuild') === true) {
$prebuilder = $this->prebuild($builder, $this->request->user());
if ($prebuilder instanceof Builder) {
$builder = $prebuilder;
}
}
} else {
if (method_exists($this, 'viewable') === true) {
if ($this->viewable($model, $this->request->user()) === false) {
return null;
}
}
}
/* Get attributes from table or use 'only' */
$attributes = [];
if (is_array($this->only) === true && count($this->only) > 0) {
$attributes = $this->only;
} else {
$attributes = Schema::getColumnListing($this->table);
}
/* Run attribute modifiers*/
$modifiedAttribs = $this->seeAttributes($attributes, $this->request->user());
if (is_array($modifiedAttribs) === true) {
$attributes = $modifiedAttribs;
}
foreach ($attributes as $key => $column) {
$method = 'see' . Str::studly($column) . 'Attribute';
if (
method_exists($this, $method) === true &&
$this->$method($this->request->user()) === false
) {
unset($attributes[$key]);
}
}
if (is_array($this->exclude) === true && count($this->exclude) > 0) {
$attributes = array_diff($attributes, $this->exclude);
}
/* Setup attributes and appends */
// $attributesAppends = array_merge($attributes, $classModel->getAppends());
/* Apply ?fields= request to attributes */
if ($this->request->has('fields') === true) {
$attributes = array_intersect($attributes, explode(',', $this->request->fields));
}
/* Hide remaining attributes in model (if present) and return */
if ($model !== null) {
// TODO: Also show $this->request->fields that are appends
$model->makeHidden(array_diff(Schema::getColumnListing($this->table), $attributes));
return $model;
}
/* Are there attributes left? */
if (count($attributes) === 0) {
$this->foundTotal = 0;
return new Collection();
}
/* apply select! */
$builder->select($attributes);
/* Setup filterables if not present */
if ($this->filterable === null) {
$this->filterable = $attributes;
}
/* Filter values */
$filterRequest = array_filter($this->request->only(array_intersect($attributes, $this->filterable)));
$this->builderArrayFilter($builder, $filterRequest);
if (is_array($this->q) === true && count($this->q) > 0) {
$qQueries = [];
foreach ($this->q as $key => $value) {
if (is_array($value) === true) {
$qKey = $key === '_' ? '' : $key;
foreach ($value as $subvalue) {
$qQueries[$key][$subvalue] = $this->request->get("q" . $qKey);
}
} elseif ($this->request->has("q") === true) {
$qQueries['_'][$value] = $this->request->get("q");
}
}
foreach ($qQueries as $key => $value) {
$builder->where(function ($query) use ($value) {
$this->builderArrayFilter($query, $value, 'or');
});
}
}//end if
/* Apply sorting */
$sortList = $this->defaultSort;
if ($this->request->has('sort') === true) {
$sortList = explode(',', $this->request->sort);
}
/* Transform sort list to array */
if (is_array($sortList) === false) {
if (strlen($sortList) > 0) {
$sortList = [$sortList];
} else {
$sortList = [];
}
}
/* Remove non-viewable attributes from sort list */
if (count($sortList) > 0) {
$sortList = array_filter($sortList, function ($item) use ($attributes) {
$parsedItem = $item;
if (substr($parsedItem, 0, 1) === '-') {
$parsedItem = substr($parsedItem, 1);
}
return in_array($parsedItem, $attributes);
});
}
/* Do we have any sort element left? */
if (count($sortList) > 0) {
foreach ($sortList as $sortAttribute) {
$prefix = substr($sortAttribute, 0, 1);
$direction = 'asc';
if (in_array($prefix, ['-', '+']) === true) {
$sortAttribute = substr($sortAttribute, 1);
if ($prefix === '-') {
$direction = 'desc';
}
}
$builder->orderBy($sortAttribute, $direction);
}//end foreach
}//end if
/* save found count */
$this->foundTotal = $builder->count();
/* Apply result limit */
$limit = $this->defaultLimit;
if ($this->request->has('limit') === true) {
$limit = intval($this->request->limit);
}
if ($limit < 1) {
$limit = 1;
}
if ($limit > $this->maxLimit && $this->maxLimit !== 0) {
$limit = $this->maxLimit;
}
$builder->limit($limit);
/* Apply page offset */
if ($this->request->has('page') === true) {
$page = intval($this->request->page);
if ($page < 1) {
$page = 1;
}
$builder->offset((intval($this->request->page) - 1) * $limit);
}
/* run spot run */
$collection = $builder->get();
return $collection;
}
/**
* Filter content based on the filterRequest
* @param mixed $builder Builder object
* @param array $filterRequest Filter key/value
* @param string $defaultBoolean Default where boolean
* @return void
*/
protected function builderArrayFilter(mixed $builder, array $filterRequest, string $defaultBoolean = 'and')
{
foreach ($filterRequest as $filterAttribute => $filterValue) {
$tags = [];
$boolean = $defaultBoolean;
$matches = preg_split('/(?<!\\\\)"/', $filterValue, -1, PREG_SPLIT_OFFSET_CAPTURE);
foreach ($matches as $idx => $match_info) {
if (($idx % 2) === true) {
if (substr($filterValue, ($match_info[1] - 2), 1) === ',') {
$tags[] = ['operator' => '', 'tag' => stripslashes(trim($match_info[0]))];
} else {
$tags[(count($tags) - 1)]['tag'] .= stripslashes(trim($match_info[0]));
}
} else {
$innerTags = [$match_info[0]];
if (strpos($match_info[0], ',') !== false) {
$innerTags = preg_split('/(?<!\\\\),/', $match_info[0]);
}
foreach ($innerTags as $tag) {
$tag = stripslashes(trim($tag));
if (strlen($tag) > 0) {
$operator = '=';
$single = substr($tag, 0, 1);
$double = substr($tag . ' ', 0, 2); // add empty space incase len $tag < 2
// check for operators at start
if (in_array($double, ['!=', '<>', '><', '>=', '<=', '=>', '=<']) === true) {
if ($double === '<>' || $double === '><') {
$double = '!=';
} elseif ($double === '=>') {
$double = '>=';
} elseif ($double === '=<') {
$double == '>=';
}
$operator = $double;
$tag = substr($tag, 2);
} else {
if (in_array($single, ['=', '!', '>', '<', '~', '%']) === true) {
if ($single === '=') {
$single = '=='; // a single '=' is actually a double '=='
}
$operator = $single;
$tag = substr($tag, 1);
}
}//end if
$tags[] = ['operator' => $operator, 'tag' => $tag];
}//end if
}//end foreach
}//end if
}//end foreach
if (count($tags) > 1) {
$boolean = 'or';
}
foreach ($tags as $tag_data) {
$operator = $tag_data['operator'];
$value = $tag_data['tag'];
$table = $this->table;
$column = $filterAttribute;
if (($dotPos = strpos($filterAttribute, '.')) !== false) {
$table = substr($filterAttribute, 0, $dotPos);
$column = substr($filterAttribute, ($dotPos + 1));
}
$columnType = DB::getSchemaBuilder()->getColumnType($table, $column);
if (
in_array($columnType, ['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint',
'decimal', 'float', 'double', 'real', 'double precision'
]) === true
) {
if (in_array($operator, ['=', '>', '<', '>=', '<=', '%', '!']) === false) {
continue;
}
$columnType = 'numeric';
} elseif (in_array($columnType, ['date', 'time', 'datetime', 'timestamp', 'year']) === true) {
if (in_array($operator, ['=', '>', '<', '>=', '<=', '!']) === false) {
continue;
}
$columnType = 'datetime';
} elseif (
in_array($columnType, ['string', 'char', 'varchar', 'timeblob', 'blob', 'mediumblob',
'longblob', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum'
]) === true
) {
if (in_array($operator, ['=', '==', '!', '!=', '~']) === false) {
continue;
}
$columnType = 'text';
if ($value === "''" || $value === '""') {
$value = '';
} elseif (strcasecmp($value, 'null') !== 0) {
if ($operator === '!') {
$operator = 'NOT LIKE';
$value = '%' . $value . '%';
} elseif ($operator === '=') {
$operator = 'LIKE';
$value = '%' . $value . '%';
} elseif ($operator === '~') {
$operator = 'SOUNDS LIKE';
} elseif ($operator === '==') {
$operator = '=';
}
}
} elseif ($columnType === 'boolean') {
if (in_array($operator, ['=', '!']) === false) {
continue;
}
if (strtolower($value) === 'true') {
$value = 1;
} elseif (strtolower($value) === 'false') {
$value = 0;
}
}//end if
$betweenSeperator = strpos($value, '<>');
if (
$operator === '=' && $betweenSeperator !== false && in_array($columnType, ['numeric',
'datetime'
]) === true
) {
$value = explode('<>', $value);
$operator = '<>';
}
if ($operator !== '') {
$this->builderWhere($builder, $table, $column, $operator, $value, $boolean);
}
}//end foreach
}//end foreach
}
/**
* Insert a where statement into the builder, taking the filter map into consideration
*
* @param Builder $builder Builder instance.
* @param string $table Table name.
* @param string $column Column name.
* @param string $operator Where operator.
* @param mixed $value Value to test.
* @param string $boolean Use Or comparison.
* @return void
* @throws RuntimeException Error applying statement.
* @throws InvalidArgumentException Error applying statement.
*/
protected function builderWhere(
Builder &$builder,
string $table,
string $column,
string $operator,
mixed $value,
string $boolean
) {
if (
(is_string($value) === true && $operator !== '<>') || (is_array($value) === true && count($value) === 2 &&
$operator === '<>')
) {
if ($table !== '' && $table !== $this->table) {
$builder->whereHas($table, function ($query) use ($column, $operator, $value, $boolean) {
if ($operator !== '<>') {
if (strcasecmp($value, 'null') === 0) {
if ($operator === '!') {
$query->whereNotNull($column, $boolean);
} else {
$query->whereNull($column, $boolean);
}
} else {
$query->where($column, $operator, $value, $boolean);
}
} else {
$query->whereBetween($column, $value, $boolean);
}
});
} else {
if ($operator !== '<>') {
if (strcasecmp($value, 'null') === 0) {
if ($operator === '!') {
$builder->whereNotNull($column, $boolean);
} else {
$builder->whereNull($column, $boolean);
}
} else {
$builder->where($column, $operator, $value, $boolean);
}
} else {
$builder->whereBetween($column, $value, $boolean);
}
}//end if
}//end if
}
/**
* Return the found total of items
* @return integer
*/
public function foundTotal()
{
return $this->foundTotal;
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Media;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
class MediaFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Media';
/**
* Determine if the user can view the media model
*
* @param Media $media The media instance.
* @param mixed $user The current logged in user.
* @return boolean
*/
protected function viewable(Media $media, mixed $user)
{
if (empty($media->permission) === false) {
return ($user?->hasPermission('admin/media') || $user?->hasPermission($media->permission));
}
return true;
}
/**
* Determine the prebuild query to limit results
*
* @param EloquentBuilder $builder The builder instance.
* @param mixed $user The current logged in user.
* @return EloquentBuilder|null
*/
protected function prebuild(Builder $builder, mixed $user)
{
if ($user === null) {
return $builder->whereNull('permission');
}
}
/**
* Show the permission attribute in the results
*
* @param User|null $user Current logged in user or null.
* @return boolean
*/
protected function seePermissionAttribute(mixed $user)
{
return ($user?->hasPermission('admin/media'));
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Post;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\Builder;
class PostFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Post';
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = '-publish_at';
/**
* Determine if the user can view the media model
*
* @param Post $post The post instance.
* @param mixed $user The current logged in user.
* @return boolean
*/
protected function viewable(Post $post, mixed $user)
{
if ($user?->hasPermission('admin/posts') !== true) {
return ($post->publish_at <= now());
}
return true;
}
/**
* Determine the prebuild query to limit results
*
* @param EloquentBuilder $builder The builder instance.
* @param mixed $user The current logged in user.
* @return EloquentBuilder|null
*/
protected function prebuild(Builder $builder, mixed $user)
{
if ($user?->hasPermission('admin/posts') !== true) {
return $builder->where('publish_at', '<=', Carbon::now());
}
}
}

View File

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

View File

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

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

@@ -0,0 +1,40 @@
<?php
/* Array Helper Functions */
/**
* Remove an item from an array.
*
* @param array $arr The array to check.
* @param string|array $item The item or items to remove.
* @return array The filtered array.
*/
function arrayRemoveItem(array $arr, string|array $item): array
{
$filteredArr = $arr;
if (is_string($item) === true) {
$item = [$item];
}
foreach ($item as $str) {
$filteredArr = array_filter($arr, function ($item) use ($str) {
return $item !== $str;
});
}
return $filteredArr;
}
/**
* Return an array with specified the keys
*
* @param array $arr The array to filter.
* @param string|array $keys The keys to keep.
* @return array The filtered array.
*/
function arrayLimitKeys(array $arr, array $keys): array
{
return array_intersect_key($arr, array_flip($keys));
}

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;
use Illuminate\Http\JsonResponse;
use App\Enum\HttpResponseCodes;
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model;
@@ -23,9 +24,8 @@ class ApiController extends Controller
* @param array $data Response data.
* @param integer $respondCode Response status code.
* @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);
}
@@ -34,9 +34,8 @@ class ApiController extends Controller
* Return forbidden 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);
}
@@ -45,9 +44,8 @@ class ApiController extends Controller
* Return forbidden 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);
}
@@ -56,39 +54,43 @@ class ApiController extends Controller
* Return too large 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 no content
* @return \Illuminate\Http\JsonResponse
*/
public function respondNoContent()
public function respondNoContent(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT);
}
/**
* Return created
* @return \Illuminate\Http\JsonResponse
*/
public function respondCreated()
public function respondCreated(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_CREATED);
}
/**
* Return accepted
*/
public function respondAccepted(): JsonResponse
{
return response()->json([], HttpResponseCodes::HTTP_ACCEPTED);
}
/**
* Return single error message
*
* @param string $message Error message.
* @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([
'message' => $message
@@ -100,9 +102,8 @@ class ApiController extends Controller
*
* @param array $errors Error messages.
* @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);
$error = $errors[$keys[0]];
@@ -121,31 +122,41 @@ class ApiController extends Controller
/**
* Return resource data
*
* @param array|Model|Collection $data Resource data.
* @param array|null $appendData Data to append to response.
* @param integer $respondCode Resource code.
* @return \Illuminate\Http\JsonResponse
* @param array|Model|Collection $data Resource data.
* @param array $options Respond options.
*/
protected function respondAsResource(
mixed $data,
mixed $appendData = null,
int $respondCode = HttpResponseCodes::HTTP_OK
) {
array $options = [],
$validationFn = null
): JsonResponse {
$isCollection = $options['isCollection'] ?? false;
$appendData = $options['appendData'] ?? null;
$resourceName = $options['resourceName'] ?? null;
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
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();
}
}
$resourceName = $this->resourceName;
if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = $this->resourceName;
}
if ($this->resourceName === '') {
if (is_null($resourceName) === true || empty($resourceName) === true) {
$resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));
$resourceName = strtolower($resourceName);
}
$is_multiple = true;
$dataArray = [];
if ($data instanceof Collection) {
$dataArray = $data->toArray();
@@ -157,7 +168,7 @@ class ApiController extends Controller
}
$resource = [];
if ($is_multiple === true) {
if ($isCollection === true) {
$resource = [Str::plural($resourceName) => $dataArray];
} else {
$resource = [Str::singular($resourceName) => $dataArray];

View File

@@ -0,0 +1,245 @@
<?php
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\ArticleRequest;
use App\Models\Media;
use App\Models\Article;
use 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 ArticleController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'store',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource(
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Article $article)
{
if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response
*/
public function store(ArticleRequest $request)
{
if (ArticleConductor::creatable() === true) {
$article = Article::create($request->all());
return $this->respondAsResource(
ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function update(ArticleRequest $request, Article $article)
{
if (ArticleConductor::updatable($article) === true) {
$article->update($request->all());
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response
*/
public function destroy(Article $article)
{
if (ArticleConductor::destroyable($article) === true) {
$article->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
/**
* Get a list of attachments related to this model.
*
* @param Request $request The user request.
* @param Article $article The article model.
* @return JsonResponse Returns the article attachments.
* @throws InvalidFormatException
* @throws BindingResolutionException
* @throws InvalidCastException
*/
public function getAttachments(Request $request, Article $article): JsonResponse
{
if (ArticleConductor::viewable($article) === true) {
$medium = $article->attachments->map(function ($attachment) {
return $attachment->media;
});
return $this->respondAsResource(MediaConductor::collection($request, $medium), ['isCollection' => true, 'resourceName' => 'attachment']);
}
return $this->respondForbidden();
}
/**
* Store an attachment related to this model.
*
* @param Request $request The user request.
* @param Article $article The article model.
* @return JsonResponse The response.
* @throws BindingResolutionException
* @throws MassAssignmentException
*/
public function storeAttachment(Request $request, Article $article): JsonResponse
{
if (ArticleConductor::updatable($article) === true) {
if ($request->has("medium") && Media::find($request->medium)) {
$article->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated();
}
return $this->respondWithErrors(['media' => 'The media ID was not found']);
}
return $this->respondForbidden();
}
/**
* Update/replace attachments related to this model.
*
* @param Request $request The user request.
* @param Article $article The related model.
* @throws BindingResolutionException
* @throws MassAssignmentException
*/
public function updateAttachments(Request $request, Article $article): JsonResponse
{
if (ArticleConductor::updatable($article) === true) {
$mediaIds = $request->attachments;
if (is_array($mediaIds) === false) {
$mediaIds = explode(',', $request->attachments);
}
$mediaIds = array_map('trim', $mediaIds); // trim each media ID
$attachments = $article->attachments;
// Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) {
if (!in_array($attachment->media_id, $mediaIds)) {
$attachment->delete();
}
}
// Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) {
$found = false;
foreach ($attachments as $attachment) {
if ($attachment->media_id == $mediaId) {
$found = true;
break;
}
}
if (!$found) {
$article->attachments()->create(['media_id' => $mediaId]);
}
}
return $this->respondNoContent();
}//end if
return $this->respondForbidden();
}
/**
* Delete a specific related attachment.
* @param Request $request The user request.
* @param Article $article The model.
* @param Media $medium The attachment medium.
* @throws BindingResolutionException
*/
public function deleteAttachment(Request $request, Article $article, Media $medium): JsonResponse
{
if (ArticleConductor::updatable($article) === true) {
$attachments = $article->attachments;
$deleted = false;
foreach ($attachments as $attachment) {
if ($attachment->media_id === $medium->id) {
$attachment->delete();
$deleted = true;
break;
}
}
if ($deleted) {
// Attachment was deleted successfully
return $this->respondNoContent();
} else {
// Attachment with matching media ID was not found
return $this->respondNotFound();
}
}
return $this->respondForbidden();
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Attachment;
use Illuminate\Http\Request;
class AttachmentController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->except(['store', 'destroyByEmail']);
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response
*/
public function show(Attachment $attachment)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response
*/
public function edit(Attachment $attachment)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Attachment $attachment)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Attachment $attachment
* @return \Illuminate\Http\Response
*/
public function destroy(Attachment $attachment)
{
//
}
}

View File

@@ -31,9 +31,8 @@ class AuthController extends ApiController
* Current User details
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function me(Request $request)
public function me(Request $request): JsonResponse
{
$user = $request->user()->makeVisible(['permissions']);
return $this->respondAsResource($user);
@@ -47,18 +46,18 @@ class AuthController extends ApiController
*/
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) {
return $this->respondWithErrors([
'username' => 'Email address has not been verified.'
'email' => 'Email address has not been verified.'
]);
}
if ($user->disabled === true) {
return $this->respondWithErrors([
'username' => 'Account has been disabled.'
'email' => 'Account has been disabled.'
]);
}
@@ -73,13 +72,13 @@ class AuthController extends ApiController
return $this->respondAsResource(
$user->makeVisible(['permissions']),
['token' => $token]
['appendData' => ['token' => $token]]
);
}//end if
return $this->respondWithErrors([
'username' => 'Invalid username or password',
'password' => 'Invalid username or password',
'email' => 'Invalid email or password',
'password' => 'Invalid email or password',
]);
}
@@ -87,9 +86,8 @@ class AuthController extends ApiController
* Logout current user
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function logout(Request $request)
public function logout(Request $request): JsonResponse
{
$user = $request->user();

View File

@@ -3,9 +3,13 @@
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Filters\EventFilter;
use App\Http\Requests\EventRequest;
use App\Models\Event;
use App\Conductors\EventConductor;
use App\Conductors\MediaConductor;
use App\Conductors\UserConductor;
use App\Http\Requests\EventRequest;
use App\Models\Media;
use App\Models\User;
use Illuminate\Http\Request;
class EventController extends ApiController
@@ -16,62 +20,77 @@ class EventController extends ApiController
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
->only(['store','update','destroy', 'userAdd', 'userUpdate', 'userDelete']);
}
/**
* Display a listing of the resource.
*
* @param EventFilter $filter The event filter.
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(EventFilter $filter)
public function index(Request $request)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
list($collection, $total) = EventConductor::request($request);
/**
* Store a newly created resource in storage.
*
* @param EventRequest $request The event store request.
* @return \Illuminate\Http\Response
*/
public function store(EventRequest $request)
{
$event = Event::create($request->all());
return $this->respondAsResource(
(new EventFilter($request))->filter($event),
null,
HttpResponseCodes::HTTP_CREATED
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified resource.
*
* @param EventFilter $filter The event filter.
* @param \App\Models\Event $event The specified event.
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function show(EventFilter $filter, Event $event)
public function show(Request $request, Event $event)
{
return $this->respondAsResource($filter->filter($event));
if (EventConductor::viewable($event) === true) {
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\EventRequest $request The request.
* @return \Illuminate\Http\Response
*/
public function store(EventRequest $request)
{
if (EventConductor::creatable() === true) {
$event = Event::create($request->all());
return $this->respondAsResource(
EventConductor::model($request, $event),
['respondCode' => HttpResponseCodes::HTTP_CREATED]
);
} else {
return $this->respondForbidden();
}
}
/**
* Update the specified resource in storage.
*
* @param EventRequest $request The event update request.
* @param \App\Models\Event $event The specified event.
* @param \App\Http\Requests\EventRequest $request The endpoint request.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function update(EventRequest $request, Event $event)
{
$event->update($request->all());
return $this->respondAsResource((new EventFilter($request))->filter($event));
if (EventConductor::updatable($event) === true) {
$event->update($request->all());
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
@@ -82,7 +101,209 @@ class EventController extends ApiController
*/
public function destroy(Event $event)
{
$event->delete();
return $this->respondNoContent();
if (EventConductor::destroyable($event) === true) {
$event->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
/**
* Get a list of attachments related to this model.
*
* @param Request $request The user request.
* @param Event $event The event model.
* @return JsonResponse Returns the event attachments.
*/
public function getAttachments(Request $request, Event $event): JsonResponse
{
if (EventConductor::viewable($event) === true) {
$medium = $event->attachments->map(function ($attachment) {
return $attachment->media;
});
return $this->respondAsResource(MediaConductor::collection($request, $medium), ['isCollection' => true, 'resourceName' => 'attachment']);
}
return $this->respondForbidden();
}
/**
* Store an attachment related to this model.
*
* @param Request $request The user request.
* @param Event $event The event model.
* @return JsonResponse The response.
*/
public function storeAttachment(Request $request, Event $event): JsonResponse
{
if (EventConductor::updatable($event) === true) {
if ($request->has("medium") === true && Media::find($request->medium) !== null) {
$event->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated();
}
return $this->respondWithErrors(['media' => 'The media ID was not found']);
}
return $this->respondForbidden();
}
/**
* Update/replace attachments related to this model.
*
* @param Request $request The user request.
* @param Event $event The related model.
*/
public function updateAttachments(Request $request, Event $event): JsonResponse
{
if (EventConductor::updatable($event) === true) {
$mediaIds = $request->attachments;
if (is_array($mediaIds) === false) {
$mediaIds = explode(',', $request->attachments);
}
$mediaIds = array_map('trim', $mediaIds); // trim each media ID
$attachments = $event->attachments;
// Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) {
if (in_array($attachment->media_id, $mediaIds) === false) {
$attachment->delete();
}
}
// Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) {
$found = false;
foreach ($attachments as $attachment) {
if ($attachment->media_id === $mediaId) {
$found = true;
break;
}
}
if ($found === false) {
$event->attachments()->create(['media_id' => $mediaId]);
}
}
return $this->respondNoContent();
}//end if
return $this->respondForbidden();
}
/**
* Delete a specific related attachment.
*
* @param Request $request The user request.
* @param Event $event The model.
* @param Media $medium The attachment medium.
*/
public function deleteAttachment(Request $request, Event $event, Media $medium): JsonResponse
{
if (EventConductor::updatable($event) === true) {
$attachments = $event->attachments;
$deleted = false;
foreach ($attachments as $attachment) {
if ($attachment->media_id === $medium->id) {
$attachment->delete();
$deleted = true;
break;
}
}
if ($deleted === true) {
// Attachment was deleted successfully
return $this->respondNoContent();
} else {
// Attachment with matching media ID was not found
return $this->respondNotFound();
}
}
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

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

View File

@@ -2,14 +2,13 @@
namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor;
use App\Enum\HttpResponseCodes;
use App\Filters\MediaFilter;
use App\Http\Requests\MediaStoreRequest;
use App\Http\Requests\MediaUpdateRequest;
use App\Http\Requests\MediaRequest;
use App\Models\Media;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
class MediaController extends ApiController
@@ -26,151 +25,158 @@ class MediaController extends ApiController
/**
* Display a listing of the resource.
*
* @param \App\Filters\MediaFilter $filter Created filter object.
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(MediaFilter $filter)
public function index(Request $request)
{
list($collection, $total) = MediaConductor::request($request);
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
$collection,
['isCollection' => true,
'appendData' => ['total' => $total]
],
function ($options) {
return $options['total'] === 0;
}
);
}
/**
* Display the specified resource.
*
* @param MediaFilter $filter The request filter.
* @param Media $medium The request media.
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $medium The request media.
* @return \Illuminate\Http\Response
*/
public function show(MediaFilter $filter, Media $medium)
public function show(Request $request, Media $medium)
{
return $this->respondAsResource($filter->filter($medium));
if (MediaConductor::viewable($medium) === true) {
return $this->respondAsResource(MediaConductor::model($request, $medium));
}
return $this->respondForbidden();
}
/**
* Store a new media resource
*
* @param MediaStoreRequest $request The uploaded media.
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
* @return \Illuminate\Http\Response
*/
public function store(MediaStoreRequest $request)
public function store(MediaRequest $request)
{
$file = $request->file('file');
if ($file === null) {
return $this->respondError(['file' => 'An error occurred uploading the file to the server.']);
}
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondError(['file' => 'The file upload was interrupted.']);
default:
return $this->respondError(['file' => 'An error occurred uploading the file to the server.']);
if (MediaConductor::creatable() === true) {
$file = $request->file('file');
if ($file === null) {
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
}
}
if ($file->getSize() > Media::maxUploadSize()) {
return $this->respondTooLarge();
}
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors(['file' => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(['file' => 'An error occurred uploading the file to the server.']);
}
}
$title = $file->getClientOriginalName();
$mime = $file->getMimeType();
$fileInfo = Media::store($file, empty($request->input('permission')));
if ($fileInfo === null) {
return $this->respondError(
['file' => 'The file could not be stored on the server'],
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
try {
$media = Media::createFromUploadedFile($request, $file);
} catch (\Exception $e) {
if ($e->getCode() === Media::FILE_SIZE_EXCEEDED_ERROR) {
return $this->respondTooLarge();
} else {
return $this->respondWithErrors(['file' => $e->getMessage()]);
}
}
return $this->respondAsResource(
MediaConductor::model($request, $media),
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}
}//end if
$request->merge([
'title' => $title,
'mime' => $mime,
'name' => $fileInfo['name'],
'size' => filesize($fileInfo['path'])
]);
$media = $request->user()->media()->create($request->all());
return $this->respondAsResource((new MediaFilter($request))->filter($media));
return $this->respondForbidden();
}
/**
* Update the media resource in storage.
*
* @param MediaUpdateRequest $request The update request.
* @param \App\Models\Media $medium The specified media.
* @param \App\Http\Requests\MediaRequest $request The update request.
* @param \App\Models\Media $medium The specified media.
* @return \Illuminate\Http\Response
*/
public function update(MediaUpdateRequest $request, Media $medium)
public function update(MediaRequest $request, Media $medium)
{
if ((new MediaFilter($request))->filter($medium) === null) {
return $this->respondNotFound();
}
if (MediaConductor::updatable($medium) === true) {
$file = $request->file('file');
if ($file !== null) {
if ($file->isValid() !== true) {
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $this->respondTooLarge();
case UPLOAD_ERR_PARTIAL:
return $this->respondWithErrors(['file' => 'The file upload was interrupted.']);
default:
return $this->respondWithErrors(['file' => 'An error occurred uploading the file to the server.']);
}
}
$file = $request->file('file');
if ($file !== null) {
if ($file->getSize() > Media::maxUploadSize()) {
return $this->respondTooLarge();
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
}
$oldPath = $medium->path();
$fileInfo = Media::store($file, empty($request->input('permission')));
if ($fileInfo === null) {
return $this->respondError(
['file' => 'The file could not be stored on the server'],
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
);
$medium->update($request->all());
if ($file !== null) {
try {
$medium->updateWithUploadedFile($file);
} catch (\Exception $e) {
return $this->respondWithErrors(
['file' => $e->getMessage()],
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'])
]);
return $this->respondAsResource(MediaConductor::model($request, $medium));
}//end if
$medium->update($request->all());
return $this->respondWithTransformer($file);
return $this->respondForbidden();
}
/**
* Remove the specified resource from storage.
*
* @param Request $request Request instance.
* @param \App\Models\Media $medium Specified media file.
* @param \App\Models\Media $medium Specified media file.
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, Media $medium)
public function destroy(Media $medium)
{
if ((new MediaFilter($request))->filter($medium) !== null) {
if (file_exists($medium->path()) === true) {
unlink($medium->path());
}
if (MediaConductor::destroyable($medium) === true) {
$medium->delete();
return $this->respondNoContent();
}
return $this->respondNotFound();
return $this->respondForbidden();
}
/**
* Display the specified resource.
*
* @param Request $request Request instance.
* @param \App\Models\Media $medium Specified media.
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Media $medium Specified media.
* @return \Illuminate\Http\Response
*/
public function download(Request $request, Media $medium)
@@ -218,6 +224,7 @@ class MediaController extends ApiController
$headerExpires = $updated_at->addMonth()->toRfc2822String();
}//end if
// deepcode ignore InsecureHash: Browsers expect Etag to be a md5 hash
$headerEtag = md5($updated_at->format('U'));
$headerLastModified = $updated_at->toRfc2822String();

View File

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

View File

@@ -1,93 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Filters\PostFilter;
use App\Http\Requests\PostStoreRequest;
use App\Http\Requests\PostUpdateRequest;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'store',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\PostFilter $filter Post filter request.
* @return \Illuminate\Http\Response
*/
public function index(PostFilter $filter)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
/**
* Display the specified resource.
*
* @param PostFilter $filter The filter request.
* @param \App\Models\Post $post The post model.
* @return \Illuminate\Http\Response
*/
public function show(PostFilter $filter, Post $post)
{
return $this->respondAsResource($filter->filter($post));
}
/**
* Store a newly created resource in storage.
*
* @param PostStoreRequest $request The post store request.
* @return \Illuminate\Http\Response
*/
public function store(PostStoreRequest $request)
{
$post = Post::create($request->all());
return $this->respondAsResource(
(new PostFilter($request))->filter($post),
null,
HttpResponseCodes::HTTP_CREATED
);
}
/**
* Update the specified resource in storage.
*
* @param PostUpdateRequest $request The post update request.
* @param \App\Models\Post $post The specified post.
* @return \Illuminate\Http\Response
*/
public function update(PostUpdateRequest $request, Post $post)
{
$post->update($request->all());
return $this->respondAsResource((new PostFilter($request))->filter($post));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Post $post The specified post.
* @return \Illuminate\Http\Response
*/
public function destroy(Post $post)
{
$post->delete();
return $this->respondNoContent();
}
}

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

@@ -2,12 +2,14 @@
namespace App\Http\Controllers\Api;
use App\Conductors\SubscriptionConductor;
use App\Enum\HttpResponseCodes;
use App\Models\Subscription;
use App\Filters\SubscriptionFilter;
use App\Http\Requests\SubscriptionRequest;
use App\Jobs\SendEmailJob;
use App\Mail\SubscriptionConfirm;
use App\Mail\SubscriptionUnsubscribed;
use Illuminate\Http\Request;
class SubscriptionController extends ApiController
{
@@ -23,58 +25,72 @@ class SubscriptionController extends ApiController
/**
* Display a listing of subscribers.
*
* @param \App\Filters\SubscriptionFilter $filter Filter object.
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(SubscriptionFilter $filter)
public function index(Request $request)
{
$collection = $filter->filter();
list($collection, $total) = SubscriptionConductor::request($request);
return $this->respondAsResource(
$collection,
['total' => $filter->foundTotal()]
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Display the specified user.
*
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Subscription $subscription The subscription model.
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Subscription $subscription)
{
if (SubscriptionConductor::viewable($subscription) === true) {
return $this->respondAsResource(SubscriptionConductor::model($request, $subscription));
}
return $this->respondForbidden();
}
/**
* Store a subscriber email in the database.
*
* @param SubscriptionRequest $request The subscriber update request.
* @param \App\Http\Requests\SubscriptionRequest $request The subscriber update request.
* @return \Illuminate\Http\Response
*/
public function store(SubscriptionRequest $request)
{
if (Subscription::where('email', $request->email)->first() !== null) {
return $this->respondWithErrors(['email' => 'This email address has already subscribed']);
if (SubscriptionConductor::creatable() === true) {
Subscription::create($request->all());
dispatch((new SendEmailJob($request->email, new SubscriptionConfirm($request->email))))->onQueue('mail');
return $this->respondCreated();
} else {
return $this->respondForbidden();
}
Subscription::create($request->all());
dispatch((new SendEmailJob($request->email, new SubscriptionConfirm($request->email))))->onQueue('mail');
return $this->respondCreated();
}
/**
* Display the specified user.
*
* @param SubscriptionFilter $filter The subscription filter.
* @param Subscription $subscription The subscription model.
* @return \Illuminate\Http\Response
*/
public function show(SubscriptionFilter $filter, Subscription $subscription)
{
return $this->respondAsResource($filter->filter($subscription));
}
/**
* Update the specified resource in storage.
*
* @param SubscriptionRequest $request The subscription update request.
* @param Subscription $subscription The specified subscription.
* @param \App\Http\Requests\SubscriptionRequest $request The subscription update request.
* @param \App\Models\Subscription $subscription The specified subscription.
* @return \Illuminate\Http\Response
*/
public function update(SubscriptionRequest $request, Subscription $subscription)
{
// if (EventConductor::updatable($event) === true) {
// $event->update($request->all());
// return $this->respondAsResource(EventConductor::model($request, $event));
// }
// return $this->respondForbidden();
// $input = [];
// $updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password'];
@@ -103,14 +119,12 @@ class SubscriptionController extends ApiController
*/
public function destroy(Subscription $subscription)
{
// if ($user->hasPermission('admin/user') === false) {
// return $this->respondForbidden();
// }
$email = $subscription->email;
$subscription->delete();
return $this->respondNoContent();
if (SubscriptionConductor::destroyable($subscription) === true) {
$subscription->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
/**

View File

@@ -2,12 +2,10 @@
namespace App\Http\Controllers\Api;
use App\Conductors\EventConductor;
use App\Enum\HttpResponseCodes;
use App\Filters\UserFilter;
use App\Http\Requests\UserUpdateRequest;
use App\Http\Requests\UserStoreRequest;
use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
@@ -16,13 +14,15 @@ use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Conductors\UserConductor;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
class UserController extends ApiController
{
@@ -38,120 +38,139 @@ class UserController extends ApiController
'register',
'exists',
'forgotPassword',
'forgotUsername',
'resetPassword',
'verifyEmail',
'resendVerifyEmailCode'
'resendVerifyEmailCode',
'eventList',
]);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\UserFilter $filter Filter object.
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(UserFilter $filter)
public function index(Request $request)
{
$collection = $filter->filter();
list($collection, $total) = UserConductor::request($request);
return $this->respondAsResource(
$collection,
['total' => $filter->foundTotal()]
['isCollection' => true,
'appendData' => ['total' => $total]
]
);
}
/**
* Store a newly created user in the database.
*
* @param UserStoreRequest $request The user update request.
* @param \App\Http\Requests\UserRequest $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function store(UserStoreRequest $request)
public function store(UserRequest $request)
{
if ($request->user()->hasPermission('admin/user') !== true) {
if (UserConductor::creatable() === true) {
$user = User::create($request->all());
return $this->respondAsResource(UserConductor::model($request, $user), ['respondCode' => HttpResponseCodes::HTTP_CREATED]);
} else {
return $this->respondForbidden();
}
$user = User::create($request->all());
return $this->respondAsResource((new UserFilter($request))->filter($user), [], HttpResponseCodes::HTTP_CREATED);
}
/**
* Display the specified user.
*
* @param UserFilter $filter The user filter.
* @param User $user The user model.
* @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\User $user The user model.
* @return \Illuminate\Http\Response
*/
public function show(UserFilter $filter, User $user)
public function show(Request $request, User $user)
{
return $this->respondAsResource($filter->filter($user));
if (UserConductor::viewable($user) === true) {
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Update the specified resource in storage.
*
* @param UserUpdateRequest $request The user update request.
* @param User $user The specified user.
* @param \App\Http\Requests\UserRequest $request The user update request.
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function update(UserUpdateRequest $request, User $user)
public function update(UserRequest $request, User $user)
{
$input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password'];
if (UserConductor::updatable($user) === true) {
$input = [];
$updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
} elseif ($request->user()->is($user) !== true) {
return $this->respondForbidden();
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
}
$input = $request->only($updatable);
if (array_key_exists('password', $input) === true) {
$input['password'] = Hash::make($request->input('password'));
}
$user->update($input);
return $this->respondAsResource(UserConductor::model($request, $user));
}
$input = $request->only($updatable);
if (array_key_exists('password', $input) === true) {
$input['password'] = Hash::make($request->input('password'));
}
$user->update($input);
return $this->respondAsResource((new UserFilter($request))->filter($user));
return $this->respondForbidden();
}
/**
* Remove the user from the database.
*
* @param User $user The specified user.
* @param \App\Models\User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
if ($user->hasPermission('admin/user') === false) {
return $this->respondForbidden();
if (UserConductor::destroyable($user) === true) {
$user->delete();
return $this->respondNoContent();
}
$user->delete();
return $this->respondNoContent();
return $this->respondForbidden();
}
/**
* Register a new user
*
* @param UserRegisterRequest $request The register user request.
* @return \Illuminate\Http\Response
* @param \App\Http\Requests\UserRegisterRequest $request The register user request.
*/
public function register(UserRegisterRequest $request)
public function register(UserRegisterRequest $request): JsonResponse
{
try {
$user = User::create([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'username' => $request->input('username'),
'email' => $request->input('email'),
'phone' => $request->input('phone'),
'password' => Hash::make($request->input('password'))
$userData = $request->only([
'first_name',
'last_name',
'email',
'phone',
'password',
'display_name',
]);
$userData['password'] = Hash::make($userData['password']);
$user = User::where('email', $request->input('email'))
->whereNull('password')
->first();
if ($user === null) {
$user = User::create($userData);
} else {
unset($userData['email']);
$user->update($userData);
}//end if
$code = $user->codes()->create([
'action' => 'verify-email',
]);
@@ -168,35 +187,15 @@ class UserController extends ApiController
}//end try
}
/**
* Sends an email with all the usernames registered at that address
*
* @param 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
*
* @param UserForgotPasswordRequest $request The reset password request.
* @param \App\Http\Requests\UserForgotPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function forgotPassword(UserForgotPasswordRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([
@@ -213,7 +212,7 @@ class UserController extends ApiController
/**
* Resets a user password
*
* @param UserResetPasswordRequest $request The reset password request.
* @param \App\Http\Requests\UserResetPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function resetPassword(UserResetPasswordRequest $request)
@@ -240,14 +239,14 @@ class UserController extends ApiController
}
return $this->respondError([
'code' => 'The code was not found or has expired'
'code' => 'The code was not found or has expired.'
]);
}
/**
* Verify an email code
*
* @param UserVerifyEmailRequest $request The verify email request.
* @param \App\Http\Requests\UserVerifyEmailRequest $request The verify email request.
* @return \Illuminate\Http\Response
*/
public function verifyEmail(UserVerifyEmailRequest $request)
@@ -278,21 +277,20 @@ class UserController extends ApiController
}//end if
return $this->respondWithErrors([
'code' => 'The code was not found or has expired'
'code' => 'The code was not found or has expired.'
]);
}
/**
* Resend a new verify email
*
* @param UserResendVerifyEmailRequest $request The resend verify email request.
* @return \Illuminate\Http\Response
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request.
*/
public function resendVerifyEmail(UserResendVerifyEmailRequest $request)
public function resendVerifyEmail(UserResendVerifyEmailRequest $request): JsonResponse
{
UserCode::clearExpired();
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate();
@@ -312,12 +310,12 @@ class UserController extends ApiController
/**
* Resend verification email
*
* @param UserResendVerifyEmailRequest $request The resend user request.
* @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend user request.
* @return \Illuminate\Http\Response
*/
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete();
@@ -334,4 +332,28 @@ class UserController extends ApiController
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;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
}

View File

@@ -40,7 +40,7 @@ class Kernel extends HttpKernel
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\ForceJsonResponse::class,
'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>
*/
protected $routeMiddleware = [
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::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.
*
* @param mixed $request Request.
* @return string|null
*/
protected function redirectTo(mixed $request)
protected function redirectTo(mixed $request): ?string
{
if ($request->expectsJson() === false) {
return route('login');

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use Symfony\Component\HttpFoundation\Response;
use Closure;
use Illuminate\Http\Request;
@@ -10,11 +11,9 @@ class ForceJsonResponse
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @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');
return $next($request);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,15 +9,15 @@ class BaseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
if (method_exists($this, 'postAuthorize') === true && request()->isMethod('post') === true) {
if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) {
return $this->postAuthorize();
} elseif (method_exists($this, 'putAuthorize') === true && request()->isMethod('put') === true) {
} elseif ((request()->isMethod('put') === true || request()->isMethod('patch') === true) && method_exists($this, 'putAuthorize') === true) {
return $this->putAuthorize();
} elseif (request()->isMethod('delete') === true && method_exists($this, 'destroyAuthorize') === true) {
return $this->deleteAuthorize();
}
return true;
@@ -28,7 +28,7 @@ class BaseRequest extends FormRequest
*
* @return array<string, mixed>
*/
public function rules()
public function rules(): array
{
$rules = [];
@@ -38,8 +38,8 @@ class BaseRequest extends FormRequest
if (method_exists($this, 'postRules') === true && request()->isMethod('post') === true) {
$rules = $this->mergeRules($rules, $this->postRules());
} elseif (method_exists($this, 'putRules') === true && request()->isMethod('put') === true) {
$rules = $this->mergeRules($rules, $this->postRules());
} elseif (method_exists($this, 'putRules') === true && (request()->isMethod('put') === true || request()->isMethod('patch') === true)) {
$rules = $this->mergeRules($rules, $this->putRules());
} elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) {
$rules = $this->mergeRules($rules, $this->destroyRules());
}
@@ -52,9 +52,8 @@ class BaseRequest extends FormRequest
*
* @param array $collection1 The first collection of rules.
* @param array $collection2 The second collection of rules to merge.
* @return array
*/
private function mergeRules(array $collection1, array $collection2)
private function mergeRules(array $collection1, array $collection2): array
{
$rules = [];
@@ -73,8 +72,8 @@ class BaseRequest extends FormRequest
if (is_array($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, $collection2[$key]);
} elseif (is_string($collection1[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, explode('|', $collection1[$key]));
} elseif (is_string($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, explode('|', $collection2[$key]));
}
if (count($key_ruleset) > 0) {

View File

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

View File

@@ -2,37 +2,16 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class EventRequest extends BaseRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function postAuthorize()
{
return $this->user()?->hasPermission('admin/events');
}
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function putAuthorize()
{
return $this->user()?->hasPermission('admin/events');
}
/**
* Apply the base rules to this request
*
* @return array<string, mixed>
*/
public function baseRules()
public function baseRules(): array
{
return [
'title' => 'min:6',
@@ -44,14 +23,15 @@ class EventRequest extends BaseRequest
'end_at' => 'date|after:start_date',
'publish_at' => 'date|nullable',
'status' => [
Rule::in(['draft', 'open', 'closed', 'cancelled']),
Rule::in(['draft', 'soon', 'open', 'closed', 'cancelled']),
],
'registration_type' => [
Rule::in(['none', 'email', 'link']),
Rule::in(['none', 'email', 'link', 'message']),
],
'registration_data' => [
Rule::when(strcasecmp('email', $this->attributes->get('registration_type')) == 0, 'required|email'),
Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url')
Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url'),
Rule::when(strcasecmp('message', $this->attributes->get('registration_type')) == 0, 'required|message'),
],
'hero' => 'uuid|exists:media,id',
];
@@ -62,7 +42,7 @@ class EventRequest extends BaseRequest
*
* @return array<string, mixed>
*/
protected function postRules()
protected function postRules(): array
{
return [
'title' => 'required',

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class MediaRequest extends BaseRequest
{
/* empty */
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MediaStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
//
];
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MediaUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
//
];
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PostStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'slug' => 'string|min:6|unique:posts',
'title' => 'string|min:6|max:255',
'publish_at' => 'date',
'user_id' => 'uuid|exists:users,id',
];
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PostUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'slug' => [
'string',
'min:6',
Rule::unique('posts')->ignoreModel($this->post),
],
'title' => 'string|min:6|max:255',
'publish_at' => 'date',
'user_id' => 'uuid|exists:users,id',
];
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'required|string|max:255|min:4|unique:users',
'first_name' => 'required|string|max:255|min:2',
'last_name' => 'required|string|max:255|min:2',
'email' => 'required|string|email|max:255',
'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date'
];
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'string|max:255|min:6|unique:users',
'first_name' => 'string|max:255|min:2',
'last_name' => 'string|max:255|min:2',
'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 UserVerifyEmailRequest extends FormRequest
*
* @return array<string, mixed>
*/
public function rules()
public function rules(): array
{
return [
'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.
*
* @return void
*/
public function handle()
public function handle(): void
{
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.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
public function envelope(): Envelope
{
return new Envelope(
subject: '👋🏻 Lets change your email!',
@@ -66,10 +64,8 @@ class ChangeEmailVerify extends Mailable
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
public function content(): Content
{
return new Content(
view: 'emails.user.change_email_verify',

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,10 +45,8 @@ class ForgotPassword extends Mailable
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
public function envelope(): Envelope
{
return new Envelope(
subject: '🤦 Forgot your password?',
@@ -57,10 +55,8 @@ class ForgotPassword extends Mailable
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
public function content(): Content
{
return new Content(
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.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
public function envelope(): Envelope
{
return new Envelope(
subject: '🗞️ You\'re on the mailing list!',
@@ -48,10 +46,8 @@ class SubscriptionConfirm extends Mailable
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
public function content(): Content
{
return new Content(
view: 'emails.user.subscription_confirm',

View File

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

View File

@@ -15,4 +15,30 @@ class Analytics extends Model
* @var array
*/
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 Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
class Article extends Model
{
use HasFactory;
use Uuids;
@@ -27,12 +28,18 @@ class Post extends Model
/**
* Get the file user
*
* @return BelongsTo
* Get the article user
*/
public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get all of the article's attachments.
*/
public function attachments(): MorphMany
{
return $this->morphMany(\App\Models\Attachment::class, 'attachable');
}
}

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

@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Attachment extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'media_id',
'private',
];
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'private' => 'false',
];
/**
* Get attachments attachable
*/
public function attachable(): MorphTo
{
return $this->morphTo();
}
/**
* Get the media for this attachment.
*/
public function media(): BelongsTo
{
return $this->belongsTo(Media::class);
}
}

View File

@@ -5,6 +5,8 @@ namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Event extends Model
{
@@ -19,6 +21,7 @@ class Event extends Model
protected $fillable = [
'title',
'location',
'location_url',
'address',
'start_at',
'end_at',
@@ -27,6 +30,25 @@ class Event extends Model
'registration_type',
'registration_data',
'hero',
'content'
'content',
'price',
'ages',
];
/**
* Get all of the article's attachments.
*/
public function attachments(): MorphMany
{
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,18 +2,37 @@
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\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\Str;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Media extends Model
{
use HasFactory;
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.
@@ -22,20 +41,14 @@ class Media extends Model
*/
protected $fillable = [
'title',
'name',
'mime',
'user_id',
'mime_type',
'permission',
'storage',
'description',
'name',
'size',
'permission'
];
/**
* The attributes that are hidden.
*
* @var array<int, string>
*/
protected $hidden = [
'path',
'status',
];
/**
@@ -47,13 +60,48 @@ class Media extends Model
'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
*
* @return void
*/
protected static function boot()
protected static function boot(): void
{
parent::boot();
@@ -62,124 +110,379 @@ class Media extends Model
$origPermission = $media->getOriginal()['permission'];
$newPermission = $media->permission;
$origPath = Storage::disk(Media::getStorageId(empty($origPermission)))->path($media->name);
$newPath = Storage::disk(Media::getStorageId(empty($newPermission)))->path($media->name);
$newPermissionLen = strlen($newPermission);
if ($origPath !== $newPath) {
if (file_exists($origPath) === true) {
if (file_exists($newPath) === true) {
$fileParts = pathinfo($newPath);
$newName = '';
// 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
if ($newPermissionLen !== strlen($origPermission)) {
if ($newPermissionLen === 0) {
$this->moveToStorage('cdn');
} else {
$this->moveToStorage('private');
}
}
}
});
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 string
*/
public function getUrlAttribute()
public function getUrlAttribute(): string
{
$url = config('filesystems.disks.' . Media::getStorageId($this) . '.url');
if (empty($url) === false) {
$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
if (isset($this->attributes['name']) === true) {
return self::getUrlPath() . $this->name;
}
return '';
}
/**
* Return the file owner
*
* @return BelongsTo
*/
public function user()
public function user(): BelongsTo
{
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.
* @return string
* @param App\Models\Request $request The request data.
* @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) {
$isPublic = empty($mediaOrPublic->permission);
} else {
$isPublic = boolval($mediaOrPublic);
if ($request->get('storage') === null) {
// We store images by default locally
if (strpos($file->getMimeType(), 'image/') === 0) {
$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 boolean $public Is the file available to the public.
* @return array|null
* @param Illuminate\Http\UploadedFile $file The file.
* @return null|Media The media item.
*/
public static function store(UploadedFile $file, bool $public = true)
public function updateWithUploadedFile(UploadedFile $file): ?Media
{
$storage = Media::getStorageId($public);
$name = $file->store('', ['disk' => $storage]);
if ($file === null || $file->isValid() !== true) {
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) {
return null;
throw new \Exception('The file name already exists in storage.', self::FILE_NAME_EXISTS_ERROR);
}
$path = Storage::disk($storage)->path($name);
return [
'name' => $name,
'path' => $path
];
// remove file if there is an existing entry in this medium item
if (strlen($this->name) > 0 && strlen($this->storage) > 0) {
Storage::disk($this->storage)->delete($this->name);
foreach ($this->variants as $variantName => $fileName) {
Storage::disk($this->storage)->delete($fileName);
}
$this->name = '';
$this->variants = [];
}
if (strlen($this->title) === 0) {
$this->title = $name;
}
$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
*
* @return integer
*/
public static function maxUploadSize()
public static function getMaxUploadSize(): int
{
$sizes = [
ini_get('upload_max_filesize'),
@@ -207,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.
* @return string
* @param string $fileName The proposed file name.
* @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
[\x00-\x1F]|
# control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x00-\x1F]|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[\x7F\xA0\xAD]|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[\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]|
@@ -240,37 +634,37 @@ class Media extends Model
[{}^\~`]
~x',
'-',
$filename
$fileName
);
$filename = ltrim($filename, '.-');
$fileName = ltrim($fileName, '.-');
$filename = preg_replace([
// "file name.zip" becomes "file-name.zip"
$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"
'/-+/'
], '-', $filename);
$filename = preg_replace([
// "file--.--.-.--name.zip" becomes "file.name.zip"
], '-', $fileName);
$fileName = preg_replace([
// "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/',
// "file...name..zip" becomes "file.name.zip"
// "file...name..zip" becomes "file.name.zip"
'/\.{2,}/'
], '.', $filename);
], '.', $fileName);
// 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"
$filename = trim($filename, '.-');
$fileName = trim($fileName, '.-');
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$filename = mb_strcut(
pathinfo($filename, PATHINFO_FILENAME),
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
$fileName = mb_strcut(
pathinfo($fileName, PATHINFO_FILENAME),
0,
(255 - ($ext !== '' ? strlen($ext) + 1 : 0)),
mb_detect_encoding($filename)
mb_detect_encoding($fileName)
) . ($ext !== '' ? '.' . $ext : '');
return $filename;
return $fileName;
}
}

View File

@@ -24,10 +24,8 @@ class Permission extends Model
/**
* Get the User associated with this model
*
* @return BelongsTo
*/
public function user()
public function user(): BelongsTo
{
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 Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
@@ -25,12 +26,12 @@ class User extends Authenticatable implements Auditable
* @var array<int, string>
*/
protected $fillable = [
'username',
'first_name',
'last_name',
'email',
'phone',
'password',
'display_name',
];
/**
@@ -66,28 +67,28 @@ class User extends Authenticatable implements Auditable
'permissions'
];
// public function getPermissionsAttribute() {
// return $this->permissions()->pluck('permission')->toArray();
// }
/**
* The default attributes.
*
* @var string[]
*/
protected $attributes = [
'phone' => '',
];
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function permissions()
public function permissions(): HasMany
{
return $this->hasMany(Permission::class);
}
/**
* Get the permission attribute
*
* @return array
*/
public function getPermissionsAttribute()
public function getPermissionsAttribute(): array
{
return $this->permissions()->pluck('permission')->toArray();
}
@@ -96,50 +97,89 @@ class User extends Authenticatable implements Auditable
* Test if user has permission
*
* @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);
}
/**
* Get the list of files of the user
* Give permissions to the user
*
* @return HasMany
* @param string|array $permissions The permission(s) to give.
*/
public function media()
public function givePermission($permissions): Collection
{
if (is_array($permissions) === false) {
$permissions = [$permissions];
}
$permissions = collect($permissions)->map(function ($permission) {
return ['permission' => $permission];
});
$existingPermissions = $this->permissions()->whereIn('permission', $permissions->pluck('permission'))->get();
$newPermissions = $permissions->reject(function ($permission) use ($existingPermissions) {
return $existingPermissions->contains('permission', $permission['permission']);
});
return $this->permissions()->createMany($newPermissions->toArray());
}
/**
* Revoke permissions from the user
*
* @param string|array $permissions The permission(s) to revoke.
*/
public function revokePermission($permissions): int
{
if (is_array($permissions) === false) {
$permissions = [$permissions];
}
return $this->permissions()
->whereIn('permission', $permissions)
->delete();
}
/**
* Get the list of files of the user
*/
public function media(): HasMany
{
return $this->hasMany(Media::class);
}
/**
* 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
*
* @return HasMany
*/
public function codes()
public function codes(): HasMany
{
return $this->hasMany(UserCode::class);
}
/**
* Get the list of logins of the user
*
* @return HasMany
*/
public function logins()
public function logins(): HasMany
{
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.
*
* @return void
*/
protected static function boot()
protected static function boot(): void
{
parent::boot();
static::creating(function ($model) {
@@ -46,10 +44,8 @@ class UserCode extends Model
/**
* Generate new code
*
* @return void
*/
public function regenerate()
public function regenerate(): void
{
while (true) {
$code = random_int(100000, 999999);
@@ -62,20 +58,16 @@ class UserCode extends Model
/**
* Clear expired user codes
*
* @return void
*/
public static function clearExpired()
public static function clearExpired(): void
{
UserCode::where('updated_at', '<=', now()->subDays(5))->delete();
}
/**
* Get associated user
*
* @return BelongsTo
*/
public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

View File

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

View File

@@ -2,30 +2,34 @@
namespace App\Providers;
use App\Rules\RequiredIfAny;
use App\Rules\Uniqueish;
use Illuminate\Support\ServiceProvider;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use PDOException;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
public function register(): void
{
//
}
/**
* 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.
*
* @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.
*
* @return void
*/
public function boot()
public function boot(): void
{
Broadcast::routes();

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