Compare commits

...

165 commits

Author SHA1 Message Date
dependabot[bot] 77a06947db
Bump @tailwindcss/forms from 0.5.6 to 0.5.7 (#65)
Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.5.6 to 0.5.7.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.6...v0.5.7)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-24 13:55:43 -08:00
dependabot[bot] d73d3180b7
Bump tailwindcss from 3.3.3 to 3.3.5 (#63)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.3 to 3.3.5.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.3...v3.3.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 19:11:08 -07:00
dependabot[bot] 1eae77f27c
Bump postcss from 8.4.23 to 8.4.31 (#61)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 20:12:30 -07:00
dependabot[bot] 0a52b31559
Bump antcms/antloader from 2.0.1 to 2.0.2 (#60)
Bumps [antcms/antloader](https://github.com/AntCMS-org/AntLoader) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/AntCMS-org/AntLoader/releases)
- [Commits](https://github.com/AntCMS-org/AntLoader/compare/2.0.1...2.0.2)

---
updated-dependencies:
- dependency-name: antcms/antloader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 20:10:53 -07:00
dependabot[bot] eb19768d6a
Bump @tailwindcss/typography from 0.5.9 to 0.5.10 (#59)
Bumps [@tailwindcss/typography](https://github.com/tailwindcss/typography) from 0.5.9 to 0.5.10.
- [Release notes](https://github.com/tailwindcss/typography/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindcss/typography/compare/v0.5.9...v0.5.10)

---
updated-dependencies:
- dependency-name: "@tailwindcss/typography"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 18:36:44 -07:00
dependabot[bot] a06956f164
Bump actions/checkout from 3 to 4 (#58)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 18:35:21 -07:00
dependabot[bot] ae629ccebd
Bump league/commonmark from 2.4.0 to 2.4.1 (#57)
Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.4/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.4.0...2.4.1)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 18:35:09 -07:00
dependabot[bot] be214fbc94
Bump twig/twig from 3.7.0 to 3.7.1 (#56)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.7.0 to 3.7.1.
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.7.0...v3.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-28 18:48:25 -07:00
dependabot[bot] 8eb49cda0b
Bump @tailwindcss/forms from 0.5.5 to 0.5.6 (#55)
Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.5.5 to 0.5.6.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.5...v0.5.6)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-28 18:21:28 -07:00
dependabot[bot] 50d181373a
Bump @tailwindcss/forms from 0.5.4 to 0.5.5 (#54)
Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.5.4 to 0.5.5.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.4...v0.5.5)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 18:42:48 -07:00
dependabot[bot] 7d02c1a2a1
Bump twig/twig from 3.6.1 to 3.7.0 (#53)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.6.1 to 3.7.0.
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.6.1...v3.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 05:35:08 -07:00
dependabot[bot] 5c0bb8d1af
Bump @tailwindcss/forms from 0.5.3 to 0.5.4 (#52)
Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.5.3 to 0.5.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.3...v0.5.4)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 18:56:35 -07:00
dependabot[bot] 5041f824df
Bump tailwindcss from 3.3.2 to 3.3.3 (#51)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.3.3/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.2...v3.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 18:54:25 -07:00
Belle Aerni 57bb266635 Fix issue with the HTTPS redirect 2023-06-21 15:08:07 -07:00
Belle Aerni c613271c48 Switch to hrtime and display time to render in ms 2023-06-20 05:20:24 -07:00
Belle Aerni 8d9e894af8
~ 50% reduction in the time to render a page (#49) 2023-06-19 16:18:49 -07:00
dependabot[bot] d920b26f52
Bump antcms/antloader from 2.0.0 to 2.0.1 (#47)
Bumps [antcms/antloader](https://github.com/AntCMS-org/AntLoader) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/AntCMS-org/AntLoader/releases)
- [Commits](https://github.com/AntCMS-org/AntLoader/compare/2.0.0...2.0.1)

---
updated-dependencies:
- dependency-name: antcms/antloader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 19:40:17 -07:00
dependabot[bot] ccf05911ef
Bump twig/twig from 3.6.0 to 3.6.1 (#48)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.6.0 to 3.6.1.
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.6.0...v3.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 19:40:06 -07:00
Belle Aerni ebb0fce492
Bump AntLoader to version 2 (#46)
* Bump AntLoader to version 2

* Whoops - I forgot about the tests

* Oh yeah, cron.

Note to self: turn the autoloader into an individual file that can be included by the tests / cron file so I don't need to update 3 of them manually
2023-05-27 02:14:33 -07:00
Belle Aerni 945881365e
Implement per-page template functionality (#45)
Can be enabled by adding "Template: templatename" to a file header.
Then AntCMS will attempt to load the associated template from the  "Templates" directory of your current theme
2023-05-27 00:24:28 -07:00
Belle Aerni 38e51166c3
Implemented APCu caching (#44)
* Start implementing APCu caching

* Fix tests

* Now make PHPStan happy :)

* Fix tests

* Also enable the APCu extension in the tests

* Set maxlife to 7 days & clear with cron

* Implement a more correct way to clear the cache

* Also mention the APCu caching in the readme
2023-05-26 23:46:42 -07:00
Belle Aerni b61ac80227
Started work on routing improvements (#42)
* Started work on routing improvements

Creates a helper class to make handling routes a bit easier. Also should an issue that prevented SSL certs from being renewed via the .well-known folder

* Bugfix

* Added PHPDocs
2023-05-26 05:44:27 -07:00
dependabot[bot] fa87852dc5
Bump embed/embed from 4.4.7 to 4.4.8 (#43)
Bumps [embed/embed](https://github.com/oscarotero/Embed) from 4.4.7 to 4.4.8.
- [Release notes](https://github.com/oscarotero/Embed/releases)
- [Changelog](https://github.com/oscarotero/Embed/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oscarotero/Embed/compare/v4.4.7...v4.4.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 02:52:04 -07:00
Belle Aerni d03ec564f6 Remove AntKeywords, minor formatting changes 2023-05-13 13:08:33 -07:00
dependabot[bot] 95c2774222
Bump antcms/antloader from 1.0.1 to 1.0.2 (#41)
Bumps [antcms/antloader](https://github.com/AntCMS-org/AntLoader) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/AntCMS-org/AntLoader/releases)
- [Commits](https://github.com/AntCMS-org/AntLoader/compare/1.0.1...1.0.2)

---
updated-dependencies:
- dependency-name: antcms/antloader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 19:22:41 -07:00
dependabot[bot] 18e83c915a
Bump antcms/antloader from 1.0.0 to 1.0.1 (#40)
Bumps [antcms/antloader](https://github.com/AntCMS-org/AntLoader) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/AntCMS-org/AntLoader/releases)
- [Commits](https://github.com/AntCMS-org/AntLoader/compare/1.0.0...1.0.1)

---
updated-dependencies:
- dependency-name: antcms/antloader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-07 19:15:14 -07:00
Belle Aerni e01a00d638 Update dependabot.yml 2023-05-04 19:41:13 -07:00
Belle Aerni b31e88a428
Move to using AntLoader (#38)
* Move to using AntLoader

* Update composer lockfile

I fixed the wrong classname in AntLoader

* Update tests

* It works!

* Use AntLoader 1.0.0
2023-05-04 19:36:15 -07:00
dependabot[bot] e6a59c0b16
Bump twig/twig from 3.5.1 to 3.6.0 (#37)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.5.1 to 3.6.0.
- [Release notes](https://github.com/twigphp/Twig/releases)
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.5.1...v3.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-04 07:57:30 -07:00
Belle Aerni a58113087f Fix PHPStan action 2023-05-03 18:37:53 -07:00
dependabot[bot] 3239a19245
Bump tailwindcss from 3.3.1 to 3.3.2 (#35)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.3.2/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.1...v3.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 19:09:08 -07:00
Belle Aerni 480ab7185d Small bugfix 2023-04-25 14:11:58 -07:00
dependabot[bot] 2e737545ad
Bump nyholm/psr7 from 1.6.1 to 1.7.0 (#34)
Bumps [nyholm/psr7](https://github.com/Nyholm/psr7) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/Nyholm/psr7/releases)
- [Changelog](https://github.com/Nyholm/psr7/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Nyholm/psr7/compare/1.6.1...1.7.0)

---
updated-dependencies:
- dependency-name: nyholm/psr7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-21 00:21:35 -07:00
dependabot[bot] 79eef0aa46
Bump nyholm/psr7 from 1.6.0 to 1.6.1 (#33)
Bumps [nyholm/psr7](https://github.com/Nyholm/psr7) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/Nyholm/psr7/releases)
- [Changelog](https://github.com/Nyholm/psr7/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Nyholm/psr7/compare/1.6.0...1.6.1)

---
updated-dependencies:
- dependency-name: nyholm/psr7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-17 19:59:52 -07:00
dependabot[bot] 0cff3b92df
Bump nyholm/psr7 from 1.5.1 to 1.6.0 (#32)
Bumps [nyholm/psr7](https://github.com/Nyholm/psr7) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/Nyholm/psr7/releases)
- [Changelog](https://github.com/Nyholm/psr7/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Nyholm/psr7/compare/1.5.1...1.6.0)

---
updated-dependencies:
- dependency-name: nyholm/psr7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-09 20:00:49 -07:00
Belle Aerni 8e018600a5 Minor, but important improvements 2023-04-01 19:30:12 -07:00
Belle Aerni 0663fbc604 Incorporate TinyZoom.JS 2023-03-30 23:01:08 -07:00
dependabot[bot] c084208db6
Bump tailwindcss from 3.3.0 to 3.3.1 (#31)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.3.1/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.0...v3.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-30 19:10:58 -07:00
Belle Aerni 1770d5376a Minor tweak 2023-03-30 17:02:12 -07:00
dependabot[bot] 226d100673
Bump tailwindcss from 3.2.7 to 3.3.0 (#30)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.2.7 to 3.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.2.7...v3.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-28 19:15:23 -07:00
Belle Aerni 0e252133e2 Cleanly handle no default attributes being set 2023-03-26 23:40:02 -07:00
Belle Aerni 90f0f96f13 Fix those pesky tests 2023-03-26 23:34:17 -07:00
Belle Aerni 70ff0a106f Per-theme config, default attributes, and bump BS ver 2023-03-26 23:29:17 -07:00
dependabot[bot] acb00af6f7
Bump league/commonmark from 2.3.9 to 2.4.0 (#29)
Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.3.9 to 2.4.0.
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.4/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.3.9...2.4.0)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-26 19:29:32 -07:00
Belle Aerni 61fa0fc533 Fix file name 2023-03-21 13:16:53 -07:00
Belle Aerni 23e8b18ba3
Implemented multi-user support (#27)
* Implemented rudimentary multi-user support

* Delete src.zip

* Improve the update user function

And added a new function to the auth module to invalidate a session

* Update AntAuth.php

* Rename the configs, a bit more auth stuff

* Fix test and JS regex

* Turn the admin landing page into a twig template

* plugin/admin/ -> admin/

* Refactored templating for plugins

Plus. I finally converted the remaining options in the admin plugin to twig templates. No extra styling, but it'll be easier now

* Fix PHPStan warnings

* Basic "first time" user setup

* Improved styling

* Started implementing user management

* Completed user management in the admin panel

* Renamed templates, added support for sub-dirs

* Limit and validate allowed chars for usernames

* Finished the basics of the profile plugin

* Styling for the bootstrap theme

* Some more final touches

* Added an example to show author

* Tweak to the readme
2023-03-07 02:09:32 -08:00
Belle Aerni cba9f71f78 Lazy load images 2023-02-25 04:32:07 -08:00
dependabot[bot] 404c093ccb
Bump tailwindcss from 3.2.6 to 3.2.7 (#25)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.2.6 to 3.2.7.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.2.6...v3.2.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 18:10:17 -08:00
dependabot[bot] a653154f85
Bump league/commonmark from 2.3.8 to 2.3.9 (#24)
Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.3.8 to 2.3.9.
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.3/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.3.8...2.3.9)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 18:06:48 -08:00
Belle Aerni d08d136990 Implement a basic session expiration
It's rudimentary, but this should ensure that people will need to re-authenticate after closing their browser session
2023-02-12 19:41:52 -08:00
Belle Aerni 25e1ef9434 Started creating templates for the admin plugin
Also, replaced the old <!--AntCMS-SiteLink--> with a new filter called absUrl, which can be used to convert any link to an absolute URL by adding the base site URL
2023-02-12 17:02:07 -08:00
Belle Aerni 94f251fc3f Remove the version from package.json
AntCMS hasn't even had a release yet, so that version number is meaningless
2023-02-12 15:50:26 -08:00
dependabot[bot] 29ef7ec779
Bump tailwindcss from 3.2.4 to 3.2.6 (#22)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.2.4 to 3.2.6.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.2.4...v3.2.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 17:45:46 -08:00
dependabot[bot] d262f22c98
Bump twig/twig from 3.5.0 to 3.5.1 (#23)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/twigphp/Twig/releases)
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.5.0...v3.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 17:45:04 -08:00
Belle Aerni 3ce0896af5 Created a lighttpd config
Tested and working :)
2023-02-06 18:34:34 -08:00
Belle Aerni 9eb24fee30 First try to render exceptions using twig 2023-01-28 20:54:53 -08:00
Belle Aerni af375a5499 Code styling /w Rector 2023-01-25 22:08:19 -08:00
Belle Aerni e1667b6374 Use actual twig variables for the page body
Replaces <!--AntCMS-Body--> with {{ AntCMSBody | raw }}
2023-01-25 21:32:38 -08:00
Belle Aerni e2ca10d51d Made exception rendering more flexible 2023-01-25 18:45:36 -08:00
Belle Aerni 95ed5059f0 Cleaned up the code a bit 2023-01-24 17:48:40 -08:00
dependabot[bot] 686caf149b
Bump symfony/yaml from 6.0.17 to 6.0.19 (#21)
Bumps [symfony/yaml](https://github.com/symfony/yaml) from 6.0.17 to 6.0.19.
- [Release notes](https://github.com/symfony/yaml/releases)
- [Changelog](https://github.com/symfony/yaml/blob/6.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/yaml/compare/v6.0.17...v6.0.19)

---
updated-dependencies:
- dependency-name: symfony/yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 17:22:43 -08:00
Belle Aerni 7ea9cb2e5d Some fixes to the admin plugin 2023-01-24 17:22:12 -08:00
Belle Aerni a3039f10ed Fixed the page editor after recent page list gen changes
This part of the plugin hadn't been setup to handle the new paths that were being generated.
2023-01-20 20:39:09 -08:00
Belle Aerni ecf94d36da Removed the keyword generator
It didn't really work, added a bunch to the overall file size, and computer-generated keywords just won't compare to human created ones, unless you're to use AI or a particularly advanced algorithm, which I feel is outside the scope of the project.

I've left the class in place, so if needed, it'll be really easy to add it back
2023-01-19 18:01:03 -08:00
Belle Aerni 97e6a8a4e6 Make renderWithTiwg a static function
There's zero need to create a new instance of the class before calling it, so this removes a very small amount of overhead, but more importantly removes the need to create and define a new class of it each time, which makes the code very slightly cleaner
2023-01-19 15:07:15 -08:00
Belle Aerni ea7f3ac65b Micro-optimization to the createCacheKey function
Testing showed the in_array function made it overall slower than simply calling MD4. Using a define, I was able to get the overhead down to virtually nothing:
PHP 8.2:
Execution time for MD4: 0.064558029174805 seconds
Execution time for XXH128: 0.015785932540894 seconds
Execution time for Check: 0.0119788646698 seconds

PHP 8.1:
Execution time for MD4: 0.034939050674438 seconds
Execution time for XXH128: 0.0058550834655762 seconds
Execution time for Check: 0.0062451362609863 seconds
2023-01-19 01:41:17 -08:00
Belle Aerni bf3b425c8d MD5 -> MD4 2023-01-18 23:11:18 -08:00
Belle Aerni 612adb5a17 Revert "Use the official twig StringLoaderExtension"
This reverts commit b2036b765f.
2023-01-18 22:05:50 -08:00
Belle Aerni b2036b765f Use the official twig StringLoaderExtension
Not sure how I missed the fact that this exists, but let's use it instead of an unofficial extension
2023-01-18 22:02:33 -08:00
Belle Aerni 08cfdcf460 Stip 'index' off when generating the page list 2023-01-17 20:18:13 -08:00
Belle Aerni 02e4492ecd Improve the way the pagelist is generated
Now it automatically strips off the .md extension since it's unneeded, and it sorts it so that the 'index' of the site is always the first item in the array, since that dictates the order they appear in the browser
2023-01-17 19:45:59 -08:00
Belle Aerni f388c99647 Added the ability to toggle page visibility 2023-01-17 19:23:48 -08:00
Belle Aerni 0eca5dd3e1 Admin Plugin: added the ability to delete pages 2023-01-17 18:54:44 -08:00
Belle Aerni cbb978ccad AntConfig::saveConfig verify primary keys exist
Before saving the new config, verify that the config appears to be complete by checking for the primary keys in the array
2023-01-17 12:38:38 -08:00
Belle Aerni cdee7c8a04 Update config.php to delete all, including dirs 2023-01-16 18:05:33 -08:00
Belle Aerni 4386451516 Added a test for AntTools::repairURL 2023-01-15 12:42:37 -08:00
Belle Aerni 588811ec7a Created a feature list doc 2023-01-14 22:45:37 -08:00
Belle Aerni ff9583bfdf
Rector (#19)
* Rector code quality run

* Ran Rector with coding style ruleset

* Ran Rector with the naming setlist
2023-01-14 20:44:27 -08:00
Belle Aerni 81824132fe
Merge pull request #18 from AntCMS-org/tests
Run unit tests on PHP 8.0, 8.1, and 8.2
2023-01-14 15:52:31 -08:00
Belle Aerni fcd9e31af9 Run unit tests on PHP 8.0, 8.1, and 8.2 2023-01-14 15:50:05 -08:00
Belle Aerni c90823afbe Default to having keyword generation off 2023-01-14 15:37:57 -08:00
Belle Aerni 5669e8a429
Merge pull request #17 from AntCMS-org/mdextensions
Adding some new markdown extensions
2023-01-14 03:31:19 -08:00
Belle Aerni 8ed69831ee Check for cache before setting up the MD parser 2023-01-14 03:04:56 -08:00
Belle Aerni b8956614b0 Add nyholm/psr7 2023-01-14 02:44:13 -08:00
Belle Aerni c5b999294b Added embed extension for markdown 2023-01-14 02:38:20 -08:00
Belle Aerni 3353be4920 Replaced my homebrew keyword generator
It now uses the one found here: https://github.com/Donatello-za/rake-php-plus
This is much better than the one I had hacked together.

Makes AntCMS a bit bigger.. but not by too much. I may end up removing the keyword generator outright, but for now I'm going to keep it.
2023-01-14 00:49:21 -08:00
Belle Aerni 91395db9c4 Added robots.txt plugin
Automatically generates the content, including the correct link to the sitemap.
2023-01-14 00:30:29 -08:00
Belle Aerni eaca96fd6b Added a sitemap generator 2023-01-13 23:46:17 -08:00
Belle Aerni ca54a56eaa fix active page highlight in the Bootstrap theme 2023-01-13 01:38:07 -08:00
Belle Aerni 44de365946 Fix header regex (again) 2023-01-13 01:18:43 -08:00
Belle Aerni 4f96f5c496 Highlight the active page in the navbar
Closes #6

Also dropped PHPStan back down to level 5 because I decided level 6 was a bit much
2023-01-12 19:08:35 -08:00
Belle Aerni 325221f527 Quick run with Rector 2023-01-12 00:20:13 -08:00
Belle Aerni 36ab96bf8c Added some info to composer.json 2023-01-11 21:09:36 -08:00
Belle Aerni a2f8c3617a Slightly cleanup auth + enable CGIPassAuth
Should hopefully fix any issues with CGI servers.
Also renamed the 'SiteInfo' config key to 'siteInfo' to follow the naming scheme for everything else
2023-01-11 19:30:34 -08:00
Belle Aerni 0979d4ee03
Merge pull request #12 from AntCMS-org/dependabot/composer/phpstan/phpstan-1.9.9
Bump phpstan/phpstan from 1.9.8 to 1.9.9
2023-01-11 17:48:16 -08:00
dependabot[bot] ab5a46a4cd
Bump phpstan/phpstan from 1.9.8 to 1.9.9
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 1.9.8 to 1.9.9.
- [Release notes](https://github.com/phpstan/phpstan/releases)
- [Changelog](https://github.com/phpstan/phpstan/blob/1.10.x/CHANGELOG.md)
- [Commits](https://github.com/phpstan/phpstan/compare/1.9.8...1.9.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-12 01:37:41 +00:00
Belle Aerni 891012f6ff Fixed AntCMS matching the page header multiple times 2023-01-11 14:07:39 -08:00
Belle Aerni c513b01bad Workaround for AntCMS trying to extract header
data from the body of the getting started block. Will find a fix soonish
2023-01-10 23:48:58 -08:00
Belle Aerni 584f99bac7 Created gettingstarted 2023-01-10 23:36:58 -08:00
Belle Aerni 016103f7cd Updated readme, moved minimum PHP ver to 8.0 2023-01-10 18:50:10 -08:00
Belle Aerni 506e66c0ae
Merge pull request #11 from AntCMS-org/docsandtypehints
Added missing type hints, bumped PHPStan to level 6
2023-01-10 18:22:09 -08:00
Belle Aerni 7adb5d51a6 Added missing type hints, bumped PHPStan to level 6 2023-01-10 18:20:24 -08:00
Belle Aerni 4c0a950179
Merge pull request #10 from AntCMS-org/currentconfig
Refactor AntConfig::currentConfig
2023-01-10 17:26:46 -08:00
Belle Aerni c15bf1f449 Refactor AntConfig::currentConfig 2023-01-10 17:24:30 -08:00
Belle Aerni aff54916e9
Merge pull request #9 from AntCMS-org/dependabot/npm_and_yarn/tailwindcss/typography-0.5.9
Bump @tailwindcss/typography from 0.5.8 to 0.5.9
2023-01-10 17:22:30 -08:00
dependabot[bot] e7e7628d91
Bump @tailwindcss/typography from 0.5.8 to 0.5.9
Bumps [@tailwindcss/typography](https://github.com/tailwindcss/typography) from 0.5.8 to 0.5.9.
- [Release notes](https://github.com/tailwindcss/typography/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindcss/typography/compare/v0.5.8...v0.5.9)

---
updated-dependencies:
- dependency-name: "@tailwindcss/typography"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-11 01:13:14 +00:00
Belle Aerni da69eb3f81 Updated file extension to .twig 2023-01-10 06:32:51 -08:00
Belle Aerni 22007f4c57 Improved tailwind theme's layout for desktop 2023-01-10 04:53:49 -08:00
Belle Aerni cb0a1917a9 Update admin plugin to render with twig 2023-01-10 00:26:05 -08:00
Belle Aerni 2a1af73f75 Use the .twig file extension 2023-01-10 00:07:20 -08:00
Belle Aerni 3405291db1 Move the config and pages .yaml files to /Config 2023-01-09 22:55:02 -08:00
Belle Aerni 51211d87ec Allow access to a sitemap.xml file if it exists 2023-01-09 16:09:22 -08:00
Belle Aerni 52e102e63b Bootstrap: make the footer fixed to the bottom 2023-01-09 15:03:56 -08:00
Belle Aerni 89b182b3be
Merge pull request #8 from AntCMS-org/twig
Started to migrate to twig
2023-01-09 13:16:58 -08:00
Belle Aerni 9510bf2e9b Update tailwind for twig 2023-01-09 13:09:06 -08:00
Belle Aerni fa1d942d4e Started to migrate to twig 2023-01-09 12:50:11 -08:00
Belle Aerni d4331326f6 Missed codeblocks 2023-01-09 11:15:48 -08:00
Belle Aerni ff68feeae6 Improved readme 2023-01-09 11:11:32 -08:00
Belle Aerni 55e20d3f25 Update CMSTest.php 2023-01-09 10:55:34 -08:00
Belle Aerni b60439a9a9 Tests
Test, tests, baby (insert sounds from ice, ice, baby)
2023-01-09 10:47:55 -08:00
Belle Aerni 37a0f2ffcd Make testMarkdownIsFast much stricter
Now it runs 10 times and averages the time spent, it needs to be less than 0.015 seconds
2023-01-09 10:15:50 -08:00
Belle Aerni 1f4b40a787 Fix assertion 2023-01-09 01:09:11 -08:00
Belle Aerni 2acfc2d50d And added some more 2023-01-09 01:05:24 -08:00
Belle Aerni 5df8872a62 Fix AntTools::repairFilePath 2023-01-09 00:32:49 -08:00
Belle Aerni 22efe315c4 More unit tests 2023-01-09 00:26:30 -08:00
Belle Aerni 757b1b3ade This fix it? 2023-01-08 23:49:17 -08:00
Belle Aerni e09d4ca60d Experimenting with unit tests 2023-01-08 23:43:17 -08:00
Belle Aerni 1dc25e2d32 Move from php-markdown to commonmark
Better maintained, much more popular, supports GH style markdown.
Also added an emoji plugin for it so we don't have to maintain our own list of them.

Unfortunately, moving this does nearly double the size of AntCMS, but it's a worthwhile tradeoff
2023-01-08 22:01:05 -08:00
dependabot[bot] 074cc47625
Bump phpstan/phpstan from 1.9.7 to 1.9.8 (#7)
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 1.9.7 to 1.9.8.
- [Release notes](https://github.com/phpstan/phpstan/releases)
- [Changelog](https://github.com/phpstan/phpstan/blob/1.9.x/CHANGELOG.md)
- [Commits](https://github.com/phpstan/phpstan/compare/1.9.7...1.9.8)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-08 18:35:47 -08:00
Belle Aerni 073ee79eae Create nginx.conf
This is untested, but *should* work, it's not a complicated config
2023-01-08 17:46:22 -08:00
Belle Aerni 930766b1a3
Create LICENCE.md 2023-01-08 17:41:17 -08:00
Belle Aerni d31da240c4 Added robots.txt
And allow it through the .htaccess
2023-01-08 17:28:08 -08:00
Belle Aerni 6d3f8a3da1 Admin plugin : added create page option
Also introduced AntTools::repairFilePath
2023-01-08 16:53:58 -08:00
Belle Aerni d6588573b0 Fallback to the default theme
if the configured one doesn't exist, fallback to "Default"
2023-01-08 15:47:23 -08:00
Belle Aerni 7739b2875d + real time markdown preview
Also created admin plugin templates for the Bootstrap theme
2023-01-08 15:38:25 -08:00
Belle Aerni 6fe8dd90cd Link to the website instead of repo 2023-01-08 11:50:59 -08:00
Belle Aerni 6b0a424a52 Updated repo links 2023-01-08 09:51:21 -08:00
Belle Aerni 2de92731cf Updated readme 2023-01-08 09:28:24 -08:00
Belle Aerni 90a31b2941 Created a cron file to cleanup cache
+ use a few directory separators for better portability in the code
2023-01-08 09:14:40 -08:00
Belle Aerni 457cce525b
Basic plugin support + admin plugin (#5)
* Started writing plugin support

* Started work on the Admin plugin

* Added page list regeneration option

* Started work on the config editor

* Rebased

* Admin plugin can now edit the config

* Ability to edit pages + verify config before saving

* Make PHPStan happy :)

* Implemented authentication
2023-01-08 08:54:54 -08:00
Yağızhan 0c79ec0ba6 Prose styling 2023-01-08 08:48:49 +03:00
Belle Aerni dbf2600afb Update .htaccess 2023-01-07 21:23:15 -08:00
Belle Aerni 8d68293798 Hide code backticks. Related to #3 2023-01-07 18:04:55 -08:00
Belle Aerni f5844cb2cc Rewrote to server content through index.php 2023-01-07 17:44:16 -08:00
Belle Aerni ca005bd074 This .htaccess seems to work better for me 2023-01-07 16:48:45 -08:00
Belle Aerni 47d86d8b6d Improvements to the readme
Also made the index.md file a copy of the readme, so it will be the first thing people see when they access it
2023-01-07 14:58:42 -08:00
Belle Aerni f86b8a988b Use AntTools::getFileList while getting page list 2023-01-07 14:29:12 -08:00
Belle Aerni 1d65fca192 Fix a small typo 2023-01-07 14:09:01 -08:00
Belle Aerni d1fd6521f9 Make tailwind the default and generate the CSS
Closes #1 + makes the smaller and faster theme the default
2023-01-07 14:06:47 -08:00
Belle Aerni 3ee0161c11 Small tweak to the preview generator 2023-01-07 13:42:55 -08:00
Belle Aerni e96967f604 Two quick fixes 2023-01-07 13:37:49 -08:00
Belle Aerni b953821dd2 Add template fallbacks. Closes #2
Also improved slightly the keyword generator, and added npm to dependabot scanning
2023-01-07 13:35:14 -08:00
Belle Aerni a36755bb80 Improved caching 2023-01-07 12:15:29 -08:00
Belle Aerni ee92c2f058 False -> no 2023-01-07 11:11:10 -08:00
Belle Aerni 906c88273f Don't include dev packages in the preview build 2023-01-07 11:08:27 -08:00
Yağızhan 7af26306f6
Update readme.md 2023-01-07 20:34:19 +03:00
Yağızhan 4d745a6c9d
Update readme.md 2023-01-07 20:33:32 +03:00
Yağızhan ce656562e9 Check the description
- Separated the part that generates the page layout into a new function.
- What we did above allowed us to use the page layout for pages that aren't meant to be loaded from local Markdown files, such as the error pages
- This allowed us to create a better error page. The new error page uses the theme's default layout, resulting in better error pages.
2023-01-07 17:41:17 +03:00
Yağızhan 5562f9b676 Allow defining what theme to get the content of 2023-01-07 17:25:12 +03:00
Yağızhan c89c82fb64 This comment no longer applies 2023-01-07 16:59:01 +03:00
Yağızhan a198f3995e Tweaking Tailwind configuration 2023-01-07 16:55:36 +03:00
Yağızhan f5195315ca Updated the README section for theming 2023-01-07 13:51:38 +03:00
Yağızhan d153dc9a46 Moved the templates to their own folders 2023-01-07 13:49:47 +03:00
Yağızhan 44dcbb7cfe Merge branch 'main' of https://github.com/BelleNottelling/AntCMS 2023-01-07 13:44:22 +03:00
Yağızhan 5670529b11 Adjusting Tailwind config 2023-01-07 13:44:03 +03:00
91 changed files with 7581 additions and 2299 deletions

View file

@ -14,3 +14,11 @@ updates:
directory: "/"
schedule:
interval: "daily"
ignore:
- dependency-name: "php-actions/phpunit"
versions: ["5", "6", "7", "8", "9"]
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

View file

@ -11,9 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: php-actions/composer@v6
- uses: php-actions/phpstan@v3
with:
configuration: phpstan.neon
memory_limit: 256M
version: latest

View file

@ -10,17 +10,26 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: php-actions/composer@v6
with:
dev: no
php_version: "8.0"
- run: |
sudo apt-get install zip -y
zip -r AntCMS.zip src
zip -r AntCMS.zip .
working-directory: ./src
- run: |
npm install -D tailwindcss
rm -f ./src/Themes/Default/Assets/Dist/tailwind.css
npx tailwindcss -i ./src/Themes/Default/Assets/tailwind.css -o ./src/Themes/Default/Assets/Dist/tailwind.css --minify
- uses: ncipollo/release-action@v1
with:
artifacts: "AntCMS.zip"
artifacts: "./src/AntCMS.zip"
prerelease: true
allowUpdates: true
artifactErrorsFailBuild: true
generateReleaseNotes: true
name: "Preview Build"
tag: "latest-preview"
body: "A rolling preview release of AntCMS that is updated with each commit."

26
.github/workflows/unittests.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Unit Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-test:
runs-on: ubuntu-latest
strategy:
matrix:
php_version: [8.0, 8.1, 8.2]
steps:
- uses: actions/checkout@v4
- uses: php-actions/composer@v6
- run: |
cp ./tests/Includes/Config.yaml ./src/Config/Config.yaml
- uses: php-actions/phpunit@v3
with:
bootstrap: src/Vendor/autoload.php
php_version: ${{ matrix.php_version }}
args: "tests"
php_extensions: apcu

4
.gitignore vendored
View file

@ -1,5 +1,5 @@
/src/Vendor/
node_modules
src/Cache/*
src/config.yaml
src/pages.yaml
src/Config/Config.yaml
src/Config/Pages.yaml

201
LICENCE.md Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,12 +1,24 @@
{
"name": "belle/ant-cms",
"name": "antcms/antcms",
"type": "project",
"license": "Apache-2.0",
"description": "A simple CMS built with PHP and Markdown",
"homepage": "https://antcms.org/",
"minimum-stability": "stable",
"support": {
"issues": "https://github.com/AntCMS-org/AntCMS/issues"
},
"require": {
"michelf/php-markdown": "^2.0",
"php": ">=8.0",
"antcms/antloader": "^2.0.0",
"elgigi/commonmark-emoji": "^2.0",
"embed/embed": "^4.4",
"league/commonmark": "^2.3",
"nyholm/psr7": "^1.5",
"shapecode/twig-string-loader": "^1.1",
"simonvomeyser/commonmark-ext-lazy-image": "^2.0",
"symfony/yaml": "^6.0",
"php": ">=7.4"
"twig/twig": "^3.5"
},
"authors": [
{
@ -15,9 +27,12 @@
}
],
"config": {
"vendor-dir": "src/Vendor"
"vendor-dir": "src/Vendor",
"sort-packages": true
},
"require-dev": {
"phpstan/phpstan": "^1.9"
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.5",
"rector/rector": "^0.15.4"
}
}

3592
composer.lock generated

File diff suppressed because it is too large Load diff

8
configs/lighttpd.conf Normal file
View file

@ -0,0 +1,8 @@
$HTTP["host"] == "example.com" {
server.document-root = vhosts_dir + "/example.com/htdocs"
url.rewrite-final = (
"^/Themes/[^/]+/Assets/.+$" => "$0",
"^/(.+)$" => "/index.php/$1"
)
}

10
configs/nginx.conf Normal file
View file

@ -0,0 +1,10 @@
location ~ ^/Themes/[^/]+/Assets/ {
# If the requested file is an asset, serve it directly
if (-f $request_filename) {
break;
}
}
location / {
try_files $uri $uri/ /index.php?$args;
}

998
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"name": "antcms",
"version": "1.0.0",
"description": "A simple CMS built with PHP and Markdown",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@ -15,7 +14,8 @@
},
"homepage": "https://github.com/BelleNottelling/AntCMS#readme",
"devDependencies": {
"@tailwindcss/typography": "^0.5.8",
"tailwindcss": "^3.2.4"
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"tailwindcss": "^3.3.5"
}
}

View file

@ -5,3 +5,6 @@ parameters:
excludePaths:
analyse:
- src/Vendor
- src/Cache
- src/AntCMS/AntKeywords.php

View file

@ -1,29 +1,70 @@
# AntCMS
[![PHPStan](https://github.com/BelleNottelling/AntCMS/actions/workflows/phpstan.yml/badge.svg)](https://github.com/BelleNottelling/AntCMS/actions/workflows/phpstan.yml)
[![PHPStan](https://github.com/AntCMS-org/AntCMS/actions/workflows/phpstan.yml/badge.svg)](https://github.com/AntCMS-org/AntCMS/actions/workflows/phpstan.yml)
[![Unit Tests](https://github.com/AntCMS-org/AntCMS/actions/workflows/unittests.yml/badge.svg)](https://github.com/AntCMS-org/AntCMS/actions/workflows/unittests.yml)
A tiny and fast CMS system for static websites.
## What is AntCMS
## What is AntCMS?
AntCMS is a lightweight CMS system designed for simplicity, speed, and small size. It is a flat file CMS, meaning it lacks advanced features but benefits from improved speed and reduced complexity.
AntCMS is a lightweight CMS system designed for simplicity, speed, and small size. It is a flat-file CMS, meaning it lacks advanced features but benefits from improved speed and reduced complexity.
### How fast is AntCMS?
AntCMS is extremely fast, thanks to its simple backend and caching. It can render and deliver pages to end users in milliseconds.
AntCMS is designed for speed, with a simple backend and caching capabilities that allow it to quickly render and deliver pages to users in milliseconds. This speed is further enhanced by the use of Tailwind CSS in the default theme, which is only 25KB.
Our unit tests also ensure that rendering markdown content takes less than 0.015 seconds, as demonstrated by the following recent results: `Markdown rendering speed with cache: 0.000289 VS without: 0.003414`.
### How does it work?
AntCMS is very straightforward to use. First, you need a template in HTML with special elements for AntCMS. Then, you write your content using [markdown](https://www.markdownguide.org/getting-started/), a popular way to format plain text documents. AntCMS converts the markdown to HTML, integrates it into the template, and sends it to the viewer. Even without caching, this process is quick, but AntCMS also has caching capabilities to further improve rendering times.
Using AntCMS is simple. First, you need an HTML template with special elements for AntCMS. Then, you can write your content using the popular [markdown](https://www.markdownguide.org/cheat-sheet/) formatting syntax. AntCMS will convert the markdown to HTML, integrate it into the template, and send it to the viewer. This process is already quick, but AntCMS also has caching capabilities that can further improve rendering times.
### Themeing with AntCMS
AntCMS will also automatically leverage the APCu extension for caching, which helps to further improve your website's response time.
AntCMS stores it's themes under `/Themes`. Each theme is extremely simple, just a simple page layout template.
A theme may also have a `/Themes/Example/Assets` folder, these files can be accessed directly from the server. Files stored in any other location will be inaccessible otherwise.
For example, this is what the default theme folder structure looks like:
### Theming with AntCMS
AntCMS stores its themes in the `/Themes` directory. Each theme consists of a simple page layout template. A theme may also have an `/Assets` folder within its directory, which can be accessed directly from the server. Any files stored outside of this folder will be inaccessible.
Here is an example of the default theme folder structure:
- `/Themes`
- `/Default`
- `default_layout.html`
- `/Templates`
- `default.html.twig`
- `nav.html.twig`
- `/Assets`
- `tailwind.css`
Changing the theme is easy, simply edit `Config.yaml` and set the `activeTheme` to match the folder name of your custom theme.
To change the active theme, simply edit `Config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
### Configuring AntCMS
AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `Config.yaml`, `Pages.yaml`, and `Users.yaml`. These files will be automatically generated by AntCMS if they do not exist.
#### Options in `Config/Config.yaml`
- `siteInfo:`
- `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website.
- `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection.
- `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use.
- `enableCache: true` - Enables or disables file caching in AntCMS.
- `debug: true`- Enabled or disables debug mode.
- `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed.
#### Options in `Config/Pages.yaml`
The `Pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `Pages.yaml` file.
The order of which files are stored inside of the `Pages.yaml` file dictates what order they will be displayed in the browser window.
Here's what the `Pages.yaml` file looks like:
- `pageTitle: 'Hello World'` - This defines what the title of the page is in the navbar.
- `fullPagePath: /antcms.example.com/public_html/Content/index.md` - This defines the full path to your page, as PHP would use to access it.
- `functionalPagePath: /index.md` - This is the actual path you would use to access the page from online. Ex: `antcms.example.com/index.php`
- `showInNav: true` - If you'd like to hide a page from the navbar, set this to false and it will be hidden.
#### The Admin Plugin
AntCMS has a very simple admin plugin that you can access it by visiting `antcms.example.com/admin`.
It will then require you to authenticate using your AntCMS credentials and from there will give you a few simple actions such as editing your config, a page, or regenerating the page list.
The admin plugin also features a live preview of the content you are creating, but it's important to note that the preview doesn't support all of the markdown syntax that AntCMS does, such as emojis.
Note: when editing the config, if you 'save' it and it didn't update, this means you made an error in the config file and AntCMS prevented the file from being saved.

35
rector.php Normal file
View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\CodingStyle\Rector\FuncCall\ConsistentPregDelimiterRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassLike\RemoveAnnotationRector;
use Rector\Php80\Rector\FunctionLike\MixedTypeRector;
use Rector\Php80\Rector\FunctionLike\UnionTypesRector;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
$rectorConfig->skip([
__DIR__ . '/src/Vendor',
__DIR__ . '/src/Cache',
UnionTypesRector::class,
MixedTypeRector::class,
EncapsedStringsToSprintfRector::class,
ConsistentPregDelimiterRector::class,
RemoveAnnotationRector::class,
]);
$rectorConfig->sets([
SetList::PHP_80,
SetList::CODE_QUALITY,
SetList::CODING_STYLE,
SetList::NAMING,
SetList::DEAD_CODE,
]);
};

View file

@ -1,7 +1,8 @@
RewriteEngine On
CGIPassAuth On
# If the requested file is an asset, serve it directly
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^Themes/[^/]+/Assets/.+$ - [L]
RewriteRule ^(.+)$ index.php [L,QSA]
RewriteRule ^(.+)$ index.php [L,QSA]

96
src/AntCMS/AntAuth.php Normal file
View file

@ -0,0 +1,96 @@
<?php
namespace AntCMS;
use AntCMS\AntConfig;
class AntAuth
{
protected $role;
protected $username;
protected $authenticated;
public function getRole()
{
return $this->role;
}
public function getUsername()
{
return $this->username;
}
public function getName()
{
$currentUser = AntUsers::getUser($this->username);
return $currentUser['name'];
}
public function isAuthenticated()
{
return $this->authenticated ?? false;
}
/**
* Check if the user is authenticated using the credentials in the config file.
* If the plain text password in the config file is still present, it will be hashed and the config file will be updated.
* If the user is not authenticated, it will call AntAuth::requireAuth()
*
* @return void
*/
public function checkAuth()
{
$username = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$currentUser = AntUsers::getUser($username);
if (is_null($currentUser) || empty($currentUser['password'])) {
$this->requireAuth();
}
// If the stored password is not hashed in the config, hash it
if ($password == $currentUser['password']) {
AntUsers::updateUser($username, ['password' => $password]);
// Reload the user info so the next step can pass
$currentUser = AntUsers::getUser($username);
}
// If the credentials are still set valid, but the auth cookie has expired, re-require authentication.
if (!isset($_COOKIE['auth']) && $_COOKIE['auth'] == 'valid') {
$this->requireAuth();
}
if (password_verify($password, $currentUser['password'])) {
$this->username = $username;
$this->role = $currentUser['role'] ?? '';
return;
}
$this->requireAuth();
}
/**
* Send an authentication challenge to the browser, with the realm set to the site title in config.
*
* @return void
*/
private function requireAuth()
{
setcookie("auth", "valid");
$siteInfo = AntConfig::currentConfig('siteInfo');
header('WWW-Authenticate: Basic realm="' . $siteInfo['siteTitle'] . '"');
http_response_code(401);
echo 'You must enter a valid username and password to access this page';
exit;
}
public function invalidateSession()
{
$this->authenticated = false;
$this->requireAuth();
}
}

View file

@ -3,79 +3,114 @@
namespace AntCMS;
use AntCMS\AntMarkdown;
use AntCMS\AntKeywords;
use AntCMS\AntPages;
use AntCMS\AntConfig;
class AntCMS
{
public function renderPage($page, $params = null)
protected $antTwig;
public function __construct()
{
$start_time = microtime(true);
$this->antTwig = new AntTwig();
}
/**
* Renders a page based on the provided page name.
*
* @param string $page The name of the page to be rendered
* @return string The rendered HTML of the page
*/
public function renderPage(string $page)
{
$start_time = hrtime(true);
$content = $this->getPage($page);
$siteInfo = AntCMS::getSiteInfo();
$currentConfig = AntConfig::currentConfig();
$themeConfig = Self::getThemeConfig();
if (!$content || !is_array($content)) {
$this->renderException("404");
}
$markdown = AntMarkdown::renderMarkdown($content['content']);
$theme = $this->getThemeContent();
$pageTemplate = $theme['default_layout'];
$pageTemplate = $this->getPageLayout(null, $page, $content['template']);
$pageTemplate = str_replace('<!--AntCMS-Description-->', $content['description'], $pageTemplate);
$pageTemplate = str_replace('<!--AntCMS-Author-->', $content['author'], $pageTemplate);
$pageTemplate = str_replace('<!--AntCMS-Keywords-->', $content['keywords'], $pageTemplate);
$params = [
'AntCMSTitle' => $content['title'],
'AntCMSDescription' => $content['description'],
'AntCMSAuthor' => $content['author'],
'AntCMSKeywords' => $content['keywords'],
'AntCMSBody' => AntMarkdown::renderMarkdown($content['content']),
'ThemeConfig' => $themeConfig['config'] ?? [],
];
$pageTemplate = str_replace('<!--AntCMS-Title-->', $content['title'], $pageTemplate);
$pageTemplate = str_replace('<!--AntCMS-Navigation-->', AntPages::generateNavigation($theme['nav_layout']), $pageTemplate);
$pageTemplate = str_replace('<!--AntCMS-Body-->', $markdown, $pageTemplate);
$pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params);
$elapsed_time = (hrtime(true) - $start_time) / 1e+6;
$pageTemplate = str_replace('<!--AntCMS-SiteTitle-->', $siteInfo['siteTitle'], $pageTemplate);
$pageTemplate = str_replace('<!--AntCMS-SiteLink-->', '//' . $currentConfig['baseURL'], $pageTemplate);
$end_time = microtime(true);
$elapsed_time = round($end_time - $start_time, 4);
if ($currentConfig['debug']) {
$pageTemplate = str_replace('<!--AntCMS-Debug-->', '<p>Took ' . $elapsed_time . ' seconds to render the page. </p>', $pageTemplate);
if (AntConfig::currentConfig('debug')) {
$pageTemplate = str_replace('<!--AntCMS-Debug-->', '<p>Took ' . $elapsed_time . ' milliseconds to render the page. </p>', $pageTemplate);
}
return $pageTemplate;
}
/**
* Returns the default layout of the active theme unless otherwise specified.
*
* @param string|null $theme optional - the theme to get the page layout for.
* @param string $currentPage optional - What page is the active page.
* @return string the default page layout
*/
public static function getPageLayout(string $theme = null, string $currentPage = '', string | null $template = null)
{
$layout = empty($template) ? 'default' : $template;
$pageTemplate = self::getThemeTemplate($layout, $theme);
return str_replace('<!--AntCMS-Navigation-->', AntPages::generateNavigation(self::getThemeTemplate('nav', $theme), $currentPage), $pageTemplate);
}
/**
* Render an exception page with the provided exception code.
*
* @param string $exceptionCode The exception code to be displayed on the error page
* @param int $httpCode The HTTP response code to return, 404 by default.
* @param string $exceptionString An optional parameter to define a custom string to be displayed along side the exception.
* @return never
*/
public function renderException(string $exceptionCode, int $httpCode = 404, string $exceptionString = 'That request caused an exception to be thrown.')
{
$exceptionString .= " (Code {$exceptionCode})";
$pageTemplate = self::getPageLayout();
$params = [
'AntCMSTitle' => 'An Error Ocurred',
'AntCMSBody' => '<h1>An error ocurred</h1><p>' . $exceptionString . '</p>',
];
try {
$pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params);
} catch (\Exception) {
$pageTemplate = str_replace('{{ AntCMSTitle }}', $params['AntCMSTitle'], $pageTemplate);
$pageTemplate = str_replace('{{ AntCMSBody | raw }} ', $params['AntCMSBody'], $pageTemplate);
}
http_response_code($httpCode);
echo $pageTemplate;
exit;
}
public function renderException($exceptionCode)
{
$content = "# Error";
$content .= '<br>';
$content .= "That request caused an exception code ($exceptionCode)";
echo AntMarkdown::renderMarkdown($content);
exit;
}
public function getPage($page)
/**
* @return array<mixed>|false
*/
public function getPage(string $page)
{
$page = strtolower($page);
$pagePath = AntDir . "/Content/$page";
$pagePath = str_replace('//', '/', $pagePath);
if (is_dir($pagePath)) {
$pagePath = $pagePath . '/index.md';
} else {
$pagePath = (file_exists($pagePath)) ? $pagePath : $pagePath . '.md';
}
$pagePath = AntTools::convertFunctionaltoFullpath($page);
if (file_exists($pagePath)) {
try {
$pageContent = file_get_contents($pagePath);
$pageHeaders = AntCMS::getPageHeaders($pageContent);
// Remove the AntCMS section from the content
$pageContent = preg_replace('/--AntCMS--.*--AntCMS--/s', '', $pageContent);
$result = ['content' => $pageContent, 'title' => $pageHeaders['title'], 'author' => $pageHeaders['author'], 'description' => $pageHeaders['description'], 'keywords' => $pageHeaders['keywords']];
return $result;
} catch (\Exception $e) {
$pageContent = preg_replace('/\A--AntCMS--.*?--AntCMS--/sm', '', $pageContent);
return ['content' => $pageContent, 'title' => $pageHeaders['title'], 'author' => $pageHeaders['author'], 'description' => $pageHeaders['description'], 'keywords' => $pageHeaders['keywords'], 'template' => $pageHeaders['template']];
} catch (\Exception) {
return false;
}
} else {
@ -83,46 +118,79 @@ class AntCMS
}
}
public function getThemeContent()
/**
* @param string|null $theme
* @return string
*/
public static function getThemeTemplate(string $layout = 'default', string $theme = null)
{
$currentConfig = AntConfig::currentConfig();
$themePath = antThemePath . '/' . $currentConfig['activeTheme'];
$themeContent['default_layout'] = file_get_contents($themePath . '/default_layout.html');
$themeContent['nav_layout'] = file_get_contents($themePath . '/nav_layout.html');
$theme ??= AntConfig::currentConfig('activeTheme');
if (!$themeContent['nav_layout']) {
$themeContent['default_layout'] = '';
if (!is_dir(antThemePath . DIRECTORY_SEPARATOR . $theme)) {
$theme = 'Default';
}
if (!$themeContent['default_layout']) {
$themeContent['default_layout'] = '
<!DOCTYPE html>
<html>
<head>
<title><!--AntCMS-Title--></title>
<meta name="description" content="<!--AntCMS-Description-->">
<meta name="author" content="<!--AntCMS-Author-->">
<meta name="keywords" content="<!--AntCMS-Keywords-->">
</head>
<body>
<!--AntCMS-Body-->
</body>
</html>';
$basePath = AntTools::repairFilePath(antThemePath . DIRECTORY_SEPARATOR . $theme);
if (strpos($layout, '_') !== false) {
$layoutPrefix = explode('_', $layout)[0];
$templatePath = $basePath . DIRECTORY_SEPARATOR . 'Templates' . DIRECTORY_SEPARATOR . $layoutPrefix;
$defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates' . '/' . $layoutPrefix);
} else {
$templatePath = $basePath . DIRECTORY_SEPARATOR . 'Templates';
$defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates');
}
return $themeContent;
try {
$template = @file_get_contents($templatePath . DIRECTORY_SEPARATOR . $layout . '.html.twig');
if (empty($template)) {
$template = file_get_contents($defaultTemplates . DIRECTORY_SEPARATOR . $layout . '.html.twig');
}
} catch (\Exception) {
}
if (empty($template)) {
if ($layout == 'default') {
$template = '
<!DOCTYPE html>
<html>
<head>
<title>{{ AntCMSTitle }}</title>
<meta name="description" content="{{ AntCMSDescription }}">
<meta name="author" content="{{ AntCMSAuthor }}">
<meta name="keywords" content="{{ AntCMSKeywords }}">
</head>
<body>
<p>AntCMS had an error when fetching the page template, please contact the site administrator.</p>
{{ AntCMSBody | raw }}
</body>
</html>';
} else {
$template = '
<h1>There was an error</h1>
<p>AntCMS had an error when fetching the page template, please contact the site administrator.</p>';
}
}
return $template;
}
public static function getPageHeaders($pageContent)
/**
* @return array<mixed>
*/
public static function getPageHeaders(string $pageContent)
{
$AntKeywords = new AntKeywords();
$pageHeaders = [
'title' => 'AntCMS',
'author' => 'AntCMS',
'description' => 'AntCMS',
'keywords' => '',
];
preg_match('/--AntCMS--.*--AntCMS--/s', $pageContent, $matches);
// Remove the AntCMS section from the content
$pageContent = preg_replace('/--AntCMS--.*--AntCMS--/s', '', $pageContent);
$pageHeaders = [];
// First get the AntCMS header and store it in the matches varible
preg_match('/\A--AntCMS--.*?--AntCMS--/sm', $pageContent, $matches);
if ($matches) {
if (isset($matches[0])) {
$header = $matches[0];
preg_match('/Title: (.*)/', $header, $matches);
@ -135,21 +203,58 @@ class AntCMS
$pageHeaders['description'] = trim($matches[1] ?? 'AntCMS');
preg_match('/Keywords: (.*)/', $header, $matches);
$pageHeaders['keywords'] = trim($matches[1] ?? $AntKeywords->generateKeywords($pageContent));
} else {
$pageHeaders = [
'title' => 'AntCMS',
'author' => 'AntCMS',
'description' => 'AntCMS',
'keywords' => trim($AntKeywords->generateKeywords($pageContent)),
];
$pageHeaders['keywords'] = trim($matches[1] ?? '');
preg_match('/Template: (.*)/', $header, $matches);
$pageHeaders['template'] = trim($matches[1] ?? '');
}
return $pageHeaders;
}
/**
* @return mixed
*/
public static function getSiteInfo()
{
$currentConfig = AntConfig::currentConfig();
return $currentConfig['SiteInfo'];
return AntConfig::currentConfig('siteInfo');
}
/**
* @return void
*/
public function serveContent(string $path)
{
if (!file_exists($path)) {
$this->renderException('404');
} else {
$asset_mime_type = mime_content_type($path);
header('Content-Type: ' . $asset_mime_type);
readfile($path);
}
exit;
}
public static function redirect(string $url)
{
$url = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . $url);
header("Location: $url");
exit;
}
public static function getThemeConfig(string|null $theme = null)
{
$theme = $theme ?? AntConfig::currentConfig('activeTheme');
if (!is_dir(antThemePath . '/' . $theme)) {
$theme = 'Default';
}
$configPath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Config.yaml');
if (file_exists($configPath)) {
$config = AntYaml::parseFile($configPath);
}
return $config ?? [];
}
}

View file

@ -3,47 +3,175 @@
namespace AntCMS;
use AntCMS\AntConfig;
use Symfony\Component\Yaml\Exception\ParseException;
class AntCache
{
public function setCache($key, $content)
private int $cacheType = 0;
private string $cacheKeyApcu = '';
const noCache = 0;
const fileCache = 1;
const apcuCache = 2;
/**
* Creates a new cache object, sets the correct caching type. ('auto', 'filesystem', 'apcu', or 'none')
*/
public function __construct(null|string $mode = null)
{
$cachePath = AntCachePath . "/$key.cache";
$config = AntConfig::currentConfig();
if ($config['enableCache']) {
try {
file_put_contents($cachePath, (string)$content);
return true;
} catch (\Exception $e) {
$mode = $mode ?? AntConfig::currentConfig('cacheMode') ?? 'auto';
switch ($mode) {
case 'none':
$this->cacheType = self::noCache;
break;
case 'auto':
if (extension_loaded('apcu') && apcu_enabled()) {
$this->cacheType = self::apcuCache;
$this->cacheKeyApcu = 'AntCMS_' . hash('md5', __DIR__) . '_';
} else {
$this->cacheType = self::fileCache;
}
break;
case 'filesystem':
$this->cacheType = self::fileCache;
break;
case 'apcu':
$this->cacheType = self::apcuCache;
$this->cacheKeyApcu = 'AntCMS_' . hash('md5', __DIR__) . '_';
break;
default:
throw new \Exception("Invalid cache type. Must be 'auto', 'filesystem', 'apcu', or 'none'.");
}
}
/**
* Caches a value for a given cache key.
*
* @param string $key The cache key to use for the cached value.
* @param string $content The value to cache.
* @return bool True if the value was successfully cached, false otherwise.
* @throws ParseException If there is an error parsing the AntCMS configuration file.
*/
public function setCache(string $key, string $content)
{
switch ($this->cacheType) {
case self::noCache:
return false;
case self::fileCache:
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
return file_put_contents($cachePath, $content);
case self::apcuCache:
$apcuKey = $this->cacheKeyApcu . $key;
return apcu_store($apcuKey, $content, 7 * 24 * 60 * 60); // Save it for one week.
default:
return false;
}
}
/**
* Retrieves the cached value for a given cache key.
*
* @param string $key The cache key used to retrieve the cached value.
* @return string|false The cached value, or false if there was an error loading it or if caching is disabled.
* @throws ParseException If there is an error parsing the AntCMS configuration file.
*/
public function getCache(string $key)
{
switch ($this->cacheType) {
case self::noCache:
return false;
case self::fileCache:
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
return file_get_contents($cachePath);
case self::apcuCache:
$apcuKey = $this->cacheKeyApcu . $key;
$success = false;
$result = apcu_fetch($apcuKey, $success);
return $success ? $result : false;
default:
return false;
}
}
/**
* Determines if a cache key has a corresponding cached value.
*
* @param string $key The cache key to check.
* @return bool True if the cache key has a corresponding cached value, false otherwise. Will also return false if caching is disabled.
* @throws ParseException If there is an error parsing the AntCMS configuration file.
*/
public function isCached(string $key)
{
switch ($this->cacheType) {
case self::noCache:
return false;
case self::fileCache:
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
return file_exists($cachePath);
case self::apcuCache:
$apcuKey = $this->cacheKeyApcu . $key;
return apcu_exists($apcuKey);
default:
return false;
}
}
/**
* Generates a unique cache key for the associated content and a salt value.
* The salt is used to ensure that each cache key is unique to each component, even if multiple components are using the same source content but caching different results.
*
* @param string $content The content to generate a cache key for.
* @param string $salt An optional salt value to use in the cache key generation. Default is 'cache'.
* @return string The generated cache key.
*/
public function createCacheKey(string $content, string $salt = 'cache')
{
return hash(self::getHashAlgo(), $content . $salt);
}
/**
* Generates a unique cache key for a file and a salt value.
* The salt is used to ensure that each cache key is unique to each component, even if multiple components are using the same source content but caching different results.
*
* @param string $filePath The file path to create a cache key for.
* @param string $salt An optional salt value to use in the cache key generation. Default is 'cache'.
* @return string The generated cache key.
*/
public function createCacheKeyFile(string $filePath, string $salt = 'cache')
{
return hash_file(self::getHashAlgo(), $filePath) . $salt;
}
public static function clearCache(): void
{
$di = new \RecursiveDirectoryIterator(AntCachePath, \FilesystemIterator::SKIP_DOTS);
$ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($ri as $file) {
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
}
if (extension_loaded('apcu') && apcu_enabled()) {
$prefix = 'AntCMS_' . hash('md5', __DIR__) . '_';
$cacheInfo = apcu_cache_info();
$keys = $cacheInfo['cache_list'];
foreach ($keys as $keyInfo) {
$key = $keyInfo['info'];
if (str_starts_with($key, $prefix)) {
apcu_delete($key);
}
}
}
}
public function getCache($key)
public static function getHashAlgo(): string
{
$cachePath = AntCachePath . "/$key.cache";
$config = AntConfig::currentConfig();
if ($config['enableCache']) {
try {
$contents = file_get_contents($cachePath);
return $contents;
} catch (\Exception $e) {
return false;
}
} else {
return false;
}
}
public function isCached($key)
{
$config = AntConfig::currentConfig();
if ($config['enableCache']) {
$cachePath = AntCachePath . "/$key.cache";
return file_exists($cachePath);
} else {
return false;
}
/**
* If the server is modern enough to have xxh128, use that. It is really fast and still produces long hashes
* If not, use MD4 since it's still quite fast.
* Source: https://php.watch/articles/php-hash-benchmark
*/
return defined('HAS_XXH128') ? 'xxh128' : 'md4';
}
}

View file

@ -3,37 +3,92 @@
namespace AntCMS;
use AntCMS\AntYaml;
use Exception;
class AntConfig
{
private static $ConfigKeys = [
'siteInfo',
'forceHTTPS',
'activeTheme',
'cacheMode',
'debug',
'baseURL',
'embed',
];
/**
* Generates the default config file and saves it.
* @return void
*/
public static function generateConfig()
{
$defaultOptions = array(
'SiteInfo' => array(
$defaultOptions = [
'siteInfo' => [
'siteTitle' => 'AntCMS',
),
],
'forceHTTPS' => true,
'activeTheme' => 'Default',
'generateKeywords' => true,
'enableCache' => true,
'admin' => array(
'password' => '',
'username' => '',
),
'cacheMode' => 'auto',
'debug' => true,
'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
);
'embed' => [
'allowed_domains' => ['youtube.com', 'twitter.com', 'github.com', 'vimeo.com', 'flickr.com', 'instagram.com', 'facebook.com'],
]
];
AntYaml::saveFile(antConfigFile, $defaultOptions);
Self::saveConfig($defaultOptions);
}
public static function currentConfig()
/**
* Retrieves the current configuration from the AntCMS config file.
*
* @param string|null $key The key of the configuration item to retrieve. Use dot notation to specify nested keys.
* @return mixed The configuration array or a specific value if the key is specified.
*/
public static function currentConfig(?string $key = null)
{
return AntYaml::parseFile(antConfigFile);
// FS cache enabled to save ~10% of the time to deliver the file page.
$config = AntYaml::parseFile(antConfigFile, true);
if (is_null($key)) {
return $config;
} else {
$keys = explode('.', $key);
return self::getArrayValue($config, $keys);
}
}
public static function saveConfig($config)
/**
* @param array<mixed> $array
* @param array<mixed> $keys
* @return mixed
*/
private static function getArrayValue(array $array, array $keys)
{
AntYaml::saveFile(antConfigFile, $config);
foreach ($keys as $key) {
if (isset($array[$key])) {
return $array[$key];
} else {
return null;
}
}
}
/**
* Saves the AntCMS configuration
*
* @param array<mixed> $config The config data to be saved.
* @return bool
* @throws exception
*/
public static function saveConfig(array $config)
{
foreach (self::$ConfigKeys as $ConfigKey) {
if (!array_key_exists($ConfigKey, $config)) {
throw new Exception("New config is missing the required {$ConfigKey} key from it's array!");
}
}
return AntYaml::saveFile(antConfigFile, $config);
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace AntCMS;
class AntEnviroment
{
public static function isCli(): bool
{
return (php_sapi_name() === 'cli' || !http_response_code());
}
}

View file

@ -1,64 +0,0 @@
<?php
namespace AntCMS;
use AntCMS\AntCache;
use AntCMS\AntConfig;
class AntKeywords
{
public function generateKeywords($content = '', $count = 15)
{
$cache = new AntCache();
$cacheKey = hash('sha3-512', $content).'keywords';
$currentConfig = AntConfig::currentConfig();
if(!$currentConfig['generateKeywords']){
return '';
}
if ($cache->isCached($cacheKey)) {
$cachedKeywords = $cache->getCache($cacheKey);
if ($cachedKeywords !== false && !empty($cachedKeywords)) {
return $cachedKeywords;
}
}
// A bunch of characters we don't want to use for keyword generation
$stopWords = array(' a ', ' an ', ' and ', ' are ', ' as ', ' at ', ' be ', ' by ', ' for ', ' from ', ' has ', ' have ', ' he ', ' in ', ' is ', ' it ', ' its ', ' of ', ' on ', ' that ', ' the ', ' to ', ' was ', ' were ', ' will ', ' with ');
$symbols = array('$', '€', '£', '¥', 'CHF', '₹', '+', '-', '×', '÷', '=', '>', '<', '.', ',', ';', ':', '!', '?', '"', '\'', '(', ')', '[', ']', '{', '}', '©', '™', '°', '§', '¶', '•', '_', '/');
$markdownSymbols = array('#', '##', '###', '####', '#####', '~~', '__', '**', '`', '``', '```', '*', '+', '>', '[', ']', '(', ')', '!', '&', '|');
$numbers = array('0','1','2','3','4','5','6','7','8','9');
//Strip the aforementioned characters away
$content = str_replace($stopWords, ' ', $content);
$content = str_replace($symbols, ' ', $content);
$content = str_replace($markdownSymbols, ' ', $content);
$content = str_replace($numbers, ' ', $content);
//Convert to an arrays
$words = explode(' ', $content);
// Remove newlines
$words = array_map(function ($key) {
return preg_replace('~[\r\n]+~', ' ', $key);
}, $words);
// Handle potentially empty keys
$words = array_filter($words);
// Then finally we count and sort the keywords, returning the top ones
$word_counts = array_count_values($words);
arsort($word_counts);
$count = (count($word_counts) < $count) ? count($word_counts) : $count;
$keywords = array_slice(array_keys($word_counts), 0, $count);
$keywords = implode(', ', $keywords);
$keywords = mb_substr($keywords, 3);
$cache->setCache($cacheKey, $keywords);
return $keywords;
}
}

View file

@ -2,75 +2,77 @@
namespace AntCMS;
use Michelf\MarkdownExtra;
use AntCMS\AntCache;
use AntCMS\AntConfig;
use AntCMS\AntCMS;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
//use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
use ElGigi\CommonMarkEmoji\EmojiExtension;
use League\CommonMark\Extension\Embed\Bridge\OscaroteroEmbedAdapter;
use League\CommonMark\Extension\Embed\EmbedExtension;
use SimonVomEyser\CommonMarkExtension\LazyImageExtension;
use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
class AntMarkdown
{
const emojiMap = array(
':smile:' => '😄',
':grinning:' => '😀',
':blush:' => '😊',
':wink:' => '😉',
':heart_eyes:' => '😍',
':kissing_heart:' => '😘',
':tongue:' => '😝',
':stuck_out_tongue_winking_eye:' => '😜',
':joy:' => '😂',
':satisfied:' => '😌',
':yum:' => '😋',
':neutral_face:' => '😐',
':expressionless:' => '😑',
':unamused:' => '😒',
':sweat_smile:' => '😅',
':sweat:' => '😓',
':pensive:' => '😔',
':confused:' => '😕',
':disappointed:' => '😞',
':confounded:' => '😖',
':fearful:' => '😨',
':cold_sweat:' => '😰',
':cry:' => '😢',
':sob:' => '😭',
':angry:' => '😠',
':rage:' => '😡',
':triumph:' => '😤',
':sleepy:' => '😴',
':dizzy_face:' => '😵',
':mask:' => '😷',
':scream:' => '😱',
':flushed:' => '😳',
':frowning:' => '😦',
':anguished:' => '😧',
':weary:' => '😩',
':exploding_head:' => '🤯',
':grimacing:' => '😬',
':heart:' => '💓',
':thumbsup:' => '👍',
':thumbsdown:' => '👎'
);
public static function renderMarkdown($md)
/**
* @return string
*/
public static function renderMarkdown(string $md)
{
$cache = new AntCache();
$cacheKey = hash('sha3-512', $md).'markdown';
$antCache = new AntCache();
$cacheKey = $antCache->createCacheKey($md, 'markdown');
$config = AntConfig::currentConfig();
if ($cache->isCached($cacheKey)) {
$cachedContent = $cache->getCache($cacheKey);
if ($antCache->isCached($cacheKey)) {
$cachedContent = $antCache->getCache($cacheKey);
if ($cachedContent !== false && !empty($cachedContent)) {
return $cachedContent;
}
}
$result = MarkdownExtra::defaultTransform($md);
$result = preg_replace('/(?:~~)([^~~]*)(?:~~)/', '<s>$1</s>', $result);
foreach (AntMarkdown::emojiMap as $markdown => $unicode) {
$result = str_replace($markdown, $unicode, $result);
$defaultAttributes = [];
$themeConfig = AntCMS::getThemeConfig();
foreach (($themeConfig['defaultAttributes'] ?? []) as $class => $attributes) {
$reflectionClass = new \ReflectionClass($class);
$fqcn = $reflectionClass->getName();
$defaultAttributes[$fqcn] = $attributes;
}
$cache->setCache($cacheKey, $result);
return $result;
$mdConfig = [
'embed' => [
'adapter' => new OscaroteroEmbedAdapter(),
'allowed_domains' => $config['embed']['allowed_domains'] ?? [],
'fallback' => 'link',
],
'default_attributes' => $defaultAttributes,
];
$environment = new Environment($mdConfig);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
//$environment->addExtension(new DisallowedRawHtmlExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new EmojiExtension());
$environment->addExtension(new EmbedExtension());
$environment->addExtension(new LazyImageExtension());
$environment->addExtension(new DefaultAttributesExtension());
$markdownConverter = new MarkdownConverter($environment);
$renderedContent = $markdownConverter->convert($md);
$antCache->setCache($cacheKey, $renderedContent);
return $renderedContent;
}
}

View file

@ -5,56 +5,93 @@ namespace AntCMS;
use AntCMS\AntCMS;
use AntCMS\AntYaml;
use AntCMS\AntConfig;
use AntCMS\AntCache;
use AntCMS\AntTools;
use AntCMS\AntTwig;
class AntPages
{
/** @return void */
public static function generatePages()
{
$dir = new \RecursiveDirectoryIterator(antContentPath);
$iterator = new \RecursiveIteratorIterator($dir);
$pages = array();
$pages = AntTools::getFileList(antContentPath, 'md', true);
$pageList = array();
foreach ($iterator as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) == "md") {
$pages[] = $file->getPathname();
}
}
foreach ($pages as $page) {
$page = AntTools::repairFilePath($page);
$pageContent = file_get_contents($page);
$pageHeader = AntCMS::getPageHeaders($pageContent);
$pageFunctionalPath = str_replace(antContentPath, "", $page);
// Because we are only getting a list of files with the 'md' extension, we can blindly strip off the extension from each path.
// Doing this creates more profesional looking URLs as AntCMS can automatically add the 'md' extenstion during the page rendering process.
$pageFunctionalPath = substr(str_replace(antContentPath, "", $page), 0, -3);
if ($pageFunctionalPath == '/index') {
$pageFunctionalPath = '/';
}
if (str_ends_with($pageFunctionalPath, 'index')) {
$pageFunctionalPath = substr($pageFunctionalPath, 0, -5);
}
$currentPage = array(
'pageTitle' => $pageHeader['title'],
'fullPagePath' => $page,
'functionalPagePath' => $pageFunctionalPath,
'functionalPagePath' => ($pageFunctionalPath == DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : rtrim($pageFunctionalPath, DIRECTORY_SEPARATOR),
'showInNav' => true,
);
$pageList[] = $currentPage;
// Move the index page to the first item in the page list, so it appears as the first item in the navbar.
if ($pageFunctionalPath == DIRECTORY_SEPARATOR) {
array_unshift($pageList, $currentPage);
} else {
$pageList[] = $currentPage;
}
}
AntYaml::saveFile(antPagesList, $pageList);
}
public static function getPages()
public static function getPages():array
{
return AntYaml::parseFile(antPagesList);
}
public static function generateNavigation($navTemplate = '')
/**
* @param string $currentPage optional - What page is the active page. Used for highlighting the active page in the navbar
*/
public static function generateNavigation(string $navTemplate = '', string $currentPage = ''): string
{
$currentConfig = AntConfig::currentConfig();
$baseURL = $currentConfig['baseURL'];
$pages = AntPages::getPages();
$antCache = new AntCache;
$navHTML = '';
foreach (AntPages::getPages() as $page) {
if(!$page['showInNav']){
continue;
$theme = AntConfig::currentConfig('activeTheme');
$cacheKey = $antCache->createCacheKey(json_encode($pages), $theme . $currentPage);
if ($antCache->isCached($cacheKey)) {
$cachedContent = $antCache->getCache($cacheKey);
if (!empty($cachedContent)) {
return $cachedContent;
}
$url = "//" . str_replace('//', '/',$baseURL . $page['functionalPagePath']);
$navEntry = str_replace('<!--AntCMS-PageLink-->', $url, $navTemplate);
$navEntry = str_replace('<!--AntCMS-PageTitle-->', $page['pageTitle'], $navEntry);
$navHTML .= $navEntry;
}
$baseURL = AntConfig::currentConfig('baseURL');
foreach ($pages as $key => $page) {
$url = "//" . AntTools::repairURL($baseURL . $page['functionalPagePath']);
$pages[$key]['url'] = $url;
$pages[$key]['active'] = $currentPage == $page['functionalPagePath'];
//Remove pages that are hidden from the nav from the array before sending it to twig.
if (!(bool)$page['showInNav']) {
unset($pages[$key]);
}
}
$antTwig = new AntTwig();
$navHTML = $antTwig->renderWithTiwg($navTemplate, array('pages' => $pages));
$antCache->setCache($cacheKey, $navHTML);
return $navHTML;
}
}

18
src/AntCMS/AntPlugin.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace AntCMS;
abstract class AntPlugin
{
/**
* @param array<string> $route
* @return mixed
*/
public function handlePluginRoute(array $route)
{
die("Plugin did not define a handlePluginRoute function");
}
/** @return string */
abstract function getName();
}

View file

@ -0,0 +1,26 @@
<?php
namespace AntCMS;
use AntCMS\AntTools;
class AntPluginLoader
{
/** @return array<mixed> */
public function loadPlugins()
{
$plugins = array();
$files = AntTools::getFileList(antPluginPath, null, true);
foreach ($files as $file) {
if (str_ends_with($file, "Plugin.php")) {
include_once AntTools::repairFilePath($file);
$className = pathinfo($file, PATHINFO_FILENAME);
$plugins[] = new $className();
}
}
return $plugins;
}
}

129
src/AntCMS/AntRouting.php Normal file
View file

@ -0,0 +1,129 @@
<?php
namespace AntCMS;
class AntRouting
{
private string $baseUrl;
private string $requestUri;
private array $uriExploded;
private array $indexes = ['/', '/index.php', '/index.html', '', 'index.php', 'index.html'];
/**
* @param string $baseUrl The base site URL. Ex: domain.com
* @param string $requestUri The current request URI. Ex: /page/example
*/
public function __construct(string $baseUrl, string $requestUri)
{
$this->baseUrl = $baseUrl;
$this->requestUri = $requestUri;
$this->setExplodedUri($requestUri);
}
/**
* @param string $requestUri The current request URI. Ex: /page/example
*/
public function setRequestUri(string $requestUri): void
{
$this->$requestUri = $requestUri;
$this->setExplodedUri($requestUri);
}
/**
* Used to add to the start of the request URI. Primarially used for plugin routing.
* For example: this is used internally to rewrite /profile/edit to /plugin/profile/edit
*
* @param string $append What to append to the start of the request URI.
*/
public function requestUriUnshift(string $append): void
{
array_unshift($this->uriExploded, $append);
$this->requestUri = implode('/', $this->uriExploded);
}
/**
* Used to detect if the current request is over HTTPS. If the request is over HTTP, it'll redirect to HTTPS.
*/
public function redirectHttps(): void
{
$scheme = $_SERVER['HTTPS'] ?? $_SERVER['REQUEST_SCHEME'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null;
$isHttps = !empty($scheme) && (strcasecmp('on', $scheme) == 0 || strcasecmp('https', $scheme) == 0);
if (!$isHttps) {
$url = 'https://' . AntTools::repairURL($this->baseUrl . $this->requestUri);
header('Location: ' . $url);
exit;
}
}
/**
* Used to check if the current request URI matches a specified route.
* Supports using '*' as a wild-card. Ex: '/admin/*' will match '/admin/somthing' and '/admin'
*
* @param string $uri The Route to compare against the current URI.
*/
public function checkMatch(string $uri): bool
{
$matching = explode('/', $uri);
if (empty($matching[0])) {
array_shift($matching);
}
if (count($matching) < count($this->uriExploded) && end($matching) !== '*') {
return false;
}
foreach ($this->uriExploded as $index => $value) {
if (isset($matching[$index]) && $matching[$index] !== '*' && $matching[$index] !== $value) {
return false;
}
}
return true;
}
/**
* Attempts to detect what plugin is associated with the current URI and then routes to the matching one.
*/
public function routeToPlugin(): void
{
$pluginName = $this->uriExploded[1];
$pluginLoader = new AntPluginLoader();
$plugins = $pluginLoader->loadPlugins();
//Drop the first two elements of the array so the remaining segments are specific to the plugin.
array_splice($this->uriExploded, 0, 2);
foreach ($plugins as $plugin) {
if (strtolower($plugin->getName()) === strtolower($pluginName)) {
$plugin->handlePluginRoute($this->uriExploded);
exit;
}
}
// plugin not found
header("HTTP/1.0 404 Not Found");
echo ("Error 404");
exit;
}
/**
* @return bool Returns true if the current request URI is an index request.
*/
public function isIndex(): bool
{
return (in_array($this->requestUri, $this->indexes));
}
private function setExplodedUri(string $uri): void
{
$exploaded = explode('/', $uri);
if (empty($exploaded[0])) {
array_shift($exploaded);
}
$this->uriExploded = $exploaded;
}
}

79
src/AntCMS/AntTools.php Normal file
View file

@ -0,0 +1,79 @@
<?php
namespace AntCMS;
class AntTools
{
/**
* @return array<string>
*/
public static function getFileList(string $dir, ?string $extension = null, ?bool $returnPath = false)
{
$dir = new \RecursiveDirectoryIterator($dir);
$iterator = new \RecursiveIteratorIterator($dir);
$files = array();
foreach ($iterator as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) == $extension || $extension == null) {
$files[] = ($returnPath) ? $file->getPathname() : $file->getFilename();
}
}
return $files;
}
/**
* @return string
*/
public static function repairFilePath(string $path)
{
$newPath = realpath($path);
if (!$newPath) {
$newPath = str_replace('//', '/', $path);
$newPath = str_replace('\\\\', '/', $newPath);
$newPath = str_replace('\\', '/', $newPath);
$newPath = str_replace('/', DIRECTORY_SEPARATOR, $newPath);
}
return $newPath;
}
/**
* Repairs a URL by replacing backslashes with forward slashes and removing duplicate slashes.
*
* @param string $url The URL to repair. Note: this function will not work correctly if the URL provided has its own protocol (like HTTS://).
* @return string The repaired URL
*/
public static function repairURL(string $url)
{
$newURL = str_replace('\\\\', '/', $url);
$newURL = str_replace('\\', '/', $newURL);
return str_replace('//', '/', $newURL);
}
public static function convertFunctionaltoFullpath(string $path)
{
$pagePath = AntTools::repairFilePath(antContentPath . '/' . $path);
if (is_dir($pagePath)) {
$pagePath .= '/index.md';
}
if (!str_ends_with($pagePath, ".md")) {
$pagePath .= '.md';
}
return AntTools::repairFilePath($pagePath);
}
public static function valuesNotNull(array $required, array $actual)
{
foreach ($required as $key) {
if (!key_exists($key, $actual) or is_null($actual[$key])) {
return false;
}
}
return true;
}
}

48
src/AntCMS/AntTwig.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace AntCMS;
use AntCMS\AntConfig;
class AntTwig
{
protected $twigEnvironment;
protected $theme;
public function __construct(string $theme = null)
{
$twigCache = (AntConfig::currentConfig('enableCache') !== 'none') ? AntCachePath : false;
$this->theme = $theme ?? AntConfig::currentConfig('activeTheme');
if (!is_dir(antThemePath . DIRECTORY_SEPARATOR . $this->theme)) {
$this->theme = 'Default';
}
$this->twigEnvironment = new \Twig\Environment(new \Shapecode\Twig\Loader\StringLoader(), [
'cache' => $twigCache,
'debug' => AntConfig::currentConfig('debug'),
]);
$this->twigEnvironment->addExtension(new \AntCMS\AntTwigFilters);
}
public function renderWithSubLayout(string $layout, array $params = array())
{
$subLayout = AntCMS::getThemeTemplate($layout, $this->theme);
$mainLayout = AntCMS::getPageLayout($this->theme);
$siteInfo = AntCMS::getSiteInfo();
$params['AntCMSSiteTitle'] = $siteInfo['siteTitle'];
$params['AntCMSBody'] = $this->twigEnvironment->render($subLayout, $params);
return $this->twigEnvironment->render($mainLayout, $params);
}
public function renderWithTiwg(string $content = '', array $params = array())
{
$siteInfo = AntCMS::getSiteInfo();
$params['AntCMSSiteTitle'] = $siteInfo['siteTitle'];
return $this->twigEnvironment->render($content, $params);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace AntCMS;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use AntCMS\AntTools;
use AntCMS\AntConfig;
class AntTwigFilters extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('absUrl', [$this, 'absUrl']),
];
}
public function absUrl(string $relative): string
{
return '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . '/' . trim($relative));
}
}

130
src/AntCMS/AntUsers.php Normal file
View file

@ -0,0 +1,130 @@
<?php
namespace AntCMS;
class AntUsers
{
public static function getUser($username)
{
$users = Self::getUsers();
return $users[$username] ?? null;
}
/** This function is used to get all the info of a user that is safe to publicize.
* Mostly intended to create an array that can be safely passed to twig and used to display user information on the page, such as their name.
* @param mixed $username
* @return array
*/
public static function getUserPublicalKeys($username)
{
$user = Self::getUser($username);
if (is_null($user)) {
return [];
}
unset($user['password']);
return $user;
}
public static function getUsers()
{
if (file_exists(antUsersList)) {
return AntYaml::parseFile(antUsersList);
} else {
AntCMS::redirect('/profile/firsttime');
}
}
public static function addUser($data)
{
$data['username'] = trim($data['username']);
$data['name'] = trim($data['name']);
Self::validateUsername($data['username']);
$users = Self::getUsers();
if (key_exists($data['username'], $users)) {
return false;
}
if (!AntTools::valuesNotNull(['username', 'role', 'display-name', 'password'], $data)) {
return false;
}
$users[$data['username']] = [
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'role' => $data['role'],
'name' => $data['display-name'],
];
return AntYaml::saveFile(antUsersList, $users);
}
public static function updateUser($username, $newData)
{
foreach ($newData as $key => $value) {
if (empty($value)) {
throw new \Exception("Key $key cannot be empty.");
}
}
$users = self::getUsers();
if (!key_exists($username, $users)) {
throw new \Exception("There was an error when updating the selected user.");
}
if (isset($newData['password'])) {
$users[$username]['password'] = password_hash($newData['password'], PASSWORD_DEFAULT);
}
if (isset($newData['role'])) {
$users[$username]['role'] = $newData['role'];
}
if (isset($newData['name'])) {
$newData['name'] = trim($newData['name']);
$users[$username]['name'] = $newData['name'];
}
if (isset($newData['username'])) {
$newData['username'] = trim($newData['username']);
Self::validateUsername($newData['username']);
if (key_exists($newData['username'], $users) && $newData['username'] !== $username) {
throw new \Exception("Username is already taken.");
}
$user = $users[$username];
unset($users[$username]);
$users[$newData['username']] = $user;
}
return AntYaml::saveFile(antUsersList, $users);
}
public static function setupFirstUser($data)
{
if (file_exists(antUsersList)) {
AntCMS::redirect('/');
}
$data['username'] = trim($data['username']);
$data['name'] = trim($data['name']);
Self::validateUsername($data['username']);
$users = [
$data['username'] => [
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'role' => 'admin',
'name' => $data['name'],
],
];
return AntYaml::saveFile(antUsersList, $users);
}
private static function validateUsername($username)
{
$pattern = '/^[\p{L}\p{M}*0-9]+$/u';
if (!preg_match($pattern, $username)) {
throw new \Exception("Invalid username: \"$username\". Usernames can only contain letters, numbers, and combining marks.");
}
return true;
}
}

View file

@ -2,18 +2,50 @@
namespace AntCMS;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class AntYaml
{
public static function parseFile($file)
public static function parseFile(string $file, bool $fileCache = false): array
{
return Yaml::parseFile($file);
if ($fileCache) {
$antCache = new AntCache('filesystem');
} else {
$antCache = new AntCache();
}
$cacheKey = $antCache->createCacheKeyFile($file);
if ($antCache->isCached($cacheKey)) {
$parsed = json_decode($antCache->getCache($cacheKey), true);
}
if (empty($parsed)) {
$parsed = Yaml::parseFile($file);
$antCache->setCache($cacheKey, json_encode($parsed));
}
return $parsed;
}
public static function saveFile($file, $data)
/**
* @param array<mixed> $data
*/
public static function saveFile(string $file, array $data): bool
{
$yaml = Yaml::dump($data);
file_put_contents($file, $yaml);
return (bool) file_put_contents($file, $yaml);
}
/**
* @return array<mixed>|null
*/
public static function parseYaml(string $yaml): ?array
{
try {
return Yaml::parse($yaml);
} catch (ParseException) {
return null;
}
}
}

View file

@ -1,9 +0,0 @@
<?php
spl_autoload_register(function ($class) {
$class = str_replace('\\', '/', $class);
$path = __DIR__ . '/' . $class . '.php';
if (is_readable($path)) {
require_once $path;
}
});

0
src/Config/.gitkeep Normal file
View file

13
src/Constants.php Normal file
View file

@ -0,0 +1,13 @@
<?php
const AntDir = __DIR__;
const AntCachePath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache';
const antConfigFile = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Config.yaml';
const antPagesList = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Pages.yaml';
const antUsersList = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'Users.yaml';
const antContentPath = __DIR__ . DIRECTORY_SEPARATOR . 'Content';
const antThemePath = __DIR__ . DIRECTORY_SEPARATOR . 'Themes';
const antPluginPath = __DIR__ . DIRECTORY_SEPARATOR . 'Plugins';
if (in_array('xxh128', hash_algos())) {
define('HAS_XXH128', true);
}

View file

@ -0,0 +1,132 @@
--AntCMS--
Title: Features
Author: The AntCMS Team
Description: The features AntCMS has.
--AntCMS--
# Features
## Speed and Size
AntCMS is a lightweight and speedy content management system, with a small footprint of less than a few megabytes. Its design focuses on two key features: speed and simplicity. The default theme utilizes Tailwind CSS and has a total size of 25kb, further enhancing the system's swift performance. The efficient backend caching of content ensures that pages load quickly for visitors, even under heavy traffic.
## SEO
AntCMS simplifies the often complex and tedious task of SEO, making it effortless to achieve optimal scores on Google's PageSpeed Insight report. With AntCMS, achieving 100% in all categories is virtually effortless.
AntCMS streamlines SEO with the following features:
- Automated generation of a sitemap, allowing search engines to easily index all of your content.
- Automated management of the robots.txt file, linking to the generated sitemap and specifying which content should not be crawled.
- Exceptional speed and minimal file size.
- Default themes that are optimized for both desktop and mobile devices.
- Simple customization of important metadata such as the title, description, author, and keywords for your content.
## Markdown
AntCMS utilizes markdown to streamline the process of creating website content, eliminating the need for advanced and complex content editors. This allows for an efficient writing experience, where users can write without interruption, as styling can be added directly to the content as it is being written.
Not sure what markdown is? Here's a [cheat sheet](https://www.markdownguide.org/cheat-sheet/) on it's syntax, that same website also offers more insight into what markdown is and detailed guides for writing in markdown. Once you learn the markdown syntax, it's very easy to quickly write content with it.
AntCMS supports the full basic markdown syntax, plus added support for the GitHub Flavored Markdown (GFM) and some additional options. Here is a code example of all syntax AntCMS supports with the rendered example below:
```
## Basic Syntax
## Headers!
**Bold text**, *italicized text*, ~~striked out text~~
> block quotes
1. Ordered lists
2. Hey look, a second item!
- Unordered lists
- Hey, it's another item!
`code blocks`
Horizontal rules:
---
Links:
[title](https://www.example.com)
Pictures:
![alt text](https://picsum.photos/seed/picsum/128/128)
## Extended Syntax
~~Strikethrough.~~
Tables:
| Syntax | Description |
| ----------- | ----------- |
| Header | Title |
| Paragraph | Text |
Fenced code blocks:
Note: we can't put the codeblock here inside of a codeblock, so you'll just have to trust us :wink:
Task lists:
- [x] This one is done
- [ ] But, this one isn't
Emoji support! :joy:
And finally.. emeded content, by putting a URL on a line of it's own, the content will automatically be embeded in the page.
Note: support depends on what site the content is on, but AntCMS will always fallback to a regular link
https://www.youtube.com/watch?v=dQw4w9WgXcQ
```
And now the same content, but rendered through AntCMS:
## Basic Syntax
## Headers!
**Bold text**, *italicized text*, ~~striked out text~~
> block quotes
1. Ordered lists
2. Hey look, a second item!
- Unordered lists
- Hey, it's another item!
`code blocks`
Horizontal rules:
---
Links:
[title](https://www.example.com)
Pictures:
![alt text](https://picsum.photos/seed/picsum/128/128)
## Extended Syntax
~~Strikethrough.~~
Tables:
| Syntax | Description |
| ----------- | ----------- |
| Header | Title |
| Paragraph | Text |
Task lists:
- [x] This one is done
- [ ] But, this one isn't
Emoji support! :joy:
And finally.. embedded content, by putting a URL on a line of it's own, the content will automatically be embeded in the page.
Note: support depends on what site the content is on, but AntCMS will always fallback to a regular link
https://www.youtube.com/watch?v=dQw4w9WgXcQ

View file

@ -0,0 +1,65 @@
--AntCMS--
Title: Getting Started
Author: The AntCMS Team
Description: Getting started with AntCMS.
--AntCMS--
# Getting Started with AntCMS
Due to it's simplistic nature, getting started with AntCMS is very easy compared to some CMS solutions. Simply follow the steps we have laid out below, and you'll be off to the races!
## Installing AntCMS
First, head over to our [GitHub repository](https://github.com/AntCMS-org/AntCMS/releases) and download the latest release.
At the moment, AntCMS is still under heavy development, so the only available release will be the preview build, which is automatically updated whenever we update something.
Once you've downloaded the latest release, follow these steps to install AntCMS on your webserver and get it up and running.
1. Ensure that your webserver is running at least PHP 8.0, although for best performance we recommend PHP 8.1 or newer.
2. If you are using nginx, you will need to download the nginx config from [here](https://raw.githubusercontent.com/AntCMS-org/AntCMS/main/configs/nginx.conf)
3. Copy the installation files to the `public_html` directory for your domain. Note: while it may be possible to use AntCMS under a sub directory, it's much more likely to have issues.
4. Access AntCMS from the web, by doing so you will cause AntCMS to generate it's initial configuration files. (ex: antcms.example.com)
5. Edit the `Config/Config.yaml` file to specify the options specific to your website
1. More in-depth descriptions on these options are available on our [readme](https://github.com/AntCMS-org/AntCMS#readme)
2. You should at the very least set the `siteTitle` and the `password`. Note: setting the password is only required for you to access the admin plugin or anywhere else that may require authentication
3. If you would like, you may also change the theme your site is using. We currently offer 'Default' and 'Bootstrap' themes, both of which are fast, pretty, and well optimized for SEO.
Congratulations! You have no completed the basic steps for setting up AntCMS on your website.
### Writing Content
Writing content for AntCMS is easy as it uses [markdown](https://www.markdownguide.org/cheat-sheet/). AntCMS supports most extended markdown syntax, including emojis and some GitHub styled markdown extras.
All content is stored in the `/Content` directory as `.md` files. Subfolders can be used. For example: `/Content/docs/gettingstarted.md` will be accessible by going to example.com/docs/gettingstarted.md
All pages must include a page header, this is used by AntCMS to get important page data. Please see this example for a page header:
```
--AntCMS--
Title: An Example!
Author: The AntCMS Team
Description: Getting started with AntCMS.
--AntCMS--
```
When creating your page header, be sure to put a space after the ':', omitting it will cause issues when AntCMS tries to fetch the header info.
Valid: `Title: This is a Title` invalid: `Title:This is a Title`.
When you create a new page, it won't be automatically added to the page navigation on your website. This is because of the way AntCMS generates a list of all pages and then returns that list, rather than re-discovering your pages on each request.
To manually add a new page, you can manually edit the `Config/Pages.yaml` file, delete the file which will cause AntCMS to automatically regenerate it, or use the admin plugin to regenerate the list. (covered later in this guide)
Just as how a page is simply created by adding a new file to the `/Content` directory, deleting it is as easy as deleting the file and removing it from the `/Config/Pages.yaml` file.
Note: In the future, the page management experience will be improved to provide greater flexibility and to be more streamlined.
#### The Admin Plugin
AntCMS has a basic admin plugin to make it easier to write content for your website. While the styling is limited, the plugin does provide features to help creating content a bit easier.
To login to the admin plugin, visit `example.com/plugin/admin`. You will then be prompted to login to login with the credentials you setup in your `Config/Config.yaml` file.
The plugin provides a few easy tools, such as a way to edit the configuration file of your AntCMS instance, create a new page, or edit existing content.
The plugin also provides a live preview of the content you are writing. (note: the preview may support all of the markdown features the core app has.)
Here's a preview from the admin plugin:
![alt text](https://raw.githubusercontent.com/AntCMS-org/.github/main/screenshots/contentpreview.png)
And there you go! You should now have a fully functional instance of AntCMS, a fast, tiny, and simple CMS to get your content online!

View file

@ -1,11 +1,72 @@
--AntCMS--
Title: Hello World
Author: Belle Nottelling
Description: Hello world test page for AntCMS
Keywords: AntCMS, CMS, fast, tiny
Title: AntCMS Readme
Author: The AntCMS Team
Description: The ReadMe file for AntCMS, rendered quickly and simply using AntCMS.
--AntCMS--
# Hello World
# AntCMS
This is the index.md file for my AntCMS project!
If you see this, it works!
A tiny and fast CMS system for static websites.
## What is AntCMS?
AntCMS is a lightweight CMS system designed for simplicity, speed, and small size. It is a flat-file CMS, meaning it lacks advanced features but benefits from improved speed and reduced complexity.
### How fast is AntCMS?
AntCMS is designed for speed, with a simple backend and caching capabilities that allow it to quickly render and deliver pages to users in milliseconds. This speed is further enhanced by the use of Tailwind CSS in the default theme, which is only 25KB.
Our unit tests also ensure that rendering markdown content takes less than 0.015 seconds, as demonstrated by the following recent results: `Markdown rendering speed with cache: 0.000289 VS without: 0.003414`.
### How does it work?
Using AntCMS is simple. First, you need an HTML template with special elements for AntCMS. Then, you can write your content using the popular [markdown](https://www.markdownguide.org/cheat-sheet/) formatting syntax. AntCMS will convert the markdown to HTML, integrate it into the template, and send it to the viewer. This process is already quick, but AntCMS also has caching capabilities that can further improve rendering times.
### Theming with AntCMS
AntCMS stores its themes in the `/Themes` directory. Each theme consists of a simple page layout template. A theme may also have an `/Assets` folder within its directory, which can be accessed directly from the server. Any files stored outside of this folder will be inaccessible.
Here is an example of the default theme folder structure:
- `/Themes`
- `/Default`
- `/Templates`
- `default.html.twig`
- `nav.html.twig`
- `/Assets`
- `tailwind.css`
To change the active theme, simply edit `Config.yaml` and set the `activeTheme` option to match the folder name of your custom theme.
### Configuring AntCMS
AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `Config.yaml`, `Pages.yaml`, and `Users.yaml`. These files will be automatically generated by AntCMS if they do not exist.
#### Options in `Config/Config.yaml`
- `siteInfo:`
- `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website.
- `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection.
- `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use.
- `cacheMode: auto` - Allows AntCMS to auto-detect if it should use APCu or the file system for it's cache. Also accepts 'none', 'apcu', and 'filesystem'.
- `debug: true`- Enabled or disables debug mode.
- `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed.
#### Options in `Config/Pages.yaml`
The `Pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `Pages.yaml` file.
The order of which files are stored inside of the `Pages.yaml` file dictates what order they will be displayed in the browser window.
Here's what the `Pages.yaml` file looks like:
- `pageTitle: 'Hello World'` - This defines what the title of the page is in the navbar.
- `fullPagePath: /antcms.example.com/public_html/Content/index.md` - This defines the full path to your page, as PHP would use to access it.
- `functionalPagePath: /index.md` - This is the actual path you would use to access the page from online. Ex: `antcms.example.com/index.php`
- `showInNav: true` - If you'd like to hide a page from the navbar, set this to false and it will be hidden.
#### The Admin Plugin
AntCMS has a very simple admin plugin that you can access it by visiting `antcms.example.com/admin`.
It will then require you to authenticate using your AntCMS credentials and from there will give you a few simple actions such as editing your config, a page, or regenerating the page list.
The admin plugin also features a live preview of the content you are creating, but it's important to note that the preview doesn't support all of the markdown syntax that AntCMS does, such as emojis.
Note: when editing the config, if you 'save' it and it didn't update, this means you made an error in the config file and AntCMS prevented the file from being saved.

View file

@ -1,69 +0,0 @@
--AntCMS--
Title: Markdown Test
Author: Belle Nottelling
Description: A test page for AntCMS
Keywords: AntCMS, CMS, fast, tiny
--AntCMS--
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
Paragraphs are separated by a blank line.
*Italic* or _italic_ text.
**Bold** or __bold__ text.
~~Strikethrough~~ text.
Unordered lists:
- Item 1
- Item 2
- Item 3
Ordered lists:
1. Item 1
2. Item 2
3. Item 3
Nested lists:
- Item 1
- Subitem 1
- Subitem 2
- Item 2
- Subitem 1
- Subitem 2
Links:
[Link text](http://example.com)
Images:
![Alt text](http://example.com/image.jpg)
Inline code: `code`
Blockquotes:
> Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Tables:
| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
Horizontal rule:
---
Emoji: :smile: :heart: :thumbsup:

View file

@ -1,25 +0,0 @@
--AntCMS--
Title: A Random Story
Author: ChatGPT
Description: A random story, written by ChatGPT
--AntCMS--
# The Lost City
It was a dark and stormy night when adventurer Sarah set out to find the lost city of gold. She had spent years studying ancient maps and artifacts, and she was convinced that the city was real.
Sarah packed her backpack with all the supplies she would need for the journey: a compass, a map, a flashlight, and some granola bars. She put on her waterproof coat and set off into the storm.
As she trudged through the muddy jungle, Sarah couldn't help but wonder what she would find in the lost city. Would it be filled with treasure beyond her wildest dreams? Or would it be a trap, filled with danger at every turn?
Despite the doubts that crept into her mind, Sarah was determined to find the lost city. She followed her compass through the dense jungle, and after several days of hiking, she finally saw something that made her heart skip a beat.
There, in the distance, was a glint of gold.
Sarah's heart raced as she approached the golden city. She couldn't believe her eyes. It was even more beautiful and opulent than she had imagined.
As she explored the city, Sarah found all sorts of treasure - gold coins, diamonds, and ancient artifacts. She couldn't believe her luck.
But as the sun began to set, Sarah knew it was time to return home. She had found the lost city of gold, and now it was time to share her discovery with the world.
As Sarah made her way back to civilization, she couldn't help but feel grateful for the adventure of a lifetime. She would never forget the lost city of gold, and the memories of her journey would stay with her forever.

View file

@ -1,33 +0,0 @@
--AntCMS--
Title: AntCMS Readme
Author: Belle Nottelling
Description: The ReadMe file for AntCMS, rendered quickly and simply using AntCMS.
--AntCMS--
# AntCMS
A tiny and fast CMS system for static websites.
## What is AntCMS
AntCMS is a lightweight CMS system designed for simplicity, speed, and small size. It is a flat file CMS, meaning it lacks advanced features but benefits from improved speed and reduced complexity.
### How fast is AntCMS?
AntCMS is extremely fast, thanks to its simple backend and caching. It can render and deliver pages to end users in milliseconds.
### How does it work?
AntCMS is very straightforward to use. First, you need a template in HTML with special elements for AntCMS. Then, you write your content using [markdown](https://www.markdownguide.org/getting-started/), a popular way to format plain text documents. AntCMS converts the markdown to HTML, integrates it into the template, and sends it to the viewer. Even without caching, this process is quick, but AntCMS also has caching capabilities to further improve rendering times.
### Themeing with AntCMS
AntCMS stores it's themes under `/Themes`. Each theme is extremely simple, just a simple page layout template.
A theme may also have a `/Themes/Example/Assets` folder, these files can be accessed directly from the server. Files stored in any other location will be inaccessible otherwise.
For example, this is what the default theme folder structure looks like:
- `/Themes`
- `/Default`
- `default_layout.html`
Changing the theme is easy, simply edit `Config.yaml` and set the `activeTheme` to match the folder name of your custom theme.

View file

@ -0,0 +1,328 @@
<?php
use AntCMS\AntCMS;
use AntCMS\AntPlugin;
use AntCMS\AntConfig;
use AntCMS\AntPages;
use AntCMS\AntYaml;
use AntCMS\AntAuth;
use AntCMS\AntTools;
use AntCMS\AntTwig;
use AntCMS\AntUsers;
class AdminPlugin extends AntPlugin
{
protected $auth;
protected $antCMS;
protected $AntTwig;
public function getName(): string
{
return 'Admin';
}
/**
* @param array<string> $route
* @return void
*/
public function handlePluginRoute(array $route)
{
$currentStep = $route[0] ?? 'none';
$this->auth = new AntAuth;
$this->auth->checkAuth();
$this->antCMS = new AntCMS;
$this->AntTwig = new AntTwig();
array_shift($route);
switch ($currentStep) {
case 'config':
$this->configureAntCMS($route);
case 'pages':
$this->managePages($route);
case 'users':
$this->userManagement($route);
default:
$params = [
'AntCMSTitle' => 'AntCMS Admin Dashboard',
'AntCMSDescription' => 'The AntCMS admin dashboard',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
'user' => AntUsers::getUserPublicalKeys($this->auth->getUsername()),
];
echo $this->AntTwig->renderWithSubLayout('admin_landing', $params);
break;
}
}
/**
* @param array<string> $route
* @return never
*/
private function configureAntCMS(array $route)
{
if ($this->auth->getRole() != 'admin') {
$this->antCMS->renderException("You are not permitted to visit this page.");
}
$currentConfig = AntConfig::currentConfig();
$currentConfigFile = file_get_contents(antConfigFile);
$params = array(
'AntCMSTitle' => 'AntCMS Configuration',
'AntCMSDescription' => 'The AntCMS configuration screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'edit':
$params['AntCMSActionURL'] = '//' . $currentConfig['baseURL'] . 'admin/config/save';
$params['AntCMSTextAreaContent'] = htmlspecialchars($currentConfigFile);
echo $this->AntTwig->renderWithSubLayout('textareaEdit', $params);
break;
case 'save':
if (!$_POST['textarea']) {
AntCMS::redirect('/admin/config');
}
$yaml = AntYaml::parseYaml($_POST['textarea']);
if (is_array($yaml)) {
AntYaml::saveFile(antConfigFile, $yaml);
}
AntCMS::redirect('/admin/config');
break;
default:
foreach ($currentConfig as $key => $value) {
if (is_array($value)) {
foreach ($value as $subkey => $subvalue) {
if (is_bool($subvalue)) {
$currentConfig[$key][$subkey] = ($subvalue) ? 'true' : 'false';
}
if (is_array($subvalue)) {
$currentConfig[$key][$subkey] = implode(', ', $subvalue);
}
}
} else if (is_bool($value)) {
$currentConfig[$key] = ($value) ? 'true' : 'false';
}
}
$params['currentConfig'] = $currentConfig;
echo $this->AntTwig->renderWithSubLayout('admin_config', $params);
break;
}
exit;
}
/**
* @param array<string> $route
* @return never
*/
private function managePages(array $route)
{
$pages = AntPages::getPages();
$params = array(
'AntCMSTitle' => 'AntCMS Page Management',
'AntCMSDescription' => 'The AntCMS page management screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'regenerate':
AntPages::generatePages();
AntCMS::redirect('/admin/pages');
exit;
case 'edit':
if (!isset($_POST['newpage'])) {
array_shift($route);
$pagePath = AntTools::convertFunctionaltoFullpath(implode('/', $route));
$page = file_get_contents($pagePath);
//Finally, we strip off the antContentPath for compatibility with the save function.
$pagePath = str_replace(antContentPath, '', $pagePath);
} else {
$pagePath = '/' . $_POST['newpage'];
if (!str_ends_with($pagePath, ".md")) {
$pagePath .= '.md';
}
$pagePath = AntTools::repairFilePath($pagePath);
$name = $this->auth->getName();
$page = "--AntCMS--\nTitle: New Page Title\nAuthor: $name\nDescription: Description of this page.\nKeywords: Keywords\n--AntCMS--\n";
}
$params['AntCMSActionURL'] = '//' . AntConfig::currentConfig('baseURL') . "admin/pages/save/{$pagePath}";
$params['AntCMSTextAreaContent'] = $page;
echo $this->AntTwig->renderWithSubLayout('markdownEdit', $params);
break;
case 'save':
array_shift($route);
$pagePath = AntTools::repairFilePath(antContentPath . '/' . implode('/', $route));
if (!isset($_POST['textarea'])) {
AntCMS::redirect('/admin/pages');
}
file_put_contents($pagePath, $_POST['textarea']);
AntCMS::redirect('/admin/pages');
exit;
case 'create':
$params['BaseURL'] = AntConfig::currentConfig('baseURL');
echo $this->AntTwig->renderWithSubLayout('admin_newPage', $params);
break;
case 'delete':
array_shift($route);
$pagePath = AntTools::convertFunctionaltoFullpath(implode('/', $route));
// Find the key associated with the functional page path, then remove it from our temp pages array
foreach ($pages as $key => $page) {
if ($page['fullPagePath'] == $pagePath) {
unset($pages[$key]);
}
}
// If we were able to delete the page, update the pages list with the updated pages array.
if (file_exists($pagePath) && unlink($pagePath)) {
AntYaml::saveFile(antPagesList, $pages);
}
AntCMS::redirect('/admin/pages');
break;
case 'togglevisibility':
array_shift($route);
$pagePath = AntTools::convertFunctionaltoFullpath(implode('/', $route));
foreach ($pages as $key => $page) {
if ($page['fullPagePath'] == $pagePath) {
$pages[$key]['showInNav'] = !$page['showInNav'];
}
}
AntYaml::saveFile(antPagesList, $pages);
AntCMS::redirect('/admin/pages');
break;
default:
foreach ($pages as $key => $page) {
$pages[$key]['editurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/edit/" . $page['functionalPagePath']);
$pages[$key]['deleteurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/delete/" . $page['functionalPagePath']);
$pages[$key]['togglevisibility'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/togglevisibility/" . $page['functionalPagePath']);
$pages[$key]['isvisable'] = $this->boolToWord($page['showInNav']);
}
$params = [
'AntCMSTitle' => 'AntCMS Admin Dashboard',
'AntCMSDescription' => 'The AntCMS admin dashboard',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
'pages' => $pages,
];
echo $this->AntTwig->renderWithSubLayout('admin_managePages', $params);
break;
}
exit;
}
private function userManagement(array $route)
{
if ($this->auth->getRole() != 'admin') {
$this->antCMS->renderException("You are not permitted to visit this page.");
}
$params = array(
'AntCMSTitle' => 'AntCMS User Management',
'AntCMSDescription' => 'The AntCMS user management screen',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
);
switch ($route[0] ?? 'none') {
case 'add':
echo $this->AntTwig->renderWithSubLayout('admin_userAdd', $params);
break;
case 'edit':
$user = AntUsers::getUserPublicalKeys($route[1]);
if (!$user) {
AntCMS::redirect('/admin/users');
}
$user['username'] = $route[1];
$params['user'] = $user;
echo $this->AntTwig->renderWithSubLayout('admin_userEdit', $params);
break;
case 'resetpassword':
$user = AntUsers::getUserPublicalKeys($route[1]);
if (!$user) {
AntCMS::redirect('/admin/users');
}
$user['username'] = $route[1];
$params['user'] = $user;
echo $this->AntTwig->renderWithSubLayout('admin_userResetPassword', $params);
break;
case 'save':
$data['username'] = $_POST['username'] ?? null;
$data['name'] = $_POST['display-name'] ?? null;
$data['role'] = $_POST['role'] ?? null;
$data['password'] = $_POST['password'] ?? null;
foreach ($data as $key => $value) {
if (is_null($value)) {
unset($data[$key]);
}
}
AntUsers::updateUser($_POST['originalusername'], $data);
AntCMS::redirect('/admin/users');
break;
case 'savenew':
AntUsers::addUser($_POST);
AntCMS::redirect('/admin/users');
break;
default:
$users = AntUsers::getUsers();
foreach ($users as $key => $user) {
unset($users[$key]['password']);
$users[$key]['username'] = $key;
}
$params['users'] = $users;
echo $this->AntTwig->renderWithSubLayout('admin_users', $params);
break;
}
exit;
}
/**
* @return string
*/
private function boolToWord(bool $value)
{
return $value ? 'true' : 'false';
}
}

View file

@ -0,0 +1,119 @@
<?php
use AntCMS\AntPlugin;
use AntCMS\AntAuth;
use AntCMS\AntCMS;
use AntCMS\AntTwig;
use AntCMS\AntUsers;
class ProfilePlugin extends AntPlugin
{
protected $antAuth;
protected $antTwig;
public function handlePluginRoute(array $route)
{
$this->antAuth = new AntAuth;
$this->antTwig = new AntTwig;
$currentStep = $route[0] ?? 'none';
$params = [
'AntCMSTitle' => 'AntCMS Profile Management',
'AntCMSDescription' => 'AntCMS Profile Management',
'AntCMSAuthor' => 'AntCMS',
'AntCMSKeywords' => '',
];
switch ($currentStep) {
case 'firsttime':
if (file_exists(antUsersList)) {
AntCMS::redirect('/admin');
}
echo $this->antTwig->renderWithSubLayout('profile_firstTime', $params);
break;
case 'submitfirst':
if (file_exists(antUsersList)) {
AntCMS::redirect('/admin');
}
if (isset($_POST['username']) && isset($_POST['password']) && isset($_POST['display-name'])) {
$data = [
'username' => $_POST['username'],
'password' => $_POST['password'],
'name' => $_POST['display-name'],
];
AntUsers::setupFirstUser($data);
AntCMS::redirect('/admin');
} else {
AntCMS::redirect('/profile/firsttime');
}
break;
case 'edit':
$this->antAuth->checkAuth();
$user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
if (!$user) {
AntCMS::redirect('/profile');
}
$user['username'] = $this->antAuth->getUsername();
$params['user'] = $user;
echo $this->antTwig->renderWithSubLayout('profile_edit', $params);
break;
case 'resetpassword':
$this->antAuth->checkAuth();
$user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
if (!$user) {
AntCMS::redirect('/profile');
}
$user['username'] = $this->antAuth->getUsername();
$params['user'] = $user;
echo $this->antTwig->renderWithSubLayout('profile_resetPassword', $params);
break;
case 'save':
$this->antAuth->checkAuth();
$data['username'] = $_POST['username'] ?? null;
$data['name'] = $_POST['display-name'] ?? null;
$data['password'] = $_POST['password'] ?? null;
foreach ($data as $key => $value) {
if (is_null($value)) {
unset($data[$key]);
}
}
AntUsers::updateUser($this->antAuth->getUsername(), $data);
AntCMS::redirect('/profile');
break;
case 'logout':
$this->antAuth->invalidateSession();
if (!$this->antAuth->isAuthenticated()) {
echo "You have been logged out.";
} else {
echo "There was an error logging you out.";
}
exit;
default:
$this->antAuth->checkAuth();
$params['user'] = AntUsers::getUserPublicalKeys($this->antAuth->getUsername());
echo $this->antTwig->renderWithSubLayout('profile_landing', $params);
}
exit;
}
public function getName(): string
{
return 'Profile';
}
}

View file

@ -0,0 +1,28 @@
<?php
use AntCMS\AntPlugin;
use AntCMS\AntConfig;
use AntCMS\AntTools;
class RobotstxtPlugin extends AntPlugin
{
public function handlePluginRoute(array $route)
{
$protocol = AntConfig::currentConfig('forceHTTPS') ? 'https' : 'http';
$baseURL = AntConfig::currentConfig('baseURL');
$robotstxt = 'User-agent: *' . "\n";
$robotstxt .= 'Disallow: /plugin/' . "\n";
$robotstxt .= 'Disallow: /admin/' . "\n";
$robotstxt .= 'Disallow: /profile/' . "\n";
$robotstxt .= 'Sitemap: ' . $protocol . '://' . AntTools::repairURL($baseURL . '/sitemap.xml' . "\n");
header("Content-Type: text/plain");
echo $robotstxt;
exit;
}
public function getName(): string
{
return 'Robotstxt';
}
}

View file

@ -0,0 +1,55 @@
<?php
use AntCMS\AntPlugin;
use AntCMS\AntPages;
use AntCMS\AntConfig;
use AntCMS\AntTools;
class SitemapPlugin extends AntPlugin
{
public function handlePluginRoute(array $route)
{
$protocol = AntConfig::currentConfig('forceHTTPS') ? 'https' : 'http';
$baseURL = AntConfig::currentConfig('baseURL');
$pages = AntPages::getPages();
if (extension_loaded('dom')) {
$domDocument = new DOMDocument('1.0', 'UTF-8');
$domDocument->formatOutput = true;
$domElement = $domDocument->createElement('urlset');
$domElement->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
$domDocument->appendChild($domElement);
$urls = array();
foreach ($pages as $key => $value) {
$urls[$key]['url'] = $value['functionalPagePath'];
$urls[$key]['lastchange'] = date('Y-m-d', filemtime($value['fullPagePath']));
}
foreach ($urls as $url) {
$element = $domDocument->createElement('url');
$loc = $domDocument->createElement('loc', $protocol . '://' . AntTools::repairURL($baseURL . $url['url']));
$element->appendChild($loc);
$lastmod = $domDocument->createElement('lastmod', $url['lastchange']);
$element->appendChild($lastmod);
$domElement->appendChild($element);
}
header('Content-Type: application/xml');
echo $domDocument->saveXML();
exit;
} else {
die("AntCMS is unable to generate a sitemap without having the DOM extension loadded in PHP.");
}
}
public function getName(): string
{
return 'Sitemap';
}
}

View file

@ -0,0 +1,14 @@
config:
showAuthor: false
defaultAttributes:
\League\CommonMark\Extension\Table\Table:
class: table table-hover
\League\CommonMark\Extension\CommonMark\Node\Inline\Image:
class: img-fluid TinyZoom
\League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote:
class: blockquote
\League\CommonMark\Extension\CommonMark\Node\Block\ListBlock:
class:
\League\CommonMark\Extension\CommonMark\Node\Block\ListItem:
class:

View file

@ -0,0 +1,9 @@
<h1>Page Management</h1>
<p>Create new page</p>
<form method="post" action="{{ "admin/pages/edit"|absUrl }}">
<div>
<label for="input">URL for new page: {{ BaseURL }} </label>
<input type="text" name="newpage" id="input">
<input type="submit" value="Submit" class="btn btn-primary">
</div>
</form>

View file

@ -0,0 +1,28 @@
<form method="POST" action="{{ " admin/users/savenew"|absUrl }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-3">
<label for="role" class="form-label">Role:</label>
<select name="role" id="role" multiple class="form-select">
<option value="admin">Admin</option>
<option value="writer">Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</form>

View file

@ -0,0 +1,25 @@
<form method="POST" action="{{ " admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control" value="{{ user.name }}">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control" value="{{ user.username }}">
</div>
<div class="mb-3">
<label for="role" class="form-label">Role:</label>
<select name="role" id="role" class="form-select">
<option value="admin" {% if user.role=='admin' %} selected {% endif %}>Admin</option>
<option value="writer" {% if user.role=='writer' %} selected {% endif %}>Writer</option>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,12 @@
<form method="POST" action="{{ " admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-3">
<label for="password" class="form-label">New Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,78 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ AntCMSDescription }}">
<meta name="author" content="{{ AntCMSAuthor }}">
<meta name="keywords" content="{{ AntCMSKeywords }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N"
crossorigin="anonymous"></script>
<script src="{{ "Themes/Default/Assets/Dist/TinyZoom.js"|absUrl }}" defer></script>
<style>
.fullscreen-image {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
backdrop-filter: blur(5px);
}
</style>
<title>{{ AntCMSTitle }}</title>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="{{ ""|absUrl }}">{{ AntCMSSiteTitle}}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<!--AntCMS-Navigation-->
</ul>
</div>
</div>
</nav>
<!-- Content -->
<div class="container">
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-10">
{% if AntCMSAuthor is defined and ThemeConfig.showAuthor %}
<p>This content was written by {{ AntCMSAuthor }}</p>
{% endif %}
{{ AntCMSBody | raw }}
</div>
<div class="col-md-1"></div>
</div>
</div>
<footer class="text-center text-lg-start">
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2);">
Powered by
<a href="https://antcms.org">AntCMS</a>
<!--AntCMS-Debug-->
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,31 @@
<div class="container">
<div class="row">
<div class="col-md-6">
<h3>Page Content</h3>
<form action="{{ AntCMSActionURL }}" method="post">
<textarea id="markdown-input" name="textarea" rows="100" style="width: 100%; height: 100%;"
class="form-control">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save">
</form>
</div>
<div class="col-md-6">
<h3>Page Preview</h3>
<div id="markdown-output"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const input = document.getElementById("markdown-input");
const output = document.getElementById("markdown-output");
function parseMarkdown() {
const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, "");
output.innerHTML = marked.parse(inputValue);
}
parseMarkdown();
input.addEventListener("input", parseMarkdown);
</script>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
{% for page in pages %}
{% if page.active %}
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ page.url }}">{{ page.pageTitle }}</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ page.url }}">{{ page.pageTitle }}</a>
</li>
{% endif %}
{% endfor %}

View file

@ -0,0 +1,15 @@
<form method="POST" action="{{ " profile/save"|absUrl }}">
<div class="mb-3">
<label for="display-name" class="form-label">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="form-control" value="{{ user.name }}">
</div>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" name="username" required class="form-control" value="{{ user.username }}">
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,10 @@
<form method="POST" action="{{ " profile/save"|absUrl }}">
<div class="mb-3">
<label for="password" class="form-label">New Password:</label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="mb-4 mt-4">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>

View file

@ -0,0 +1,5 @@
<form action="{{ AntCMSActionURL }}" method="post">
<textarea name="textarea" rows="25" cols="75" type="text" <textarea id="markdown-input" name="textarea" rows="100"
style="width: 100%; height: 100%;" class="form-control">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save">
</form>

View file

@ -0,0 +1 @@
class TinyZoom{constructor(e){let t=document.querySelectorAll(e);t.forEach(e=>{e.style.cursor="pointer",e.addEventListener("click",()=>{this.makeFullscreen(e)}),e.addEventListener("touchstart",t=>{t.preventDefault(),this.makeFullscreen(e)})})}makeFullscreen(e){let t=document.createElement("div");t.classList.add("fullscreen-image");let i=document.createElement("canvas"),n=i.getContext("2d"),l,a,d,s=window.devicePixelRatio||1;screen.orientation.type.startsWith("portrait"),i.style.maxWidth="85%",i.style.maxHeight="85%",i.width=e.width*s,i.height=e.height*s,n.scale(s,s),n.drawImage(e,0,0,e.width,e.height);var r=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),h=Math.max(document.documentElement.clientHeight||0,window.innerHeight||0),o=1.1*e.width>r?r/e.width*.9:1,c=1.1*e.height>h?h/e.height*.9:1;function m(t){s*=t,i.style.transform=`scale(${s})`,n.clearRect(0,0,i.width,i.height),n.drawImage(e,0,0,e.width,e.height)}t.appendChild(i),document.body.appendChild(t),t.addEventListener("click",e=>{e.target===t&&t.remove()}),t.addEventListener("wheel",e=>{e.target===t&&e.preventDefault()}),i.style.position="fixed",i.style.left=r/2-e.width/2+"px",i.style.top=h/2-e.height/2+"px",i.style.cursor="move",i.addEventListener("wheel",e=>{e.preventDefault();let t=e.deltaY>0?.9:1.1;m(t)}),i.addEventListener("mousedown",e=>{l=e.pageX-i.offsetLeft,a=e.pageY-i.offsetTop,d=!0}),i.addEventListener("mousemove",e=>{if(d){let t=e.pageX-l,n=e.pageY-a;i.style.left=`${t}px`,i.style.top=`${n}px`}}),i.addEventListener("mouseup",()=>{d=!1}),i.addEventListener("dblclick",e=>{m(e.shiftKey?.5:2)});var p=Math.min(1,o,c);1!=p&&(s=p,i.style.left=r/2-e.width*s/2+"px",i.style.top=h/2-e.height*s/2+"px",m(1))}}window.addEventListener("load",()=>{new TinyZoom(".TinyZoom")});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,6 @@
config:
showAuthor: false
defaultAttributes:
\League\CommonMark\Extension\CommonMark\Node\Inline\Image:
class: TinyZoom

View file

@ -0,0 +1,17 @@
<h1>AntCMS Configuration</h1>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/config/edit"|absUrl }}">Edit Config</a>
<ul>
{% for key, value in currentConfig %}
{% if value is iterable %}
<li>{{ key }}:</li>
<ul>
{% for subkey, subvalue in value %}
<li>{{ subkey }}: {{ subvalue }}</li>
{% endfor %}
</ul>
{% else %}
<li>{{ key }}: {{ value }}</li>
{% endif %}
{% endfor %}
</ul>

View file

@ -0,0 +1,9 @@
<h1>AntCMS Admin Plugin</h1>
<p>Welcome, {{ user.name }}. Below are all of the available options for you.</p>
{% if user.role == 'admin' %}
<a href="{{ "admin/config/"|absUrl }}">AntCMS Configuration</a> |
<a href="{{ "admin/users/"|absUrl }}">AntCMS User Management</a> |
{% endif %}
<a href="{{ "admin/pages/"|absUrl }}">Page Management</a> |
<a href="{{ "profile/"|absUrl }}">Profile</a> |
<a href="{{ "profile/logout"|absUrl }}">Logout</a>

View file

@ -0,0 +1,25 @@
<h1>Page Management</h1>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/pages/regenerate/"|absUrl }}">Regenerate Page List</a> |
<a href="{{ "admin/pages/create/"|absUrl }}">Create New Page</a>
<table>
<tr>
<th>Page Title</th>
<th>Full Page Path</th>
<th>Functional Page Path</th>
<th>Show in Navbar</th>
<th>Actions</th>
</tr>
{% for page in pages %}
<tr>
<td>{{ page.pageTitle }}</td>
<td>{{ page.fullPagePath }}</td>
<td>{{ page.functionalPagePath }}</td>
<td><a href='{{ page.togglevisibility }}'>{{ page.isvisable }}</a></td>
<td>
<a href="{{ page.editurl }}">Edit Page</a> |
<a href="{{ page.deleteurl }}">Delete Page</a>
</td>
</tr>
{% endfor %}
</table>

View file

@ -0,0 +1,9 @@
<h1>Page Management</h1>
<p>Create new page</p>
<form method="post" action="{{ "admin/pages/edit"|absUrl }}">
<div style="display:flex; flex-direction: row; justify-content: center; align-items: center">
<label for="input">URL for new page: {{ BaseURL }} </label>
<input type="text" name="newpage" id="input">
<input type="submit" value="Submit" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,28 @@
<form method="POST" action="{{ "admin/users/savenew"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="password" class="block font-medium mb-2">Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="role" class="block font-medium mb-2">Role:</label>
<select name="role" id="role" multiple class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
<option value="admin">Admin</option>
<option value="writer">Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Create User" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,25 @@
<form method="POST" action="{{ "admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.name }}">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.username }}">
</div>
<div class="mb-4">
<label for="role" class="block font-medium mb-2">Role:</label>
<select name="role" id="role" multiple class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
<option value="admin" {% if user.role == 'admin' %} selected {% endif %}>Admin</option>
<option value="writer" {% if user.role == 'writer' %} selected {% endif %}>Writer</option>
</select>
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,12 @@
<form method="POST" action="{{ "admin/users/save"|absUrl }}">
<input type="hidden" name="originalusername" value="{{ user.username }}">
<div class="mb-4">
<label for="password" class="block font-medium mb-2">New Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,19 @@
<h1>AntCMS User Management</h1>
<a href="{{ "admin/"|absUrl }}">Back</a> |
<a href="{{ "admin/users/add/"|absUrl }}">Add User</a>
<table>
<tr>
<th>Display Name</th>
<th>Username</th>
<th>Role</th>
<th>Actions</th>
</tr>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.username }}</td>
<td>{{ user.role }}</td>
<td><a href="{{ ("admin/users/edit/" ~ user.username) |absUrl }}">Edit</a> | <a href="{{ ("admin/users/resetpassword/" ~ user.username) |absUrl }}">Reset Password</a></td>
</tr>
{% endfor %}
</table>

View file

@ -0,0 +1,81 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ AntCMSDescription }}">
<meta name="author" content="{{ AntCMSAuthor }}">
<meta name="keywords" content="{{ AntCMSKeywords }}">
<link href="{{ "Themes/Default/Assets/Dist/tailwind.css"|absUrl }}" rel="stylesheet">
<script src="{{ "Themes/Default/Assets/Dist/TinyZoom.js"|absUrl }}" defer></script>
<style>
.fullscreen-image {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
backdrop-filter: blur(5px);
}
</style>
<title>{{ AntCMSTitle }}</title>
</head>
<body class="bg-gray-50 dark:bg-zinc-800 text-gray-500 dark:text-gray-400">
<!-- Navigation -->
<nav class="p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">
<div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="{{ ""|absUrl }}" class="flex items-center">
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">{{ AntCMSSiteTitle}}</span>
</a>
<button data-collapse-toggle="navbar-solid-bg" type="button"
class="inline-flex items-center p-2 ml-3 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-solid-bg" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"></path>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-solid-bg">
<ul
class="flex flex-col mt-4 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-transparent dark:bg-zinc-800 md:dark:bg-transparent dark:border-gray-700">
<!--AntCMS-Navigation-->
</ul>
</div>
</div>
</nav>
<!-- Content -->
<div class="container mx-auto my-4">
<div class="flex flex-wrap -mx-4">
<div class="w-1/12"></div>
<div class="w-10/12 prose dark:prose-invert max-w-none">{{ AntCMSBody | raw }}</div>
<div class="w-1/12"></div>
</div>
</div>
<footer class="text-center text-lg-start">
<div class="text-center p-3 bg-gray-100 dark:bg-zinc-900">
Powered by
<a href="https://antcms.org"
class="text-blue-500 dark:text-blue-400 hover:text-blue-400 dark:hover:text-blue-500">AntCMS</a>
<!--AntCMS-Debug-->
</div>
</footer>
<script src="https://unpkg.com/flowbite@1.6.0/dist/flowbite.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,31 @@
<div class="container mx-auto">
<div class="flex">
<div class="w-1/2">
<h3 class="text-2xl font-bold">Page Content</h3>
<form action="{{ AntCMSActionURL }}" method="post">
<textarea id="markdown-input" name="textarea" rows="100" style="width: 100%; height: 100%;"
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">{{ AntCMSTextAreaContent }}</textarea>
<input type="submit" value="Save" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</form>
</div>
<div class="w-1/2">
<h3 class="text-2xl font-bold">Page Preview</h3>
<div id="markdown-output"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const input = document.getElementById("markdown-input");
const output = document.getElementById("markdown-output");
function parseMarkdown() {
const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, "");
output.innerHTML = marked.parse(inputValue);
}
parseMarkdown();
input.addEventListener("input", parseMarkdown);
</script>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
{% for page in pages %}
{% if page.active %}
<li>
<a class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-200 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent" href="{{ page.url }}">{{ page.pageTitle }}</a>
</li>
{% else %}
<li>
<a class="block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent" href="{{ page.url }}">{{ page.pageTitle }}</a>
</li>
{% endif %}
{% endfor %}

View file

@ -0,0 +1,15 @@
<form method="POST" action="{{ "profile/save"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.name }}">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700" value="{{ user.username }}">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,23 @@
<h2 class="text-2xl font-bold mb-4">AntCMS First Time User Setup</h2>
<p class="mb-4">Please fill out the following form to create your new administrator account. This will allow you to access portions of AntCMS that require authentication, such as the <a href="{{ "admin/"|absUrl }}" class="text-blue-500 hover:text-blue-400 dark:text-blue-400 dark:hover:text-blue-500">Admin Panel</a>.</p>
<form method="POST" action="{{ "profile/submitfirst"|absUrl }}">
<div class="mb-4">
<label for="display-name" class="block font-medium mb-2">Display Name:</label>
<input type="text" id="display-name" name="display-name" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="username" class="block font-medium mb-2">Username:</label>
<input type="text" id="username" name="username" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4">
<label for="password" class="block font-medium mb-2">Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Create User" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,5 @@
<h1>AntCMS Profile Plugin</h1>
<p>Welcome, {{ user.name }}. Below are all of the available options for you.</p>
<a href="{{ "profile/edit"|absUrl }}">Edit Profile</a> |
<a href="{{ "profile/resetpassword"|absUrl }}">Reset Password</a> |
<a href="{{ "profile/logout"|absUrl }}">Logout</a>

View file

@ -0,0 +1,10 @@
<form method="POST" action="{{ "profile/save"|absUrl }}">
<div class="mb-4">
<label for="password" class="block font-medium mb-2">New Password:</label>
<input type="password" id="password" name="password" required class="border-gray-400 border-2 rounded-lg w-full py-2 px-3 text-gray-700">
</div>
<div class="mb-4 mt-4">
<input type="submit" value="Save Changes" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">
</div>
</form>

View file

@ -0,0 +1,7 @@
<form action="{{ AntCMSActionURL }}" method="post">
<textarea name="textarea" rows="25" cols="75" type="text"
class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">{{ AntCMSTextAreaContent }}</textarea>
<div class="flex justify-end">
<button type="submit" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded-lg">Save</button>
</div>
</form>

View file

@ -1,55 +0,0 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<!--AntCMS-Description-->">
<meta name="author" content="<!--AntCMS-Author-->">
<meta name="keywords" content="<!--AntCMS-Keywords-->">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
crossorigin="anonymous"></script>
<title><!--AntCMS-Title--></title>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="<!--AntCMS-SiteLink-->"><!--AntCMS-SiteTitle--></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<!--AntCMS-Navigation-->
</ul>
</div>
</div>
</nav>
<!-- Content -->
<div class="container">
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-10"><!--AntCMS-Body--></div>
<div class="col-md-1"></div>
</div>
</div>
<footer class="text-center text-lg-start">
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2);">
Powered by
<a href="https://github.com/BelleNottelling/AntCMS/">AntCMS</a>
<!--AntCMS-Debug-->
</div>
</footer>
</body>
</html>

View file

@ -1,3 +0,0 @@
<li class="nav-item active">
<a class="nav-link" href="<!--AntCMS-PageLink-->"><!--AntCMS-PageTitle--></a>
</li>

File diff suppressed because it is too large Load diff

View file

@ -1,54 +0,0 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<!--AntCMS-Description-->">
<meta name="author" content="<!--AntCMS-Author-->">
<meta name="keywords" content="<!--AntCMS-Keywords-->">
<link href="/Themes/Tailwind/Assets/Dist/tailwind.css" rel="stylesheet">
<title><!--AntCMS-Title--></title>
</head>
<body class="bg-gray-50 dark:bg-zinc-800 text-gray-500 dark:text-gray-400">
<!-- Navigation -->
<nav class="p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">
<div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="<!--AntCMS-SiteLink-->" class="flex items-center">
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white"><!--AntCMS-SiteTitle--></span>
</a>
<button data-collapse-toggle="navbar-solid-bg" type="button" class="inline-flex items-center p-2 ml-3 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-solid-bg" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-solid-bg">
<ul class="flex flex-col mt-4 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-transparent dark:bg-zinc-800 md:dark:bg-transparent dark:border-gray-700">
<!--AntCMS-Navigation-->
</ul>
</div>
</div>
</nav>
<!-- Content -->
<div class="container mx-auto px-4 my-4">
<div class="flex flex-wrap -mx-4">
<div class="w-1/12 px-4"></div>
<div class="w-10/12 px-4 prose dark:prose-invert"><!--AntCMS-Body--></div>
<div class="w-1/12 px-4"></div>
</div>
</div>
<footer class="text-center text-lg-start">
<div class="text-center p-3 bg-gray-100 dark:bg-zinc-900">
Powered by
<a href="https://github.com/BelleNottelling/AntCMS/" class="text-blue-500 dark:text-blue-400">AntCMS</a>
<!--AntCMS-Debug-->
</div>
</footer>
<script src="https://unpkg.com/flowbite@1.6.0/dist/flowbite.min.js"></script>
</body>
</html>

View file

@ -1,3 +0,0 @@
<li>
<a href="<!--AntCMS-PageLink-->" class="block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"><!--AntCMS-PageTitle--></a>
</li>

10
src/cron.php Normal file
View file

@ -0,0 +1,10 @@
<?php
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
$classMapPath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'classMap.php';
$loader = new AntCMS\AntLoader(['path' => $classMapPath]);
$loader->addNamespace('AntCMS\\', __DIR__ . DIRECTORY_SEPARATOR . 'AntCMS');
$loader->checkClassMap();
$loader->register();
AntCMS\AntCache::clearCache();

View file

@ -3,55 +3,73 @@
error_reporting(E_ALL);
ini_set('display_errors', '1');
const AntDir = __DIR__;
const AntCachePath = __DIR__ . '/Cache';
const antConfigFile = __DIR__ . '/config.yaml';
const antPagesList = __DIR__ . '/pages.yaml';
const antContentPath = __DIR__ . '/Content';
const antThemePath = __DIR__ . '/Themes';
require_once __DIR__ . '/Vendor/autoload.php';
require_once __DIR__ . '/Autoload.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Constants.php';
$classMapPath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'classMap.php';
$loader = new AntCMS\AntLoader(['path' => $classMapPath]);
$loader->addNamespace('AntCMS\\', __DIR__ . DIRECTORY_SEPARATOR . 'AntCMS');
$loader->checkClassMap();
$loader->register();
use AntCMS\AntCMS;
use AntCMS\AntConfig;
use AntCMS\AntPages;
$antCms = new AntCMS();
if(!file_exists(antConfigFile)){
if (!file_exists(antConfigFile)) {
AntConfig::generateConfig();
}
if(!file_exists(antPagesList)){
AntPages::generatePages();
if (!file_exists(antPagesList)) {
\AntCMS\AntPages::generatePages();
}
$currentConfg = AntConfig::currentConfig();
$antCms = new AntCMS();
if ($currentConfg['forceHTTPS'] && 'cli' !== PHP_SAPI){
$isHTTPS = false;
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
$isHTTPS = true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') {
$isHTTPS = true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) !== 'off') {
$isHTTPS = true;
}
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$baseUrl = AntConfig::currentConfig('baseURL');
$antRouting = new \AntCMS\AntRouting($baseUrl, $requestUri);
if(!$isHTTPS){
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('Location: ' . $url);
exit;
}
if (AntConfig::currentConfig('forceHTTPS') && !\AntCMS\AntEnviroment::isCli()) {
$antRouting->redirectHttps();
}
$requestedPage = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$indexes = ['/', '/index.php', '/index.html'];
if (in_array($requestedPage, $indexes)) {
$antCms->renderPage('/');
if ($antRouting->checkMatch('/themes/*/assets')) {
$antCms->serveContent(AntDir . $requestUri);
}
if ($antRouting->checkMatch('/.well-known/acme-challenge/*')) {
$antCms->serveContent(AntDir . $requestUri);
}
if ($antRouting->checkMatch('/sitemap.xml')) {
$antRouting->setRequestUri('/plugin/sitemap');
}
if ($antRouting->checkMatch('/robots.txt')) {
$antRouting->setRequestUri('/plugin/robotstxt');
}
if ($antRouting->checkMatch('/admin/*')) {
$antRouting->requestUriUnshift('plugin');
}
if ($antRouting->checkMatch('/profile/*')) {
$antRouting->requestUriUnshift('plugin');
}
if ($antRouting->checkMatch('/plugin/*')) {
$antRouting->routeToPlugin();
}
if ($antRouting->isIndex()) {
// If the users list hasn't been created, redirect to the first-time setup
if (!file_exists(antUsersList)) {
AntCMS::redirect('/profile/firsttime');
}
echo $antCms->renderPage('/');
exit;
} else {
$antCms->renderPage($requestedPage);
echo $antCms->renderPage($requestUri);
exit;
}

View file

@ -1,10 +1,35 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/Theme/Default/Templates/*.{html,js}", "./src/AntCMS/*.php"], // Including the PHP files because some of the components are generated from PHP files
content: ["./src/Themes/Default/Templates/*.{twig,html,js}"],
theme: {
extend: {},
extend: {
typography ({ theme }) {
return {
DEFAULT: {
css: {
'code::before': {
content: 'none',
},
'code::after': {
content: 'none'
},
code: {
backgroundColor: theme('colors.zinc.700'),
color: theme('colors.zinc.100'),
paddingLeft: theme('spacing[1.5]'),
paddingRight: theme('spacing[1.5]'),
paddingTop: theme('spacing.1'),
paddingBottom: theme('spacing.1'),
borderRadius: theme('borderRadius.DEFAULT'),
},
}
},
}
}
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}

84
tests/CMSTest.php Normal file
View file

@ -0,0 +1,84 @@
<?php
use AntCMS\AntCMS;
use AntCMS\AntPages;
use PHPUnit\Framework\TestCase;
include_once 'Includes' . DIRECTORY_SEPARATOR . 'Include.php';
class CMSTest extends TestCase
{
public function testgetSiteInfo()
{
$siteInfo = AntCMS::getSiteInfo();
$this->assertIsArray($siteInfo);
$this->assertArrayHasKey('siteTitle', $siteInfo);
$this->assertEquals('AntCMS', $siteInfo['siteTitle']);
}
public function testRenderPage()
{
AntPages::generatePages();
$antCMS = new AntCMS;
$pagePath = '/index.md';
$result = $antCMS->renderPage($pagePath);
$this->assertNotEmpty($result);
$this->assertIsString($result);
}
public function testGetPageLayout()
{
//We need to generate the Pages.yaml file so that the nav list can be generated.
AntPages::generatePages();
$antCMS = new AntCMS;
$pageLayout = $antCMS->getPageLayout();
$this->assertNotEmpty($pageLayout);
$this->assertIsString($pageLayout);
}
public function testGetPage()
{
$antCMS = new AntCMS;
$result = $antCMS->getPage('/index.md');
$this->assertNotEmpty($result);
$this->assertIsArray($result);
$this->assertArrayHasKey('content', $result);
$this->assertArrayHasKey('title', $result);
$this->assertArrayHasKey('author', $result);
$this->assertArrayHasKey('description', $result);
$this->assertArrayHasKey('keywords', $result);
}
public function testGetPageFailed()
{
$antCMS = new AntCMS;
$result = $antCMS->getPage('/thisdoesnotexist.md');
$this->assertEquals(false, $result);
$this->assertIsBool($result);
}
public function testGetThemeTemplate()
{
$antCMS = new AntCMS;
$result = $antCMS->getThemeTemplate();
$this->assertNotEmpty($result);
$this->assertIsString($result);
}
public function testGetThemeTemplateFallback()
{
$antCMS = new AntCMS;
$result = $antCMS->getThemeTemplate('atemplatethatjusdoesntexist');
$this->assertNotEmpty($result);
$this->assertIsString($result);
}
}

55
tests/ConfigTest.php Normal file
View file

@ -0,0 +1,55 @@
<?php
use AntCMS\AntConfig;
use PHPUnit\Framework\TestCase;
include_once 'Includes' . DIRECTORY_SEPARATOR . 'Include.php';
class ConfigTest extends TestCase
{
public function testGetConfig()
{
$config = AntConfig::currentConfig();
$expectedKeys = array(
'siteInfo',
'forceHTTPS',
'activeTheme',
'cacheMode',
'debug',
'baseURL'
);
foreach ($expectedKeys as $expectedKey) {
$this->assertArrayHasKey($expectedKey, $config, "Expected key '{$expectedKey}' not found in config array");
}
}
public function testSaveConfigFailed()
{
$Badconfig = [
'cacheMode' => 'none',
];
try {
$result = AntConfig::saveConfig($Badconfig);
} catch (Exception $exception) {
$result = $exception;
}
$this->assertNotTrue($result);
}
public function testSaveConfigPassed()
{
$currentConfig = AntConfig::currentConfig();
try {
$result = AntConfig::saveConfig($currentConfig);
} catch (Exception $exception) {
$result = $exception;
}
$this->assertTrue($result);
}
}

View file

@ -0,0 +1,9 @@
siteInfo:
siteTitle: 'AntCMS'
forceHTTPS: true
activeTheme: Default
cacheMode: auto
debug: true
baseURL: antcms.org/
embed:
allowed_domains: [youtube.com, twitter.com, github.com, vimeo.com, flickr.com, instagram.com, facebook.com]

View file

@ -0,0 +1,11 @@
<?php
$basedir = dirname(__DIR__, 2);
$srcdir = $basedir . DIRECTORY_SEPARATOR . 'src';
include_once $srcdir . DIRECTORY_SEPARATOR . 'Constants.php';
$classMapPath = $srcdir . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'classMap.php';
$loader = new AntCMS\AntLoader(['path' => $classMapPath]);
$loader->addNamespace('AntCMS\\', $srcdir . DIRECTORY_SEPARATOR . 'AntCMS');
$loader->checkClassMap();
$loader->register();

81
tests/MarkdownTest.php Normal file
View file

@ -0,0 +1,81 @@
<?php
use AntCMS\AntMarkdown;
use AntCMS\AntConfig;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\Callback;
include_once 'Includes' . DIRECTORY_SEPARATOR . 'Include.php';
class MarkdownTest extends TestCase
{
public function testCanRenderMarkdown()
{
$result = trim(AntMarkdown::renderMarkdown("# Test Content!"));
$this->assertEquals('<h1>Test Content!</h1>', $result);
}
public function testMarkdownIsFast()
{
$markdown = file_get_contents(antContentPath . DIRECTORY_SEPARATOR . 'index.md');
$totalTime = 0;
$currentConfig = AntConfig::currentConfig();
//Ensure cache is enabled
$currentConfig['cacheMode'] = 'auto';
AntConfig::saveConfig($currentConfig);
for ($i = 0; $i < 10; ++$i) {
$start = microtime(true);
AntMarkdown::renderMarkdown($markdown);
$end = microtime(true);
$totalTime += $end - $start;
}
$averageTime = $totalTime / 10;
$callback = new Callback(static function ($averageTime) {
return $averageTime < 0.015;
});
$this->assertThat($averageTime, $callback, 'AntMarkdown::renderMarkdown took too long on average!');
}
public function testMarkdownCacheWorks()
{
$markdown = file_get_contents(antContentPath . DIRECTORY_SEPARATOR . 'index.md');
$currentConfig = AntConfig::currentConfig();
//Disable cache
$currentConfig['cacheMode'] = 'none';
AntConfig::saveConfig($currentConfig);
$totalTime = 0;
for ($i = 0; $i < 10; ++$i) {
$start = microtime(true);
AntMarkdown::renderMarkdown($markdown);
$end = microtime(true);
$totalTime += $end - $start;
}
$withoutCache = $totalTime / 10;
//Enable cache
$currentConfig['cacheMode'] = 'auto';
AntConfig::saveConfig($currentConfig);
$totalTime = 0;
for ($i = 0; $i < 10; ++$i) {
$start = microtime(true);
AntMarkdown::renderMarkdown($markdown);
$end = microtime(true);
$totalTime += $end - $start;
}
$withCache = $totalTime / 10;
echo "\n Markdown rendering speed with cache: {$withCache} VS without: {$withoutCache} \n\n";
$this->assertLessThan($withoutCache, $withCache, "Cache didn't speed up rendering!");
}
}

30
tests/PagesTest.php Normal file
View file

@ -0,0 +1,30 @@
<?php
use AntCMS\AntPages;
use AntCMS\AntCMS;
use PHPUnit\Framework\TestCase;
include_once 'Includes' . DIRECTORY_SEPARATOR . 'Include.php';
class PagesTest extends TestCase
{
public function testGetGenerateAndGetPages()
{
AntPages::generatePages();
$result = AntPages::getPages();
$this->assertNotEmpty($result);
$this->assertIsArray($result);
}
public function testGetNavigation(){
$antCMS = new AntCMS;
$pageTemplate = $antCMS->getThemeTemplate();
$navLayout = $antCMS->getThemeTemplate('nav');
$result = AntPages::generateNavigation($navLayout, $pageTemplate);
$this->assertNotEmpty($result);
$this->assertIsString($result);
}
}

80
tests/ToolsTest.php Normal file
View file

@ -0,0 +1,80 @@
<?php
use AntCMS\AntTools;
use PHPUnit\Framework\TestCase;
include_once 'Includes' . DIRECTORY_SEPARATOR . 'Include.php';
class ToolsTest extends TestCase
{
public function testPathRepair()
{
$badPaths = array(
"path/to/file",
"path\\to\\file",
"/path/to/file",
"C:\\path\\to\\file",
"~/path/to/file"
);
$expectedPaths = array(
"path" . DIRECTORY_SEPARATOR . "to" . DIRECTORY_SEPARATOR . "file",
"path" . DIRECTORY_SEPARATOR . "to" . DIRECTORY_SEPARATOR . "file", DIRECTORY_SEPARATOR . "path" . DIRECTORY_SEPARATOR . "to" . DIRECTORY_SEPARATOR . "file",
"C:" . DIRECTORY_SEPARATOR . "path" . DIRECTORY_SEPARATOR . "to" . DIRECTORY_SEPARATOR . "file",
"~" . DIRECTORY_SEPARATOR . "path" . DIRECTORY_SEPARATOR . "to" . DIRECTORY_SEPARATOR . "file"
);
foreach ($badPaths as $index => $badPath) {
$goodPath = AntTools::repairFilePath($badPath);
$this->assertEquals($expectedPaths[$index], $goodPath, "Expected '$expectedPaths[$index]' but got '{$goodPath}' for input '{$badPath}'");
}
}
public function testUrlRepair()
{
$badUrls = array(
"example.com\path",
"example.com/path/",
"example.com//path",
"example.com/path/to//file",
"example.com\path\\to\\file",
"example.com\path\\to\\file?download=yes"
);
$expectedUrls = array(
"example.com/path",
"example.com/path/",
"example.com/path",
"example.com/path/to/file",
"example.com/path/to/file",
"example.com/path/to/file?download=yes"
);
foreach ($badUrls as $index => $badurl) {
$goodUrl = AntTools::repairURL($badurl);
$this->assertEquals($expectedUrls[$index], $goodUrl, "Expected '$expectedUrls[$index]' but got '{$goodUrl}' for input '{$badurl}'");
}
}
public function testGetFileList()
{
$basedir = dirname(__DIR__, 1);
$srcdir = $basedir . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Content';
$result = AntTools::getFileList($srcdir);
$this->assertNotEmpty($result);
}
public function testGetFileListWithExtension()
{
$basedir = dirname(__DIR__, 1);
$srcdir = $basedir . DIRECTORY_SEPARATOR . 'src';
$files = AntTools::getFileList($srcdir, 'md');
foreach ($files as $file) {
$this->assertEquals('md', pathinfo($file, PATHINFO_EXTENSION), "Expected file extension to be 'md', but got '" . pathinfo($file, PATHINFO_EXTENSION) . "' for file '{$file}'");
}
}
}