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 '' . __esc($label, 'quicktree') . ''; 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 @@ -