diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..e3ea689
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,58 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: 'type:bug,status:needs-verification'
+assignees: ''
+
+---
+
+# Describe the bug
+
+A clear and concise description of what the bug is.
+
+## To Reproduce
+
+Steps to reproduce the behavior:
+
+1. Go to '...'
+
+2. Click on '....'
+
+3. Scroll down to '....'
+
+4. See error
+
+## Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+## Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+## Desktop (please complete the following information)
+
+- OS: [e.g. iOS]
+
+- Browser [e.g. chrome, safari]
+
+- Version [e.g. 22]
+
+## Smartphone (please complete the following information)
+
+- Device: [e.g. iPhone6]
+
+- OS: [e.g. iOS8.1]
+
+- Browser [e.g. stock browser, safari]
+
+- Version [e.g. 22]
+
+## Additional context
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..8929937
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,33 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: 'type:enhancement'
+assignees: ''
+
+---
+
+
+# Feature Request
+
+## Is your feature request related to a problem? Please describe
+
+A clear and concise description of what the problem is. Ex. I'm always
+frustrated when [...]
+
+## Describe the solution you'd like
+
+A clear and concise description of what you want to happen.
+
+## Describe alternatives you've considered
+
+A clear and concise description of any alternative solutions or features you've
+considered.
+
+## Additional context
+
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..1e05722
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,40 @@
+
+
+## Description
+
+
+## Related Issue
+
+
+
+
+
+## Motivation and Context
+
+
+## How Has This Been Tested?
+
+
+
+
+## Screenshots (if appropriate):
+
+
+## Types of changes
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to change)
+
+## Checklist:
+
+
+- [ ] My code follows the code style of this project.
+- [ ] My change requires a change to the documentation.
+- [ ] I have updated the documentation repository accordingly.
+
+
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 1500ef8..ad27f2e 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -130,6 +130,17 @@ print '
';
Document all changes in `CHANGELOG.md`; use descriptive commit messages referencing issue/PR numbers when applicable.
+## CI & Dependency Baselines
+
+- Do not commit a `composer.json` or `composer.lock` in this plugin's own repo root — the shared CI workflow installs Pest/dev dependencies into Cacti's own Composer-managed vendor tree (checked out alongside the plugin). Use Cacti's `composer.json`, not a plugin-local one.
+- Do not add a plugin-local `.phpstan.neon`/`phpstan.neon` or `.php-cs-fixer.php`/`.php-cs-fixer.dist.php` — lint/static-analysis steps run against Cacti's own config from the Cacti core checkout, targeting this plugin's directory. Use the Cacti version, not a plugin-local config.
+- Prefer Cacti's `cacti_count()`/`cacti_sizeof()` wrappers over the raw `count()`/`sizeof()` builtins in new or edited code.
+
+## Internationalization (i18n)
+
+- Translatable strings are managed with GNU gettext via `locales/build_gettext.sh`. `locales/po/cacti.pot` is the source template; Weblate owns syncing the per-language `.po`/`.mo` files from it.
+- When a pull request adds or changes a string wrapped in `__()`/`__n()`/`__esc()`/`__x()`/`__xn()`/`__gettext()`, run `locales/build_gettext.sh` before pushing and add the resulting change to `locales/po/cacti.pot` only. Do not commit the regenerated per-language `.po`/`.mo` files in the same PR — Weblate takes care of the rest.
+
## References
- [Cacti main repo](https://github.com/Cacti/cacti/tree/1.2.x)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..eb8868a
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,59 @@
+# +-------------------------------------------------------------------------+
+# | Copyright (C) 2004-2026 The Cacti Group |
+# +-------------------------------------------------------------------------+
+# | Cacti: The Complete RRDtool-based Graphing Solution |
+# +-------------------------------------------------------------------------+
+# | http://www.cacti.net/ |
+# +-------------------------------------------------------------------------+
+#
+# CodeQL has no PHP analysis, so this covers the plugin JavaScript only. The PHP is
+# covered by the syntax check and integration jobs in plugin-ci-workflow.yml.
+
+name: "CodeQL"
+
+on:
+ push:
+ branches: [main, develop]
+ paths-ignore:
+ - "**/*.md"
+ pull_request:
+ branches: [main, develop]
+ paths-ignore:
+ - "**/*.md"
+ schedule:
+ - cron: "30 1 * * 1"
+ workflow_dispatch:
+
+concurrency:
+ group: codeql-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ["javascript-typescript"]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
+ with:
+ languages: ${{ matrix.language }}
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml
index 0392c78..3b5082b 100644
--- a/.github/workflows/plugin-ci-workflow.yml
+++ b/.github/workflows/plugin-ci-workflow.yml
@@ -46,7 +46,7 @@ jobs:
services:
mariadb:
- image: mariadb:10.6
+ image: mariadb:11.8
env:
MYSQL_ROOT_PASSWORD: cactiroot
MYSQL_DATABASE: cacti
@@ -55,7 +55,7 @@ jobs:
ports:
- 3306:3306
options: >-
- --health-cmd="mysqladmin ping"
+ --health-cmd="mariadb-admin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
@@ -64,22 +64,22 @@ jobs:
steps:
- name: Checkout Cacti
- uses: actions/checkout@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Cacti/cacti
ref: ${{ env.CACTI }}
path: cacti
- name: Checkout quicktree Plugin
- uses: actions/checkout@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: cacti/plugins/quicktree
- name: Install PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
+ uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
- coverage: xdebug
+ coverage: pcov
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp
ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York"
@@ -90,7 +90,7 @@ jobs:
run: sudo apt-get update
- name: Install System Dependencies
- run: sudo apt-get install -y rrdtool fping
+ run: sudo apt-get install -y rrdtool fping gettext
- name: Setup Permissions
run: |
@@ -102,20 +102,23 @@ jobs:
- name: Create MySQL Config
run: |
+ echo "::add-mask::cactiroot"
echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf
- cat ~/.my.cnf
+ chmod 600 ~/.my.cnf
- name: Initialize Cacti Database
- env:
- MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf'
run: |
- mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;'
- mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';"
- mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';"
- mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';"
- mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;"
- mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql
- mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti
+ MYSQL_AUTH_USR="--defaults-file=$HOME/.my.cnf"
+ mysql "$MYSQL_AUTH_USR" -e 'CREATE DATABASE IF NOT EXISTS cacti;'
+ mysql "$MYSQL_AUTH_USR" -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';"
+ mysql "$MYSQL_AUTH_USR" -e "CREATE USER IF NOT EXISTS 'cactiuser'@'127.0.0.1' IDENTIFIED BY 'cactiuser';"
+ mysql "$MYSQL_AUTH_USR" -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';"
+ mysql "$MYSQL_AUTH_USR" -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'127.0.0.1';"
+ mysql "$MYSQL_AUTH_USR" -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';"
+ mysql "$MYSQL_AUTH_USR" -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'127.0.0.1';"
+ mysql "$MYSQL_AUTH_USR" -e "FLUSH PRIVILEGES;"
+ mysql "$MYSQL_AUTH_USR" cacti < ${{ github.workspace }}/cacti/cacti.sql
+ mysql "$MYSQL_AUTH_USR" -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti
- name: Validate composer files
run: |
@@ -134,6 +137,13 @@ jobs:
sudo composer install --dev --no-progress
fi
+ - name: Restore vendor ownership for Pest
+ run: |
+ cd ${{ github.workspace }}/cacti
+ if [ -d include/vendor ]; then
+ sudo chown -R runner:runner include/vendor composer.lock
+ fi
+
- name: Create Cacti config.php
run: |
cat ${{ github.workspace }}/cacti/include/config.php.dist | \
@@ -143,26 +153,6 @@ jobs:
sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php
sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php
- - name: Configure Apache
- run: |
- cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf
-
- ServerAdmin webmaster@localhost
- DocumentRoot GITHUB_WORKSPACE/cacti
-
-
- Options Indexes FollowSymLinks
- AllowOverride All
- Require all granted
-
-
- ErrorLog ${APACHE_LOG_DIR}/error.log
- CustomLog ${APACHE_LOG_DIR}/access.log combined
-
- EOF
- sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf
- sudo systemctl restart apache2
-
- name: Install Cacti via CLI
run: |
cd ${{ github.workspace }}/cacti
@@ -176,8 +166,19 @@ jobs:
- name: Check PHP Syntax for Plugin
run: |
cd ${{ github.workspace }}/cacti/plugins/quicktree
- if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then
- echo "Syntax errors found!"
+ find . -path './vendor' -prune -o -type f -name '*.php' -print0 | xargs -0 -r -n1 php -l
+
+ - name: Verify translation template is up to date
+ run: |
+ cd ${{ github.workspace }}/cacti/plugins/quicktree
+ sudo chmod +x locales/build_gettext.sh
+ sudo ./locales/build_gettext.sh
+ grep -v '^"POT-Creation-Date:' locales/po/cacti.pot > /tmp/cacti.pot.new
+ git checkout -- locales/
+ grep -v '^"POT-Creation-Date:' locales/po/cacti.pot > /tmp/cacti.pot.old
+ if ! cmp -s /tmp/cacti.pot.old /tmp/cacti.pot.new; then
+ echo "locales/po/cacti.pot is out of date. Run locales/build_gettext.sh and commit the updated locales/po/cacti.pot."
+ diff -u /tmp/cacti.pot.old /tmp/cacti.pot.new || true
exit 1
fi
@@ -190,12 +191,11 @@ jobs:
run: |
cd ${{ github.workspace }}/cacti
include/vendor/bin/pest --configuration=plugins/quicktree/phpunit.xml \
- --coverage-clover=plugins/quicktree/coverage/clover.xml \
- plugins/quicktree/tests
+ --coverage-clover=plugins/quicktree/coverage/clover.xml
- name: Upload coverage report
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-php${{ matrix.php }}
path: ${{ github.workspace }}/cacti/plugins/quicktree/coverage/
diff --git a/locales/build_gettext.sh b/locales/build_gettext.sh
old mode 100644
new mode 100755
index 2f4cba4..c03863a
--- a/locales/build_gettext.sh
+++ b/locales/build_gettext.sh
@@ -52,7 +52,7 @@ fi
echo "Updating Cacti language gettext language file..."
cd ${BASE_PATH}
-${XGETTEXT_BIN} --no-wrap --copyright-holder="The Cacti Group" --package-name="Cacti" --package-version=`cat include/cacti_version` --msgid-bugs-address="developers@cacti.net" -F -k__gettext -k__ -k__n:1,2 -k__x:1c,2 -k__xn:1c,2,3 -k__esc -k__esc_n:1,2 -k__esc_x:1c,2 -k__esc_xn:1c,2,3 -k__date -o locales/po/cacti.pot `find . -maxdepth 2 -name \*.php`
+${XGETTEXT_BIN} --no-wrap --copyright-holder="The Cacti Group" --package-name="Cacti" --package-version=`cat include/cacti_version` --msgid-bugs-address="developers@cacti.net" -F -k__gettext -k__ -k__n:1,2 -k__x:1c,2 -k__xn:1c,2,3 -k__esc -k__esc_n:1,2 -k__esc_x:1c,2 -k__esc_xn:1c,2,3 -k__date -o locales/po/cacti.pot `find . -maxdepth 2 -name \*.php | sort`
sed -i 's/FULL NAME /Howard Jones /g' locales/po/cacti.pot
sed -i 's/LANGUAGE /Howard Jones /g' locales/po/cacti.pot
diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot
index 192a8c4..7e8c767 100644
--- a/locales/po/cacti.pot
+++ b/locales/po/cacti.pot
@@ -8,211 +8,211 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti \n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
-"POT-Creation-Date: 2022-01-30 20:53-0500\n"
+"POT-Creation-Date: 2026-09-21 13:14-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: Cacti Developers >\n"
-"Language-Team: Cacti Developers \n"
+"Last-Translator: Howard Jones >\n"
+"Language-Team: Howard Jones \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: quicktree.php:10 quicktree.php:343
+#: quicktree.php:36 quicktree.php:357
msgid "Save To New Tree"
msgstr ""
-#: quicktree.php:11 quicktree.php:344
+#: quicktree.php:37 quicktree.php:358
msgid "Save To Branch"
msgstr ""
-#: quicktree.php:12
+#: quicktree.php:38
msgid "Clear All Graphs"
msgstr ""
-#: quicktree.php:60
+#: quicktree.php:85
msgid "Graph Added to QuickTree"
msgstr ""
-#: quicktree.php:61
+#: quicktree.php:86
#, php-format
msgid "Graph %s added to QuickTree. Goto the QuickTree page to move all Graphs to a new or existing Tree."
msgstr ""
-#: quicktree.php:64
+#: quicktree.php:89
msgid "Graph Not Added to QuickTree"
msgstr ""
-#: quicktree.php:65
+#: quicktree.php:90
#, php-format
msgid "Graph %s was NOT added to QuickTree as it's already there waiting to be placed on a Tree. Goto the QuickTree page to move all Graphs to a new or existing Tree."
msgstr ""
-#: quicktree.php:69
+#: quicktree.php:94
msgid "Graph Not Specified. Not added to QuickTree"
msgstr ""
-#: quicktree.php:70
+#: quicktree.php:95
msgid "No Graph was added to QuickTree as no Graph was specified."
msgstr ""
-#: quicktree.php:88
+#: quicktree.php:107
msgid "Click 'Continue' to Add the following Tree."
msgstr ""
-#: quicktree.php:94 quicktree.php:145
+#: quicktree.php:113 quicktree.php:157
msgid "Tree Name"
msgstr ""
-#: quicktree.php:97
+#: quicktree.php:116
msgid "New Tree"
msgstr ""
-#: quicktree.php:103 quicktree.php:162
+#: quicktree.php:122 quicktree.php:174
msgid "Branch Name"
msgstr ""
-#: quicktree.php:106 quicktree.php:165 quicktree.php:329 setup.php:129
-#: setup.php:136 setup.php:143 setup.php:150 setup.php:157 setup.php:164
+#: quicktree.php:125 quicktree.php:177 quicktree.php:343 setup.php:152
+#: setup.php:159 setup.php:166 setup.php:173 setup.php:180 setup.php:187
msgid "QuickTree"
msgstr ""
-#: quicktree.php:114 quicktree.php:173
+#: quicktree.php:133 quicktree.php:185
msgid "Add To Branch"
msgstr ""
-#: quicktree.php:114 quicktree.php:173
+#: quicktree.php:133 quicktree.php:185
msgid "Cancel"
msgstr ""
-#: quicktree.php:114 quicktree.php:173
+#: quicktree.php:133 quicktree.php:185
msgid "Continue"
msgstr ""
-#: quicktree.php:137
+#: quicktree.php:149
msgid "Click 'Continue' to Add the following Tree Branch."
msgstr ""
-#: quicktree.php:139
+#: quicktree.php:151
msgid "Add to which Graph Tree?"
msgstr ""
-#: quicktree.php:279
+#: quicktree.php:292
msgid "QuickTree has Created Tree and/or Branch and added Graphs"
msgstr ""
-#: quicktree.php:293
+#: quicktree.php:306
msgid "QuickTree has no Graphs Queued"
msgstr ""
-#: quicktree.php:332
+#: quicktree.php:346
msgid "Information/Directions"
msgstr ""
-#: quicktree.php:342
+#: quicktree.php:356
msgid "The Graphs below are Queue to be added to a Cacti Tree. You may keep them here for as long as you like, or you can perform one of the following actions"
msgstr ""
-#: quicktree.php:343
+#: quicktree.php:357
msgid "Save your selection to a new Graph Tree so you can keep them for later and work on something new."
msgstr ""
-#: quicktree.php:344
+#: quicktree.php:358
msgid "Save your selection as a branch to an existing tree so that they appear in a specific section of an existing tree."
msgstr ""
-#: quicktree.php:345
+#: quicktree.php:359
msgid "Clear all graphs"
msgstr ""
-#: quicktree.php:345
+#: quicktree.php:359
msgid "Clear the Graphs on this page from the Graphs Queue so that you have a blank QuickTree ready for new selections"
msgstr ""
-#: quicktree.php:346
+#: quicktree.php:360
msgid "You can manage the individual graphs that appear here by clicking:"
msgstr ""
-#: quicktree.php:347
+#: quicktree.php:361
msgid "Add"
msgstr ""
-#: quicktree.php:347
+#: quicktree.php:361
msgid "Graph View Page"
msgstr ""
-#: quicktree.php:347
+#: quicktree.php:361
#, php-format
msgid "This icon is next to a Graph on the %s tab."
msgstr ""
-#: quicktree.php:348
+#: quicktree.php:362
msgid "Delete"
msgstr ""
-#: quicktree.php:348
+#: quicktree.php:362
msgid "This icon next to the Graphs below to remove them from the QuickTree Queue."
msgstr ""
-#: quicktree.php:349
+#: quicktree.php:363
msgid "Note:"
msgstr ""
-#: quicktree.php:350
+#: quicktree.php:364
msgid "Adding, removing or clearing on this page does not affect any other parts of Cacti (only Creating/Saving does)"
msgstr ""
-#: quicktree.php:367
+#: quicktree.php:381
msgid "Actions"
msgstr ""
-#: quicktree.php:391
+#: quicktree.php:405
msgid "Remove This Graph From QuickTree"
msgstr ""
-#: quicktree.php:402
+#: quicktree.php:416
msgid "No Graphs Added Yet"
msgstr ""
-#: setup.php:29
+#: setup.php:35
+msgid "QuickTree Management"
+msgstr ""
+
+#: setup.php:54
msgid "Misc"
msgstr ""
-#: setup.php:33 setup.php:87
+#: setup.php:58 setup.php:112
msgid "Quicktree"
msgstr ""
-#: setup.php:37
+#: setup.php:62
msgid "Page Style"
msgstr ""
-#: setup.php:38
+#: setup.php:63
msgid "Where to display the QuickTree page"
msgstr ""
-#: setup.php:41
+#: setup.php:66
msgid "Tab"
msgstr ""
-#: setup.php:42
+#: setup.php:67
msgid "Console Menu"
msgstr ""
-#: setup.php:43
+#: setup.php:68
msgid "Both Tab and Console Menu"
msgstr ""
-#: setup.php:99
+#: setup.php:124
msgid "Add this graph to QuickTree"
msgstr ""
-#: setup.php:108
-msgid "QuickTree Tree Management"
-msgstr ""
-
-#: setup.php:111
+#: setup.php:134
msgid "Management"
msgstr ""
-#: setup.php:111
+#: setup.php:134
msgid "QuickTree Trees"
msgstr ""
diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php
deleted file mode 100644
index fb31227..0000000
--- a/tests/Security/Php74CompatibilityTest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-toBe(0, "{$f} uses str_contains");
- }
- });
-
- it('does not use str_starts_with (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- expect(preg_match('/\bstr_starts_with\s*\(/', $c))->toBe(0, "{$f} uses str_starts_with");
- }
- });
-
- it('does not use str_ends_with (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- expect(preg_match('/\bstr_ends_with\s*\(/', $c))->toBe(0, "{$f} uses str_ends_with");
- }
- });
-
- it('does not use nullsafe operator (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- expect(preg_match('/\?->/', $c))->toBe(0, "{$f} uses nullsafe operator");
- }
- });
-
- it('does not use match expression (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- // Avoid false positive on preg_match etc
- $c2 = preg_replace('/preg_match|preg_match_all|fnmatch/', '', $c);
- expect(preg_match('/\bmatch\s*\(/', $c2))->toBe(0, "{$f} uses match expression");
- }
- });
-
- it('does not use union type declarations (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- // Match function params/return with union types like string|false
- $hits = preg_match_all('/function\s+\w+\s*\([^)]*\w+\s*\|\s*\w+/', $c);
- expect($hits)->toBe(0, "{$f} uses union types in function signatures");
- }
- });
-
- it('does not use constructor property promotion (PHP 8.0)', function () use ($files) {
- foreach ($files as $f) {
- $p = realpath(__DIR__ . '/../../' . $f);
- if ($p === false) continue;
- $c = file_get_contents($p);
- if ($c === false) continue;
- expect(preg_match('/function\s+__construct\s*\([^)]*\b(public|private|protected|readonly)\s/', $c))->toBe(0,
- "{$f} uses constructor promotion"
- );
- }
- });
-});
diff --git a/tests/Security/PhpCompatibilityTest.php b/tests/Security/PhpCompatibilityTest.php
new file mode 100644
index 0000000..7237066
--- /dev/null
+++ b/tests/Security/PhpCompatibilityTest.php
@@ -0,0 +1,167 @@
+getExtension() !== 'php') {
+ continue;
+ }
+
+ $relativeFile = ltrim(str_replace($pluginRoot, '', $file->getPathname()), DIRECTORY_SEPARATOR);
+ $relativeFile = str_replace(DIRECTORY_SEPARATOR, '/', $relativeFile);
+
+ if (strpos($relativeFile, 'tests/') === 0 || strpos($relativeFile, 'vendor/') === 0 || strpos($relativeFile, 'include/vendor/') === 0) {
+ continue;
+ }
+
+ $files[] = $relativeFile;
+}
+
+sort($files);
+
+function plugin_test_read_source_file($relativeFile) {
+ $path = realpath(__DIR__ . '/../../' . $relativeFile);
+
+ if ($path === false) {
+ throw new RuntimeException("Unable to resolve required plugin source: {$relativeFile}");
+ }
+
+ $contents = file_get_contents($path);
+
+ if ($contents === false) {
+ throw new RuntimeException("Unable to read required plugin source: {$relativeFile}");
+ }
+
+ return $contents;
+}
+
+it('does not use asymmetric visibility (PHP 8.4)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/\bprivate\s*\(\s*set\s*\)|\bprotected\s*\(\s*set\s*\)/', $contents))->toBe(0,
+ "{$relativeFile} uses asymmetric visibility which requires PHP 8.4"
+ );
+ }
+});
+
+it('does not use the #[Override] attribute (PHP 8.3)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/#\[\s*\\\\?Override\s*\]/i', $contents))->toBe(0,
+ "{$relativeFile} uses #[Override] which requires PHP 8.3"
+ );
+ }
+});
+
+it('does not use the #[Deprecated] attribute (PHP 8.4)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/#\[\s*\\\\?Deprecated\b/i', $contents))->toBe(0,
+ "{$relativeFile} uses #[Deprecated] which requires PHP 8.4"
+ );
+ }
+});
+
+it('does not use json_validate() (PHP 8.3)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/\bjson_validate\s*\(/', $contents))->toBe(0,
+ "{$relativeFile} uses json_validate() which requires PHP 8.3"
+ );
+ }
+});
+
+it('does not use array_find()/array_any()/array_all() (PHP 8.4)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/\barray_(find|any|all)\s*\(/', $contents))->toBe(0,
+ "{$relativeFile} uses array_find()/array_any()/array_all() which requires PHP 8.4"
+ );
+ }
+});
+
+it('does not use typed class constants (PHP 8.3)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/\bconst\s+(?:\?\s*)?[A-Za-z_\\\\][A-Za-z0-9_\\\\]*(?:\s*\|\s*(?:\?\s*)?[A-Za-z_\\\\][A-Za-z0-9_\\\\]*)*\s+[A-Za-z_][A-Za-z0-9_]*\s*=/', $contents))->toBe(0,
+ "{$relativeFile} uses a typed class constant which requires PHP 8.3"
+ );
+ }
+});
+
+it('does not use a dynamic class constant fetch (PHP 8.3)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/::\s*\{\s*\$/', $contents))->toBe(0,
+ "{$relativeFile} uses a dynamic class constant fetch (Foo::" . '{$bar}' . ") which requires PHP 8.3"
+ );
+ }
+});
+
+it('does not use each() (removed in PHP 8.0)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ // (?]) excludes jQuery-style calls like $.each(/.each(/->each( - not the removed global each().
+ expect(preg_match('/(?])\beach\s*\(/', $contents))->toBe(0,
+ "{$relativeFile} uses each() which was removed in PHP 8.0"
+ );
+ }
+});
+
+it('does not use create_function() (removed in PHP 8.0)', function () use ($files) {
+ foreach ($files as $relativeFile) {
+ $contents = plugin_test_read_source_file($relativeFile);
+
+ expect(preg_match('/(?])\bcreate_function\s*\(/', $contents))->toBe(0,
+ "{$relativeFile} uses create_function() which was removed in PHP 8.0"
+ );
+ }
+});
+
diff --git a/tests/TestCase.php b/tests/TestCase.php
deleted file mode 100644
index 4395961..0000000
--- a/tests/TestCase.php
+++ /dev/null
@@ -1,44 +0,0 @@
-