Compare commits
165 commits
latest-pre
...
main
Author | SHA1 | Date | |
---|---|---|---|
77a06947db | |||
d73d3180b7 | |||
1eae77f27c | |||
0a52b31559 | |||
eb19768d6a | |||
a06956f164 | |||
ae629ccebd | |||
be214fbc94 | |||
8eb49cda0b | |||
50d181373a | |||
7d02c1a2a1 | |||
5c0bb8d1af | |||
5041f824df | |||
57bb266635 | |||
c613271c48 | |||
8d9e894af8 | |||
d920b26f52 | |||
ccf05911ef | |||
ebb0fce492 | |||
945881365e | |||
38e51166c3 | |||
b61ac80227 | |||
fa87852dc5 | |||
d03ec564f6 | |||
95c2774222 | |||
18e83c915a | |||
e01a00d638 | |||
b31e88a428 | |||
e6a59c0b16 | |||
a58113087f | |||
3239a19245 | |||
480ab7185d | |||
2e737545ad | |||
79eef0aa46 | |||
0cff3b92df | |||
8e018600a5 | |||
0663fbc604 | |||
c084208db6 | |||
1770d5376a | |||
226d100673 | |||
0e252133e2 | |||
90f0f96f13 | |||
70ff0a106f | |||
acb00af6f7 | |||
61fa0fc533 | |||
23e8b18ba3 | |||
cba9f71f78 | |||
404c093ccb | |||
a653154f85 | |||
d08d136990 | |||
25e1ef9434 | |||
94f251fc3f | |||
29ef7ec779 | |||
d262f22c98 | |||
3ce0896af5 | |||
9eb24fee30 | |||
af375a5499 | |||
e1667b6374 | |||
e2ca10d51d | |||
95ed5059f0 | |||
686caf149b | |||
7ea9cb2e5d | |||
a3039f10ed | |||
ecf94d36da | |||
97e6a8a4e6 | |||
ea7f3ac65b | |||
bf3b425c8d | |||
612adb5a17 | |||
b2036b765f | |||
08cfdcf460 | |||
02e4492ecd | |||
f388c99647 | |||
0eca5dd3e1 | |||
cbb978ccad | |||
cdee7c8a04 | |||
4386451516 | |||
588811ec7a | |||
ff9583bfdf | |||
81824132fe | |||
fcd9e31af9 | |||
c90823afbe | |||
5669e8a429 | |||
8ed69831ee | |||
b8956614b0 | |||
c5b999294b | |||
3353be4920 | |||
91395db9c4 | |||
eaca96fd6b | |||
ca54a56eaa | |||
44de365946 | |||
4f96f5c496 | |||
325221f527 | |||
36ab96bf8c | |||
a2f8c3617a | |||
0979d4ee03 | |||
ab5a46a4cd | |||
891012f6ff | |||
c513b01bad | |||
584f99bac7 | |||
016103f7cd | |||
506e66c0ae | |||
7adb5d51a6 | |||
4c0a950179 | |||
c15bf1f449 | |||
aff54916e9 | |||
e7e7628d91 | |||
da69eb3f81 | |||
22007f4c57 | |||
cb0a1917a9 | |||
2a1af73f75 | |||
3405291db1 | |||
51211d87ec | |||
52e102e63b | |||
89b182b3be | |||
9510bf2e9b | |||
fa1d942d4e | |||
d4331326f6 | |||
ff68feeae6 | |||
55e20d3f25 | |||
b60439a9a9 | |||
37a0f2ffcd | |||
1f4b40a787 | |||
2acfc2d50d | |||
5df8872a62 | |||
22efe315c4 | |||
757b1b3ade | |||
e09d4ca60d | |||
1dc25e2d32 | |||
074cc47625 | |||
073ee79eae | |||
930766b1a3 | |||
d31da240c4 | |||
6d3f8a3da1 | |||
d6588573b0 | |||
7739b2875d | |||
6fe8dd90cd | |||
6b0a424a52 | |||
2de92731cf | |||
90a31b2941 | |||
457cce525b | |||
0c79ec0ba6 | |||
dbf2600afb | |||
8d68293798 | |||
f5844cb2cc | |||
ca005bd074 | |||
47d86d8b6d | |||
f86b8a988b | |||
1d65fca192 | |||
d1fd6521f9 | |||
3ee0161c11 | |||
e96967f604 | |||
b953821dd2 | |||
a36755bb80 | |||
ee92c2f058 | |||
906c88273f | |||
7af26306f6 | |||
4d745a6c9d | |||
ce656562e9 | |||
5562f9b676 | |||
c89c82fb64 | |||
a198f3995e | |||
f5195315ca | |||
d153dc9a46 | |||
44dcbb7cfe | |||
5670529b11 |
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
@ -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"
|
||||
|
|
3
.github/workflows/phpstan.yml
vendored
3
.github/workflows/phpstan.yml
vendored
|
@ -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
|
||||
|
|
15
.github/workflows/preview.yml
vendored
15
.github/workflows/preview.yml
vendored
|
@ -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
26
.github/workflows/unittests.yml
vendored
Normal 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
4
.gitignore
vendored
|
@ -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
201
LICENCE.md
Normal 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.
|
|
@ -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
3592
composer.lock
generated
File diff suppressed because it is too large
Load diff
8
configs/lighttpd.conf
Normal file
8
configs/lighttpd.conf
Normal 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
10
configs/nginx.conf
Normal 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
998
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,6 @@ parameters:
|
|||
excludePaths:
|
||||
analyse:
|
||||
- src/Vendor
|
||||
- src/Cache
|
||||
- src/AntCMS/AntKeywords.php
|
||||
|
||||
|
|
63
readme.md
63
readme.md
|
@ -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
35
rector.php
Normal 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,
|
||||
]);
|
||||
};
|
|
@ -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
96
src/AntCMS/AntAuth.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
11
src/AntCMS/AntEnviroment.php
Normal file
11
src/AntCMS/AntEnviroment.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace AntCMS;
|
||||
|
||||
class AntEnviroment
|
||||
{
|
||||
public static function isCli(): bool
|
||||
{
|
||||
return (php_sapi_name() === 'cli' || !http_response_code());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
18
src/AntCMS/AntPlugin.php
Normal 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();
|
||||
}
|
26
src/AntCMS/AntPluginLoader.php
Normal file
26
src/AntCMS/AntPluginLoader.php
Normal 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
129
src/AntCMS/AntRouting.php
Normal 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
79
src/AntCMS/AntTools.php
Normal 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
48
src/AntCMS/AntTwig.php
Normal 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);
|
||||
}
|
||||
}
|
23
src/AntCMS/AntTwigFilters.php
Normal file
23
src/AntCMS/AntTwigFilters.php
Normal 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
130
src/AntCMS/AntUsers.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
0
src/Config/.gitkeep
Normal file
13
src/Constants.php
Normal file
13
src/Constants.php
Normal 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);
|
||||
}
|
132
src/Content/docs/features.md
Normal file
132
src/Content/docs/features.md
Normal 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
|
65
src/Content/docs/gettingstarted.md
Normal file
65
src/Content/docs/gettingstarted.md
Normal 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!
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
|
@ -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.
|
|
@ -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.
|
328
src/Plugins/Admin/AdminPlugin.php
Normal file
328
src/Plugins/Admin/AdminPlugin.php
Normal 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';
|
||||
}
|
||||
}
|
119
src/Plugins/Profile/ProfilePlugin.php
Normal file
119
src/Plugins/Profile/ProfilePlugin.php
Normal 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';
|
||||
}
|
||||
}
|
28
src/Plugins/Robotstxt/RobotstxtPlugin.php
Normal file
28
src/Plugins/Robotstxt/RobotstxtPlugin.php
Normal 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';
|
||||
}
|
||||
}
|
55
src/Plugins/Sitemap/SitemapPlugin.php
Normal file
55
src/Plugins/Sitemap/SitemapPlugin.php
Normal 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';
|
||||
}
|
||||
}
|
14
src/Themes/Bootstrap/Config.yaml
Normal file
14
src/Themes/Bootstrap/Config.yaml
Normal 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:
|
|
@ -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>
|
28
src/Themes/Bootstrap/Templates/admin/admin_userAdd.html.twig
Normal file
28
src/Themes/Bootstrap/Templates/admin/admin_userAdd.html.twig
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
78
src/Themes/Bootstrap/Templates/default.html.twig
Normal file
78
src/Themes/Bootstrap/Templates/default.html.twig
Normal 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>
|
31
src/Themes/Bootstrap/Templates/markdownEdit.html.twig
Normal file
31
src/Themes/Bootstrap/Templates/markdownEdit.html.twig
Normal 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>
|
11
src/Themes/Bootstrap/Templates/nav.html.twig
Normal file
11
src/Themes/Bootstrap/Templates/nav.html.twig
Normal 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 %}
|
|
@ -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>
|
|
@ -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>
|
5
src/Themes/Bootstrap/Templates/textareaEdit.html.twig
Normal file
5
src/Themes/Bootstrap/Templates/textareaEdit.html.twig
Normal 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>
|
1
src/Themes/Default/Assets/Dist/TinyZoom.js
Normal file
1
src/Themes/Default/Assets/Dist/TinyZoom.js
Normal 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")});
|
1
src/Themes/Default/Assets/Dist/tailwind.css
Normal file
1
src/Themes/Default/Assets/Dist/tailwind.css
Normal file
File diff suppressed because one or more lines are too long
6
src/Themes/Default/Config.yaml
Normal file
6
src/Themes/Default/Config.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
config:
|
||||
showAuthor: false
|
||||
|
||||
defaultAttributes:
|
||||
\League\CommonMark\Extension\CommonMark\Node\Inline\Image:
|
||||
class: TinyZoom
|
17
src/Themes/Default/Templates/admin/admin_config.html.twig
Normal file
17
src/Themes/Default/Templates/admin/admin_config.html.twig
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
28
src/Themes/Default/Templates/admin/admin_userAdd.html.twig
Normal file
28
src/Themes/Default/Templates/admin/admin_userAdd.html.twig
Normal 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>
|
25
src/Themes/Default/Templates/admin/admin_userEdit.html.twig
Normal file
25
src/Themes/Default/Templates/admin/admin_userEdit.html.twig
Normal 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>
|
|
@ -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>
|
19
src/Themes/Default/Templates/admin/admin_users.html.twig
Normal file
19
src/Themes/Default/Templates/admin/admin_users.html.twig
Normal 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>
|
81
src/Themes/Default/Templates/default.html.twig
Normal file
81
src/Themes/Default/Templates/default.html.twig
Normal 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>
|
31
src/Themes/Default/Templates/markdownEdit.html.twig
Normal file
31
src/Themes/Default/Templates/markdownEdit.html.twig
Normal 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>
|
11
src/Themes/Default/Templates/nav.html.twig
Normal file
11
src/Themes/Default/Templates/nav.html.twig
Normal 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 %}
|
15
src/Themes/Default/Templates/profile/profile_edit.html.twig
Normal file
15
src/Themes/Default/Templates/profile/profile_edit.html.twig
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
7
src/Themes/Default/Templates/textareaEdit.html.twig
Normal file
7
src/Themes/Default/Templates/textareaEdit.html.twig
Normal 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>
|
|
@ -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>
|
|
@ -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
|
@ -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>
|
|
@ -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
10
src/cron.php
Normal 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();
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
84
tests/CMSTest.php
Normal 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
55
tests/ConfigTest.php
Normal 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);
|
||||
}
|
||||
}
|
9
tests/Includes/Config.yaml
Normal file
9
tests/Includes/Config.yaml
Normal 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]
|
11
tests/Includes/Include.php
Normal file
11
tests/Includes/Include.php
Normal 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
81
tests/MarkdownTest.php
Normal 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
30
tests/PagesTest.php
Normal 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
80
tests/ToolsTest.php
Normal 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}'");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue