├── .editorconfig
├── .eslintrc
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── release-assets.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── .vscode
├── extensions.json
└── settings.json
├── .wp-env.json
├── .wporg
├── banner-1544x500.png
├── banner-772x250.png
├── blueprints
│ └── blueprint.json
├── icon-128x128.png
├── icon-256x256.png
├── screenshot-1.png
├── screenshot-2.png
├── screenshot-3.png
├── screenshot-4.png
├── screenshot-5.png
└── screenshot-6.png
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── app
├── CommonScripts.php
├── Concerns
│ └── DontInstantiate.php
├── Contracts
│ └── Enabler.php
├── Features
│ ├── AdminBar
│ │ ├── AdminBar.php
│ │ └── RegisteredMenu.php
│ ├── Attachment.php
│ ├── Comments.php
│ ├── DashboardWidgets.php
│ ├── Embeds.php
│ ├── Feeds.php
│ ├── Gutenberg.php
│ ├── Heartbeat
│ │ ├── Heartbeat.php
│ │ ├── ManageAdmin.php
│ │ └── ManagePostEditor.php
│ ├── LoginIdentifier.php
│ ├── MaintenanceMode.php
│ ├── MediaViewMode.php
│ ├── ObfuscateUsernames.php
│ ├── PrivateMode.php
│ └── Updates
│ │ ├── Helpers
│ │ ├── AutoUpdate.php
│ │ ├── AutoUpdateComponents.php
│ │ ├── Updates.php
│ │ └── UpdatesComponents.php
│ │ ├── ManageCore.php
│ │ ├── ManagePlugins.php
│ │ ├── ManageThemes.php
│ │ └── Updates.php
├── Helpers
│ ├── Admin.php
│ ├── Assets.php
│ ├── Option.php
│ ├── Str.php
│ └── URL.php
├── InlineData.php
├── Modules
│ ├── Admin.php
│ ├── Advanced.php
│ ├── General.php
│ ├── Mail.php
│ ├── Media.php
│ ├── Modules.php
│ ├── Security.php
│ └── Site.php
├── Plugin.php
└── SettingPage.php
├── composer.json
├── composer.lock
├── inc
├── bootstrap
│ ├── app.php
│ ├── dev.php
│ └── providers.php
├── config
│ └── app.php
├── languages
│ └── syntatis-feature-flipper.pot
├── settings
│ └── all.php
└── views
│ ├── maintenance-mode.php
│ └── settings-page.php
├── package-lock.json
├── package.json
├── phpcs.xml.dist
├── phpstan.neon.dist
├── phpunit.xml.dist
├── readme.txt
├── scoper.inc.php
├── src
├── admin-bar
│ ├── EnvironmentType
│ │ ├── EnvironmentType.jsx
│ │ ├── EnvironmentType.module.scss
│ │ └── index.js
│ └── index.js
├── comments
│ └── index.js
├── embeds
│ └── index.js
└── setting-page
│ ├── Page.jsx
│ ├── Page.scss
│ ├── components
│ ├── Details
│ │ ├── Details.jsx
│ │ ├── Details.module.scss
│ │ └── index.js
│ ├── HelpContent
│ │ ├── HelpContent.jsx
│ │ ├── HelpContent.module.scss
│ │ └── index.js
│ ├── HelpTip
│ │ ├── HelpTip.jsx
│ │ ├── HelpTip.module.scss
│ │ └── index.js
│ └── index.js
│ ├── fieldset
│ ├── AdminBarFieldset.jsx
│ ├── CommentsFieldset.jsx
│ ├── CommentsFieldset.module.scss
│ ├── DashboardWidgetsFieldset.jsx
│ ├── GutenbergFieldset.jsx
│ ├── HeartbeatFieldset.jsx
│ ├── HeartbeatFieldset.module.scss
│ ├── ImageQualityFieldset.jsx
│ ├── RadioGroupFieldset.jsx
│ ├── RevisionsFieldset.jsx
│ ├── SiteAccessFieldset.jsx
│ ├── SiteAccessFieldset.module.scss
│ ├── SwitchFieldset.jsx
│ ├── TextFieldset.jsx
│ ├── UpdatesFieldset.jsx
│ ├── UpdatesFieldset.module.scss
│ ├── index.js
│ └── styles.module.scss
│ ├── form
│ ├── Fieldset.jsx
│ ├── Fieldset.module.scss
│ ├── Form.jsx
│ ├── Form.module.scss
│ ├── FormNotice.jsx
│ ├── FormNotice.module.scss
│ ├── SettingsProvider.jsx
│ ├── SubmitButton.jsx
│ ├── index.js
│ ├── useSettings.js
│ └── useSettingsContext.js
│ ├── index.js
│ └── tabs
│ ├── AdminTab.jsx
│ ├── AdvancedTab.jsx
│ ├── AdvancedTab.module.scss
│ ├── GeneralTab.jsx
│ ├── MailTab.jsx
│ ├── MediaTab.jsx
│ ├── SecurityTab.jsx
│ ├── SiteTab.jsx
│ └── index.js
├── syntatis-feature-flipper.php
├── tests
├── phpstan
│ └── woocommerce-stubs.php
└── phpunit
│ ├── WPTestCase.php
│ ├── WithAdminBar.php
│ ├── app
│ ├── Features
│ │ ├── AdminBarTest.php
│ │ ├── AttachmentTest.php
│ │ ├── CommentsTest.php
│ │ ├── DashboardWidgetsTest.php
│ │ ├── GutenbergTest.php
│ │ ├── Heartbeat
│ │ │ ├── HeartbeatTest.php
│ │ │ ├── ManageAdminTest.php
│ │ │ └── ManagePostEditorTest.php
│ │ ├── LoginIdentifierTest.php
│ │ ├── MaintenanceModeTest.php
│ │ ├── ObfuscateUsernamesTest.php
│ │ ├── PrivateModeTest.php
│ │ ├── Updates
│ │ │ ├── Helpers
│ │ │ │ ├── AutoUpdateTest.php
│ │ │ │ └── UpdatesTest.php
│ │ │ ├── ManageCoreTest.php
│ │ │ ├── ManagePluginsTest.php
│ │ │ └── ManageThemesTest.php
│ │ └── UpdatesTest.php
│ ├── Helpers
│ │ ├── AdminTest.php
│ │ ├── OptionTest.php
│ │ └── StrTest.php
│ ├── InlineDataTest.php
│ └── Modules
│ │ ├── GeneralTest.php
│ │ └── MailTest.php
│ └── bootstrap.php
├── uninstall.php
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = tab
9 | indent_size = 4
10 |
11 | [*.json]
12 | indent_style = space
13 |
14 | [*.{yml,yaml,neon,neon.dist}]
15 | indent_style = space
16 | indent_size = 2
17 |
18 | [*.md]
19 | trim_trailing_whitespace = false
20 |
21 | [{*.txt,wp-config-sample.php}]
22 | end_of_line = crlf
23 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | labels:
8 | - "dependencies"
9 | - "ci-cd"
10 |
11 | - package-ecosystem: "composer"
12 | versioning-strategy: increase
13 | open-pull-requests-limit: 5
14 | directory: "/"
15 | schedule:
16 | interval: "weekly"
17 | day: "friday"
18 | labels:
19 | - "dependencies"
20 | - "composer"
21 | ignore:
22 | - dependency-name: "symfony/*"
23 | versions: [">=6.0"]
24 | groups:
25 | composer-require:
26 | dependency-type: "production"
27 | update-types:
28 | - "minor"
29 | - "patch"
30 | composer-require-dev:
31 | dependency-type: "development"
32 | update-types:
33 | - "minor"
34 | - "patch"
35 |
36 | - package-ecosystem: "npm"
37 | versioning-strategy: increase
38 | open-pull-requests-limit: 5
39 | directory: "/"
40 | schedule:
41 | interval: "weekly"
42 | day: "sunday"
43 | labels:
44 | - "dependencies"
45 | - "npm"
46 | allow:
47 | - dependency-type: "direct"
48 | groups:
49 | npm-dependencies:
50 | dependency-type: "production"
51 | update-types:
52 | - "minor"
53 | - "patch"
54 | npm-dev-dependencies:
55 | dependency-type: "development"
56 | update-types:
57 | - "minor"
58 | - "patch"
59 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - main
8 | paths:
9 | - ".github/workflows/ci.yml"
10 | - "**.php"
11 | - "composer.json"
12 | - "phpcs.xml.dist"
13 | - "phpstan.neon.dist"
14 | - "phpunit.xml.dist"
15 |
16 | push:
17 | branches:
18 | - main
19 | paths:
20 | - ".github/workflows/ci.yml"
21 | - "**.php"
22 | - "composer.json"
23 | - "phpcs.xml.dist"
24 | - "phpstan.neon.dist"
25 | - "phpunit.xml.dist"
26 |
27 | jobs:
28 | checks:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout code
32 | uses: actions/checkout@v4
33 |
34 | - name: Setup PHP
35 | uses: shivammathur/setup-php@v2
36 | with:
37 | php-version: "7.4"
38 |
39 | - name: Install PHP dependencies
40 | uses: ramsey/composer-install@v3
41 |
42 | - name: Run PHPCS
43 | run: composer run phpcs
44 |
45 | - name: Run PHPStan
46 | run: composer run phpstan
47 |
48 | tests:
49 | runs-on: ubuntu-latest
50 | strategy:
51 | fail-fast: true
52 | max-parallel: 2
53 | matrix:
54 | php-version: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]
55 | wp-core-version: ["WordPress/WordPress", "WordPress/WordPress#6.0"]
56 |
57 | steps:
58 | - name: Checkout code
59 | uses: actions/checkout@v4
60 |
61 | - name: Setup Node.js
62 | uses: actions/setup-node@v4
63 | with:
64 | cache: "npm"
65 | node-version: "20.x"
66 |
67 | - name: Setup PHP
68 | uses: shivammathur/setup-php@v2
69 | with:
70 | php-version: "7.4"
71 | tools: composer:v2
72 |
73 | - name: Get Docker version
74 | run: docker -v
75 |
76 | - name: Install NodeJS dependencies
77 | run: npm ci
78 |
79 | - name: Install PHP dependencies
80 | uses: ramsey/composer-install@v3
81 |
82 | - name: Start wp-env
83 | run: |
84 | jq -n \
85 | --arg phpVersion "${{ matrix.php-version }}" \
86 | --arg wpCoreVersion "${{ matrix.wp-core-version }}" \
87 | '{"phpVersion":$phpVersion, "core":$wpCoreVersion}' \
88 | > .wp-env.override.json
89 | npm run wp-env:start
90 | npm run wp-env:tests-wordpress php -- -v
91 |
92 | - name: Run PHPUnit
93 | run: composer run phpunit
94 |
95 | typos:
96 | runs-on: ubuntu-latest
97 | steps:
98 | - name: Checkout code
99 | uses: actions/checkout@v4
100 |
101 | - name: Check spelling
102 | uses: crate-ci/typos@v1.32.0
103 |
--------------------------------------------------------------------------------
/.github/workflows/release-assets.yml:
--------------------------------------------------------------------------------
1 | name: release-assets
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | release:
8 | runs-on: ubuntu-22.04
9 | steps:
10 | - name: Checkout code
11 | uses: actions/checkout@v4
12 |
13 | - name: Push Assets to WordPress.org
14 | uses: 10up/action-wordpress-plugin-asset-update@stable
15 | env:
16 | SLUG: syntatis-feature-flipper
17 | ASSETS_DIR: .wporg
18 | IGNORE_OTHER_FILES: true
19 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
20 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [released]
7 |
8 | env:
9 | PLUGIN_SLUG: syntatis-feature-flipper
10 |
11 | jobs:
12 | release:
13 | runs-on: ubuntu-22.04
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: "7.4"
22 | tools: composer:v2
23 |
24 | - name: Install dependencies
25 | run: |
26 | npm install
27 | composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader
28 |
29 | - name: Build plugin
30 | run: |
31 | npm run build
32 | composer run build
33 | composer run plugin:zip -- --file=build
34 | unzip build.zip -d build
35 |
36 | - name: "Check latest tagged version"
37 | if: ${{ github.event_name == 'release' }}
38 | run: |
39 | LATEST_TAG="${{ github.ref_name }}";
40 | HEADER_VERSION="v$(sed -n -e '0,/^\s*\* Version:\s\+\([0-9].\+\)$/ s##\1#p' syntatis-feature-flipper.php)";
41 | PLUGIN_VERSION="v$(sed -n -e "0,/^const PLUGIN_VERSION = '\\([0-9].\\+\\)';$/ s##\\1#p" syntatis-feature-flipper.php)";
42 | README_VERSION="v$(sed -n -e '0,/^Stable tag: \([0-9].\+\)$/ s##\1#p' readme.txt | tr -d '[:space:]')";
43 | if [[ "${LATEST_TAG}" != "${HEADER_VERSION}" || "${LATEST_TAG}" != "${PLUGIN_VERSION}" || "${LATEST_TAG}" != "${README_VERSION}" ]];
44 | then
45 | echo "::error::Latest tag differs from current version"
46 | exit 10
47 | fi
48 |
49 | - name: Deploy to WordPress.org
50 | uses: 10up/action-wordpress-plugin-deploy@stable
51 | with:
52 | dry-run: ${{ github.event_name != 'release' }}
53 | env:
54 | ASSETS_DIR: .wporg
55 | BUILD_DIR: build
56 | SLUG: ${{ env.PLUGIN_SLUG }}
57 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
58 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS generated files #
2 | ######################
3 | .DS_Store
4 | Thumbs.db
5 |
6 | # Logs and databases #
7 | ######################
8 | *.sql
9 | *.log
10 |
11 | # Compiled source #
12 | ###################
13 | *.tar.gz
14 | *.zip
15 | dist*
16 | build*
17 | @*
18 |
19 | # Dependencies #
20 | ##########################
21 | node_modules
22 | vendor*
23 |
24 | # Temporary files #
25 | #############
26 | tmp
27 | .phpunit*
28 | *.cache
29 |
30 | # wp-env #
31 | #####################
32 | .wp-env.override*
33 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "bmewburn.vscode-intelephense-client",
4 | "christian-kohler.path-intellisense",
5 | "dotjoshjohnson.xml",
6 | "editorconfig.editorconfig",
7 | "kasik96.latte",
8 | "mikestead.dotenv",
9 | "ms-vscode-remote.remote-containers",
10 | "neilbrayfield.php-docblocker",
11 | "sanderronde.phpstan-vscode",
12 | "wmaurer.change-case",
13 | "valeryanm.vscode-phpsab",
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "intelephense.stubs": [
3 | "apache",
4 | "bcmath",
5 | "bz2",
6 | "calendar",
7 | "com_dotnet",
8 | "Core",
9 | "ctype",
10 | "curl",
11 | "date",
12 | "dba",
13 | "dom",
14 | "enchant",
15 | "exif",
16 | "FFI",
17 | "fileinfo",
18 | "filter",
19 | "fpm",
20 | "ftp",
21 | "gd",
22 | "gettext",
23 | "gmp",
24 | "hash",
25 | "iconv",
26 | "imap",
27 | "intl",
28 | "json",
29 | "ldap",
30 | "libxml",
31 | "mbstring",
32 | "meta",
33 | "mysqli",
34 | "oci8",
35 | "odbc",
36 | "openssl",
37 | "pcntl",
38 | "pcre",
39 | "PDO",
40 | "pgsql",
41 | "Phar",
42 | "posix",
43 | "pspell",
44 | "readline",
45 | "redis",
46 | "Reflection",
47 | "session",
48 | "shmop",
49 | "SimpleXML",
50 | "snmp",
51 | "soap",
52 | "sockets",
53 | "sodium",
54 | "SPL",
55 | "sqlite3",
56 | "standard",
57 | "superglobals",
58 | "sysvmsg",
59 | "sysvsem",
60 | "sysvshm",
61 | "tidy",
62 | "tokenizer",
63 | "wordpress",
64 | "xml",
65 | "xmlreader",
66 | "xmlrpc",
67 | "xmlwriter",
68 | "xsl",
69 | "Zend OPcache",
70 | "zip",
71 | "zlib"
72 | ],
73 | "[json]": {
74 | "editor.defaultFormatter": "vscode.json-language-features"
75 | },
76 | "[php]": {
77 | "editor.defaultFormatter": "wongjn.php-sniffer"
78 | },
79 | "files.exclude": {
80 | "**/.git": true,
81 | "**/.svn": true,
82 | "**/.hg": true,
83 | "**/.DS_Store": true,
84 | "**/Thumbs.db": true
85 | },
86 | "search.exclude": {
87 | "**/dist": true
88 | },
89 | "intelephense.files.exclude": [
90 | "**/.git/**",
91 | "**/.svn/**",
92 | "**/.hg/**",
93 | "**/.DS_Store/**",
94 | "**/.history/**",
95 | "**/CVS/**"
96 | ],
97 | "phpSniffer.autoDetect": true,
98 | "phpSniffer.run": "onSave"
99 | }
100 |
--------------------------------------------------------------------------------
/.wp-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schemas.wp.org/trunk/wp-env.json",
3 | "phpVersion": "7.4",
4 | "core": "https://wordpress.org/latest.zip",
5 | "plugins": [
6 | "."
7 | ],
8 | "port": 8801,
9 | "config": {
10 | "WP_DEBUG_LOG": true
11 | },
12 | "env": {
13 | "tests": {
14 | "port": 8901
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.wporg/banner-1544x500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/banner-1544x500.png
--------------------------------------------------------------------------------
/.wporg/banner-772x250.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/banner-772x250.png
--------------------------------------------------------------------------------
/.wporg/blueprints/blueprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json",
3 | "preferredVersions": {
4 | "php": "7.4",
5 | "wp": "latest"
6 | },
7 | "landingPage": "/wp-admin/options-general.php?page=syntatis-feature-flipper",
8 | "steps": [
9 | {
10 | "step": "login"
11 | },
12 | {
13 | "step": "installPlugin",
14 | "pluginData": {
15 | "resource": "wordpress.org/plugins",
16 | "slug": "syntatis-feature-flipper"
17 | },
18 | "options": {
19 | "activate": true
20 | }
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.wporg/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/icon-128x128.png
--------------------------------------------------------------------------------
/.wporg/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/icon-256x256.png
--------------------------------------------------------------------------------
/.wporg/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-1.png
--------------------------------------------------------------------------------
/.wporg/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-2.png
--------------------------------------------------------------------------------
/.wporg/screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-3.png
--------------------------------------------------------------------------------
/.wporg/screenshot-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-4.png
--------------------------------------------------------------------------------
/.wporg/screenshot-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-5.png
--------------------------------------------------------------------------------
/.wporg/screenshot-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syntatis/wp-feature-flipper/b3f304cfcc0c554dff673f42b9e3b43ea0407b56/.wporg/screenshot-6.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 |
5 | [](https://github.com/syntatis/wp-feature-flipper/actions/workflows/ci.yml)
6 | [](https://wordpress.org/plugins/syntatis-feature-flipper/)
7 | [](https://wordpress.org/plugins/syntatis-feature-flipper/)
8 | [](https://wordpress.org/plugins/syntatis-feature-flipper/)
9 | [](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/syntatis/wp-feature-flipper/main/.wporg/blueprints/blueprint.json)
10 |
11 |
12 |
13 | ---
14 |
15 | > [!NOTE]
16 | > This plugin is built to serve as a showcase for [Howdy](https://github.com/syntatis/howdy), a starter kit for developing WordPress® plugins.
17 |
18 | This plugin gives you the ability to manage core WordPress features like Comments, the Block Editor (Gutenberg), Emojis, XML-RPC, Feeds, Updates, Automatic Updates, Cron, Heartbeat, and more. If you don't need certain features, you can easily toggle them off or customize their behavior.
19 |
20 | It also includes additional utility features, like showing your site's environment type in the admin bar, enabling maintenance or private mode, or using random URLs for media pages, which you can enable or disable as needed.
21 |
22 | [Read more on WordPress.org](https://wordpress.org/plugins/syntatis-feature-flipper/)
23 |
24 | ## Contributing
25 |
26 | Found a bug? [Open an issue](https://github.com/syntatis/wp-feature-flipper/issues/new) or [submit a pull request](https://github.com/syntatis/wp-feature-flipper/compare). Have an idea, or suggestion? Let's discuss it in the [Discussions](https://github.com/syntatis/wp-feature-flipper/discussions) tab.
27 |
28 | ## Security Vulnerabilities
29 |
30 | If you discover a security issue, please report it through the **Patchstack Vulnerability Disclosure Program (VDP)**. The Patchstack team will validate, triage, and assist in resolving any reported vulnerabilities to keep the plugin secure.
31 |
32 | [Report a security vulnerability](https://patchstack.com/database/wordpress/plugin/syntatis-feature-flipper/vdp)
33 |
--------------------------------------------------------------------------------
/app/CommonScripts.php:
--------------------------------------------------------------------------------
1 | addAction('admin_enqueue_scripts', [$this, 'enqueueScripts']);
16 | $hook->addAction('wp_enqueue_scripts', [$this, 'enqueueScripts']);
17 | }
18 |
19 | public function enqueueScripts(): void
20 | {
21 | if (! is_user_logged_in()) {
22 | return;
23 | }
24 |
25 | wp_enqueue_style(App::name() . '-common', App::url('dist/assets/index.css'));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Concerns/DontInstantiate.php:
--------------------------------------------------------------------------------
1 | [
38 | 'id' => 'customize',
39 | 'parent' => false,
40 | ],
41 | 'edit' => [
42 | 'id' => 'edit',
43 | 'parent' => false,
44 | ],
45 | 'search' => [
46 | 'id' => 'search',
47 | 'parent' => false,
48 | ],
49 | ];
50 |
51 | private ?WP_Admin_Bar $wpAdminBar;
52 |
53 | /** @phpstan-var array */
54 | private array $registeredMenu;
55 |
56 | private function __construct()
57 | {
58 | /** @var WP_Admin_Bar|null $wpAdminBar */
59 | $wpAdminBar = $GLOBALS['wp_admin_bar'] ?? null;
60 |
61 | $this->wpAdminBar = $wpAdminBar;
62 | $this->registeredMenu = $this->getRegisteredMenu();
63 | }
64 |
65 | /**
66 | * @phpstan-param "top"|null $level The level of the menu to retrieve.
67 | *
68 | * @phpstan-return array
69 | */
70 | public static function all(?string $level = null): array
71 | {
72 | $instance = new self();
73 |
74 | switch ($level) {
75 | case 'top':
76 | return $instance->getTopItems();
77 |
78 | default:
79 | return $instance->registeredMenu;
80 | }
81 | }
82 |
83 | /**
84 | * Retrieve top-level menu of the admin bar.
85 | *
86 | * @phpstan-return array
87 | */
88 | private function getTopItems(): array
89 | {
90 | $topItems = [];
91 |
92 | foreach ($this->registeredMenu as $key => $menu) {
93 | $menuParent = $menu['parent'] ?? null;
94 |
95 | /**
96 | * If the node is not a top-level node or a node within the `top-secondary`,
97 | * skip it. The `top-secondary` section may contain some menus items such
98 | * as the "environment type" and "site status" menu. Even though these
99 | * menu are technically not top-level menus, they are considered as
100 | * such since users can see them on the top-level and might want
101 | * to toggle them on or off.
102 | *
103 | * @see Syntatis\FeatureFlipper\Features\AdminBar::addEnvironmentTypeNode()
104 | * @see Syntatis\FeatureFlipper\Features\MaintenanceMode
105 | * @see Syntatis\FeatureFlipper\Features\PrivateMode
106 | */
107 | if ($menuParent !== false && $menuParent !== 'top-secondary') {
108 | continue;
109 | }
110 |
111 | if (in_array($key, self::getExcludedMenu(), true)) {
112 | continue;
113 | }
114 |
115 | $topItems[$key] = $menu;
116 | }
117 |
118 | return $topItems;
119 | }
120 |
121 | /** @phpstan-return array */
122 | private function getRegisteredMenu(): array
123 | {
124 | if (! $this->wpAdminBar instanceof WP_Admin_Bar) {
125 | return [];
126 | }
127 |
128 | /** @phpstan-var array|null $nodes */
129 | $nodes = $this->wpAdminBar->get_nodes();
130 |
131 | if (! is_array($nodes)) {
132 | return [];
133 | }
134 |
135 | $items = [];
136 |
137 | foreach ($nodes as $id => $node) {
138 | $id = property_exists($node, 'id') && is_string($node->id) ?
139 | $node->id :
140 | null;
141 |
142 | if ($id === '' || $id === null) {
143 | continue;
144 | }
145 |
146 | $parent = property_exists($node, 'parent') && is_string($node->parent) ?
147 | $node->parent :
148 | false;
149 |
150 | $items[$id] = [
151 | 'id' => $id,
152 | 'parent' => $parent,
153 | ];
154 | }
155 |
156 | return array_merge($items, self::INCLUDE_CORE_MENU_ITEMS);
157 | }
158 |
159 | /** @phpstan-return list */
160 | private static function getExcludedMenu(): array
161 | {
162 | $excludes = self::EXCLUDE_MENU_ITEMS;
163 |
164 | if (! Option::isOn('comments')) {
165 | $excludes = [
166 | ...$excludes,
167 | 'comments',
168 | ];
169 | }
170 |
171 | return $excludes;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/app/Features/Attachment.php:
--------------------------------------------------------------------------------
1 | addFilter(Option::hook('default:attachment_page'), static function (): bool {
21 | /**
22 | * In WordPress 6.4, A new option, `wp_attachment_pages_enabled` is introduced
23 | * to control the attachment page behavior.
24 | *
25 | * - For new sites, the option will be set to `0` by default, and the attachment
26 | * page will be disabled.
27 | * - For existing sites, the option will be set to `1` after the upgrade and
28 | * the attachment page will
29 | * remain enabled.
30 | *
31 | * By default, this plugin will follow this default behavior.
32 | *
33 | * @see https://make.wordpress.org/core/2023/10/16/changes-to-attachment-pages/
34 | */
35 | $enabled = get_option('wp_attachment_pages_enabled', null);
36 |
37 | return $enabled === '1' || $enabled === null;
38 | });
39 | $hook->addAction(
40 | Option::hook('add:attachment_page'),
41 | static function ($option, $value): void {
42 | update_option('wp_attachment_pages_enabled', (bool) $value ? '1' : '0');
43 | },
44 | 10,
45 | 2,
46 | );
47 | $hook->addAction(
48 | Option::hook('update:attachment_page'),
49 | static function ($oldValue, $newValue): void {
50 | update_option('wp_attachment_pages_enabled', (bool) $newValue ? '1' : '0');
51 | },
52 | 10,
53 | 2,
54 | );
55 |
56 | // 1. Attachment Page.
57 | if (! Option::isOn('attachment_page')) {
58 | $hook->addAction('template_redirect', function (): void {
59 | if (! is_attachment()) {
60 | return;
61 | }
62 |
63 | $this->toNotFound();
64 | });
65 |
66 | $hook->addFilter(
67 | 'redirect_canonical',
68 | /* @phpstan-ignore shipmonk.missingNativeReturnTypehint */
69 | function (string $url) {
70 | if (! is_attachment()) {
71 | return $url;
72 | }
73 |
74 | $this->toNotFound();
75 | },
76 | );
77 |
78 | /**
79 | * Replace the link to "View Attachment Page" with the actual attachment URL.
80 | */
81 | $hook->addFilter('attachment_link', static function (string $url, int $id): string {
82 | $attachmentUrl = wp_get_attachment_url($id);
83 |
84 | if (is_string($attachmentUrl)) {
85 | return $attachmentUrl;
86 | }
87 |
88 | return $url;
89 | }, 99, 2);
90 | }
91 |
92 | if (Option::isOn('attachment_slug')) {
93 | return;
94 | }
95 |
96 | $hook->addFilter(
97 | 'wp_unique_post_slug',
98 | static function (string $slug, string $id, string $status, string $type): string {
99 | if ($type !== 'attachment' || Uuid::isValid($slug)) {
100 | return $slug;
101 | }
102 |
103 | try {
104 | return (string) Uuid::v5(Uuid::fromString(Uuid::NAMESPACE_URL), $slug);
105 | } catch (Throwable $th) {
106 | return $slug;
107 | }
108 | },
109 | 99,
110 | 4,
111 | );
112 | }
113 |
114 | private function toNotFound(): void
115 | {
116 | /** @var WP_Query|null $wpQuery */
117 | $wpQuery = $GLOBALS['wp_query'] ?? null;
118 |
119 | if ($wpQuery === null) {
120 | return;
121 | }
122 |
123 | $wpQuery->set_404();
124 | status_header(404);
125 | nocache_headers();
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/Features/Embeds.php:
--------------------------------------------------------------------------------
1 | addAction('init', fn () => $this->disables($hook), PHP_INT_MAX);
31 | }
32 |
33 | private function disables(Hook $hook): void
34 | {
35 | // phpcs:disable
36 | /** @var WP $wp */
37 | $wp = $GLOBALS['wp'];
38 | $wp->public_query_vars = array_diff($wp->public_query_vars, ['embed']);
39 | // phpcs:enable
40 |
41 | $hook->addAction(Option::hook('update:cron'), [$this, 'flushPermalinks']);
42 | $hook->addAction('enqueue_block_editor_assets', [$this, 'disableOnBlockEditor']);
43 | $hook->addAction('wp_default_scripts', [$this, 'disableScriptDependencies']);
44 | $hook->addFilter('embed_oembed_discover', '__return_false');
45 | $hook->addFilter('oembed_response_data', [$this, 'disableResponseData']);
46 | $hook->addFilter('rest_endpoints', [$this, 'disableEndpoint']);
47 | $hook->addFilter('rewrite_rules_array', [$this, 'disableRewrite']);
48 | $hook->addFilter('tiny_mce_plugins', [$this, 'disableOnTinyEditor']);
49 |
50 | $hook->removeAction('wp_head', 'wp_oembed_add_discovery_links');
51 | $hook->removeAction('wp_head', 'wp_oembed_add_host_js');
52 | $hook->removeFilter('oembed_dataparse', 'wp_filter_oembed_result', 10);
53 | $hook->removeFilter('pre_oembed_result', 'wp_filter_pre_oembed_result', 10);
54 | }
55 |
56 | public function flushPermalinks(): void
57 | {
58 | flush_rewrite_rules(false);
59 | }
60 |
61 | /**
62 | * @param array $data
63 | *
64 | * @return array|false
65 | */
66 | public function disableResponseData(array $data)
67 | {
68 | if (defined('REST_REQUEST') && REST_REQUEST) {
69 | return false;
70 | }
71 |
72 | return $data;
73 | }
74 |
75 | /**
76 | * @param array $endpoints
77 | *
78 | * @return array
79 | */
80 | public function disableEndpoint(array $endpoints): array
81 | {
82 | if (isset($endpoints['/oembed/1.0/embed'])) {
83 | unset($endpoints['/oembed/1.0/embed']);
84 | }
85 |
86 | return $endpoints;
87 | }
88 |
89 | /**
90 | * @param array $rules
91 | *
92 | * @return array
93 | */
94 | public function disableRewrite(array $rules): array
95 | {
96 | foreach ($rules as $rule => $rewrite) {
97 | if (is_string($rewrite) && strpos($rewrite, 'embed=true') === false) {
98 | continue;
99 | }
100 |
101 | unset($rules[$rule]);
102 | }
103 |
104 | return $rules;
105 | }
106 |
107 | /**
108 | * @param array $plugins
109 | *
110 | * @return array
111 | */
112 | public function disableOnTinyEditor(array $plugins): array
113 | {
114 | return array_diff($plugins, ['wpembed']);
115 | }
116 |
117 | public function disableOnBlockEditor(): void
118 | {
119 | $assetFile = App::dir('dist/assets/embeds/index.asset.php');
120 |
121 | /** @phpstan-var array{dependencies?:array,version?:string} $asset */
122 | $asset = is_readable($assetFile) ? require $assetFile : [];
123 | $asset['dependencies'] ??= [];
124 | $asset['version'] ??= null;
125 |
126 | wp_enqueue_script(
127 | App::name() . '-embeds',
128 | App::url('dist/assets/embeds/index.js'),
129 | $asset['dependencies'],
130 | $asset['version'],
131 | true,
132 | );
133 | }
134 |
135 | public function disableScriptDependencies(WP_Scripts $scripts): void
136 | {
137 | if (! isset($scripts->registered['wp-edit-post'])) {
138 | return;
139 | }
140 |
141 | $scripts->registered['wp-edit-post']->deps = array_diff(
142 | $scripts->registered['wp-edit-post']->deps,
143 | ['wp-embed'],
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/Features/Feeds.php:
--------------------------------------------------------------------------------
1 | addAction('do_feed', [$this, 'toHomepage'], PHP_INT_MIN);
23 | $hook->addAction('do_feed_rdf', [$this, 'toHomepage'], PHP_INT_MIN);
24 | $hook->addAction('do_feed_rss', [$this, 'toHomepage'], PHP_INT_MIN);
25 | $hook->addAction('do_feed_rss2', [$this, 'toHomepage'], PHP_INT_MIN);
26 | $hook->addAction('do_feed_atom', [$this, 'toHomepage'], PHP_INT_MIN);
27 |
28 | // Disable comments feeds.
29 | $hook->addAction('do_feed_rss2_comments', [$this, 'toHomepage'], PHP_INT_MIN);
30 | $hook->addAction('do_feed_atom_comments', [$this, 'toHomepage'], PHP_INT_MIN);
31 |
32 | // Remove RSS feed links.
33 | $hook->addFilter('feed_links_show_posts_feed', '__return_false', 99);
34 |
35 | // Remove all extra RSS feed links.
36 | $hook->addFilter('feed_links_show_comments_feed', '__return_false', 99);
37 | }
38 |
39 | public function toHomepage(): void
40 | {
41 | wp_redirect(home_url());
42 | exit;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Features/Gutenberg.php:
--------------------------------------------------------------------------------
1 | addAction('syntatis/feature_flipper/updated_options', [$this, 'stashOptions']);
25 | $hook->addFilter('use_block_editor_for_post', [$this, 'filterUseBlockEditorForPost'], PHP_INT_MAX, 2);
26 | $hook->addFilter(
27 | Option::hook('default:gutenberg_post_types'),
28 | static fn () => self::getPostTypes(),
29 | PHP_INT_MAX,
30 | );
31 | $hook->addFilter(
32 | Option::hook('gutenberg_post_types'),
33 | static fn ($value) => Option::patch(
34 | 'gutenberg_post_types',
35 | is_array($value) ? $value : [],
36 | self::getPostTypes(),
37 | ),
38 | PHP_INT_MAX,
39 | );
40 | }
41 |
42 | /** @param array $options List of option names that have been updated. */
43 | public function stashOptions(array $options): void
44 | {
45 | if (! in_array(Option::name('gutenberg_post_types'), $options, true)) {
46 | return;
47 | }
48 |
49 | Option::stash('gutenberg_post_types', self::getPostTypes());
50 | }
51 |
52 | /**
53 | * Filter the value to determine whether to use the block editor for a post.
54 | *
55 | * @see https://developer.wordpress.org/reference/hooks/use_block_editor_for_post/
56 | *
57 | * @param int|WP_Post $post
58 | */
59 | public function filterUseBlockEditorForPost(bool $value, $post): bool
60 | {
61 | // If the Gutenberg feature is disabled, force the classic editor.
62 | if (! Option::isOn('gutenberg')) {
63 | return false;
64 | }
65 |
66 | $wpPost = is_int($post) ? get_post($post) : $post;
67 |
68 | if (! $wpPost instanceof WP_Post) {
69 | return $value;
70 | }
71 |
72 | $postTypes = Option::get('gutenberg_post_types');
73 | $postTypes = is_array($postTypes) ? $postTypes : [];
74 |
75 | // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps -- WordPress convention.
76 | return in_array($wpPost->post_type, $postTypes, true);
77 | }
78 |
79 | /** @phpstan-return list */
80 | private static function getPostTypes(): array
81 | {
82 | return array_values(array_filter(
83 | get_post_types(['public' => true]),
84 | static fn ($key): bool => use_block_editor_for_post_type($key),
85 | ));
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/Features/Heartbeat/Heartbeat.php:
--------------------------------------------------------------------------------
1 | addAction('init', [$this, 'deregisterScripts'], PHP_INT_MAX);
33 | }
34 |
35 | public function deregisterScripts(): void
36 | {
37 | if (Option::isOn('heartbeat')) {
38 | return;
39 | }
40 |
41 | /**
42 | * If the feature is disabled, deregister the Heartbeat API script, which
43 | * effectively stopping all the Heartbeat API requests on all pages.
44 | */
45 | wp_deregister_script('heartbeat');
46 | }
47 |
48 | /** @return iterable