Skill 15 · Wp Plugin Directory Guidelines
Subchapter 15.2
references/guideline-review-checklist.mdMarkdown25 KBView on GitHub
Use this section as a structured checklist when reviewing a plugin. Each guideline includes the violation signal to look for, the verdict to issue, and the fix to recommend. Cite the guideline number in every finding.
Check: Does the main plugin file have a License: header with a GPL-compatible value? Are all bundled third-party libraries under compatible licenses?
Violation signals:
License: or License URI: header in the main plugin fileProprietary, All Rights Reserved, CC-BY-NC, CC-BY-ND, SSPL, BSL, Commons Clause, EPL, EUPL, or MPL-1.0Verdict: Flag as FAIL with the specific file and license value found.
Fix: Use GPL-2.0-or-later (recommended). Add full license text or a License URI: to https://www.gnu.org/licenses/gpl-2.0.html. Replace incompatible libraries.
Check: Has the developer deliberately re-introduced previously removed code, circumvented a prior guideline decision, or included files they cannot legally distribute?
Violation signals:
Verdict: Flag as FAIL. Document the specific file or commit.
Fix: Remove the offending file or obtain and document proper licensing.
Check: Is the WordPress.org SVN version the canonical release? Is the plugin also distributed via an external channel with a newer version?
Violation signals:
readme.txt advertises a version not present in SVN trunk/tagsVerdict: Flag as FAIL if an actively maintained external version is ahead of the directory.
Fix: Keep SVN up to date. External channels may mirror but must not supersede the directory version.
Check: Is all PHP, JS, and CSS in a form that a developer can read and understand? Are build sources available?
Violation signals:
$a1b2c3 throughout.min.js) committed with no corresponding unminified source in the package or a public repo linked from readme.txtVerdict: Flag as FAIL for obfuscated PHP (always). Flag minified-only JS as FAIL if no source access is documented.
Fix: Remove obfuscation. Add a Development or Build section to readme.txt linking to the source repo (GitHub, GitLab, etc.).
Core rule: Every feature shipped in the directory must function end-to-end without a license key, payment, or account.
Check for each feature gate in the code:
has_paid_access() / is_licensed() / check_license() check gate local processing (not an external service call)?time() > $installed_at + 30 * DAY_IN_SECONDS) for local behavior?if ( $count >= 100 )) that is artificially low and only exists to pressure upgrades?Violation signals (flag as FAIL):
return / wp_die() / blocking screen shown when has_paid_access() is false for a local feature$limit = $licensed ? 10000 : 100 with no filter to extend the free capAllowed patterns (do not flag):
Code patterns:
// VIOLATION — local feature blocked by paid check
if ( ! $this->has_paid_access() ) {
echo 'Upgrade required';
return; // ← blocks execution
}
// VIOLATION — artificial cap with no extension point
$limit = $this->has_paid_access() ? 10000 : 100;// COMPLIANT — free path works; premium adds to it
$this->render_basic_export();
if ( $this->has_premium_addon() ) {
do_action( 'myplugin_premium_export_options' );
}
// COMPLIANT — cap is consistent; extensible via filter
$limit = apply_filters( 'myplugin_event_limit', 10000 );Pre-submission checklist:
Check: Does the external service provide real functionality? Is it documented in the readme?
Violation signals:
Verdict: Flag as FAIL for license-validation-only services. Do not flag genuine SaaS integrations.
Fix: Document what the external service does in readme.txt. Move license validation out of the plugin’s critical path if the functionality is local.
Check: Does the plugin send any data to an external server without the user explicitly opting in?
Violation signals:
Exception: Plugins that are interfaces to a named third-party service (e.g., Akismet, Mailchimp, a CDN) — consent is implied when the user configures the service connection.
Code patterns (violation vs compliant):
// VIOLATION — sends data on activation without consent
register_activation_hook( __FILE__, function() {
wp_remote_post(
'https://api.example.com/collect',
array(
'body' => array(
'site' => home_url(),
'admin_email' => get_option( 'admin_email' ),
),
)
);
} );
// COMPLIANT — explicit opt-in gate
if ( isset( $_POST['myplugin_opt_in'] ) && '1' === $_POST['myplugin_opt_in'] ) {
update_option( 'myplugin_tracking_opt_in', 1 );
}
if ( get_option( 'myplugin_tracking_opt_in' ) ) {
wp_remote_post( 'https://api.example.com/collect', $payload );
}Review questions:
readme.txt privacy disclosures?Verdict: Flag as FAIL for any unconsented outbound call. Include the specific URL or domain found.
Fix: Wrap all outbound calls in an opt-in gate. Add a Privacy Policy section to readme.txt describing what data is collected and why.
Pre-submission checklist:
Check: Is all JS/CSS that runs on the user’s site included in the plugin package?
Violation signals:
wp_enqueue_script() loading JS from a third-party CDN (not a self-hosted asset)file_get_contents + eval, dynamic <script src>)<iframe> pointing to an external URLExceptions allowed:
Code patterns (violation vs compliant):
// VIOLATION — executable JS loaded from third-party CDN
wp_enqueue_script(
'myplugin-admin',
'https://cdn.example.com/myplugin/admin.js',
array(),
'1.0.0',
true
);
// COMPLIANT — executable JS bundled in plugin package
wp_enqueue_script(
'myplugin-admin',
plugins_url( 'assets/js/admin.js', __FILE__ ),
array(),
MYPLUGIN_VERSION,
true
);Review questions:
eval, dynamic <script>, remote includes)?Verdict: Flag as FAIL for each externally loaded executable. Note the URL and the file/line where it is enqueued.
Fix: Bundle JS/CSS locally. Use the WP.org SVN for updates. Replace <iframe> admin pages with proper WP Admin UI backed by a REST or admin-ajax API.
Pre-submission checklist:
Check: Does the plugin engage in any deceptive, manipulative, or harmful behavior?
Violation signals:
Verdict: Flag as FAIL. This is a high-severity category; document evidence thoroughly.
Check: Does the plugin output any “Powered by” links, footer credits, or backlinks visible to site visitors?
Violation signals:
(?<!x-)powered[ -_]by)Exception: A service may brand its own rendered output (e.g., a payment form branded with the payment processor’s logo).
Code patterns (violation vs compliant):
// VIOLATION — forced credit link on public output
add_action( 'wp_footer', function() {
echo '<p class="myplugin-credit"><a href="https://vendor.example">Powered by Vendor</a></p>';
} );
// VIOLATION — backlink required for feature activation
if ( ! get_option( 'myplugin_keep_backlink' ) ) {
wp_die( 'Please keep our credit link active to use this feature.' );
}
// COMPLIANT — optional, explicit opt-in, default off
if ( get_option( 'myplugin_show_credit_link', false ) ) {
echo '<p class="myplugin-credit"><a href="https://vendor.example">Powered by Vendor</a></p>';
}Review questions:
Verdict: Flag as FAIL if the link is on by default with no opt-out. Flag as FAIL if removing it breaks functionality.
Fix: Default the setting to false (hidden). Provide a clear checkbox in settings to enable it.
Pre-submission checklist:
Check: Are admin notices, upgrade prompts, and nags limited and non-intrusive?
Violation signals:
<iframe> instead of native WP admin UICode patterns (violation vs compliant):
// VIOLATION — external iframe in admin page
add_action( 'admin_menu', function() {
add_menu_page( 'My Plugin', 'My Plugin', 'manage_options', 'myplugin', function() {
echo '<iframe src="https://app.vendor.example/dashboard" style="width:100%;height:80vh;border:0"></iframe>';
} );
} );
// COMPLIANT — native admin page shell with server/API data fetch
add_action( 'admin_menu', function() {
add_menu_page( 'My Plugin', 'My Plugin', 'manage_options', 'myplugin', function() {
echo '<div class="wrap"><h1>My Plugin</h1><div id="myplugin-admin-app"></div></div>';
} );
} );
add_action( 'admin_enqueue_scripts', function( $hook ) {
if ( 'toplevel_page_myplugin' !== $hook ) {
return;
}
wp_enqueue_script(
'myplugin-admin',
plugins_url( 'assets/js/admin.js', __FILE__ ),
array( 'wp-api-fetch' ),
MYPLUGIN_VERSION,
true
);
} );Review questions:
Verdict: Flag as FAIL for persistent undismissable notices or for notices appearing outside the plugin’s own pages.
Fix: Use is_plugin_page() or an equivalent check to scope notices. Add a dismiss handler using update_user_meta or the WP dismissible notice pattern. Never show upgrade prompts on unrelated admin pages.
Pre-submission checklist:
Check: Is the readme.txt free of keyword stuffing, excessive affiliate links, and competitor tags?
Violation signals:
Tags: fieldreadme.txt reads as a keyword list rather than useful documentationVerdict: Flag as WARNING for minor stuffing; FAIL for undisclosed affiliate links or more than 5 tags.
Fix: Reduce tags to 5 or fewer relevant terms. Disclose all affiliate links with “(affiliate link)” notation. Link affiliate URLs directly without cloaking.
Check: Does the plugin bundle its own copies of libraries that WordPress already ships?
Violation signals:
jquery.js, jquery.min.js, or loads jQuery from a CDNPHPMailer, SimplePie, PHPass, Backbone, Underscore, React, wp-polyfill, or other WP-bundled librarieswp_enqueue_script() registers a library already available as a WordPress handle (check Default Scripts (opens in a new tab))files_library_core and known core library filename matches (see scanner known-libraries.php)Code patterns (violation vs compliant):
// VIOLATION — loading custom/bundled jQuery copy
wp_enqueue_script(
'myplugin-jquery',
plugins_url( 'assets/vendor/jquery-3.7.1.min.js', __FILE__ ),
array(),
'3.7.1',
true
);
// COMPLIANT — use WordPress-bundled jQuery handle
wp_enqueue_script( 'jquery' );// VIOLATION — bundling WP core PHP libs directly
require_once __DIR__ . '/vendor/PHPMailer.php';
require_once __DIR__ . '/vendor/SimplePie.php';
// COMPLIANT — use WordPress APIs that rely on core libs
wp_mail( $to, $subject, $message, $headers );
$feed = fetch_feed( $feed_url );// VIOLATION — registering local copy for a core-shipped package
wp_register_script(
'myplugin-underscore',
plugins_url( 'assets/vendor/underscore.min.js', __FILE__ ),
array(),
'1.13.6',
true
);
// COMPLIANT — rely on core handle
wp_enqueue_script( 'underscore' );Review questions:
jquery, underscore, backbone, codemirror, moment, PHPMailer, SimplePie)?wp_mail, fetch_feed) rather than bundled core libs?Verdict: Flag as FAIL for each duplicate bundled library.
Fix: Replace bundled copies with wp_enqueue_script( 'jquery' ) (or the appropriate WP handle). Remove the local copy from the plugin package.
Pre-submission checklist:
Check: Are SVN commits release-quality and infrequent?
Violation signals:
var_dump(), error_log(), console.log( 'test' ))Note: This guideline is primarily advisory; violations do not block submission but reflect poorly on the developer.
Fix: Use a development branch (GitHub/GitLab) and commit to SVN only for releases. Each SVN commit should correspond to a version bump.
Check: Is the version number in readme.txt and the plugin header incremented for every release?
Violation signals:
Stable tag: in readme.txt does not match the Version: field in the main plugin fileStable tag: trunk used (discouraged; use an explicit version number)Code patterns (violation vs compliant):
// VIOLATION — readme/plugin header mismatch
readme.txt:
Stable tag: 1.4.0
my-plugin.php header:
Version: 1.3.9// COMPLIANT — values are aligned and bumped together
readme.txt:
Stable tag: 1.4.1
my-plugin.php header:
Version: 1.4.1// VIOLATION — releasing functional changes without version increment
SVN tag: tags/1.4.1
Current release code: changed features, still Version: 1.4.1
// COMPLIANT — each functional release gets a new version tag
SVN tags: tags/1.4.1 -> tags/1.4.2Review questions:
readme.txt Stable tag exactly match main plugin Version?Verdict: Flag as FAIL if Stable tag and plugin header Version do not match.
Fix: Bump both values together on every release. Tag the release in SVN under tags/X.Y.Z.
Pre-submission checklist:
Stable tag matches plugin header Versiontags/X.Y.Z)Check: Is the plugin functional and complete at the time of submission?
Violation signals:
Verdict: Flag as FAIL. An incomplete plugin cannot be approved.
Fix: Submit only when the plugin is feature-complete and functional for end users.
Check: Does the plugin name or slug start with a trademark or project name the developer does not own?
Violation signals:
woocommerce-, elementor-, jetpack-, yoast-, or any term in the Trademark Slug List (see Naming Rules section below)-Press from WordPress)Correct pattern: Trademark may only appear after a connector word: for, with, using, and.
Pricing Rates for WooCommerceWooCommerce Pricing RatesVerdict: Flag as FAIL with the specific trademark and the correct name structure.
Fix: Move the trademark to after a connector. Rename the slug accordingly (max 50 chars, lowercase, hyphens only).
Note: This guideline is informational — no code or readme check is required. It establishes that WordPress.org may update guidelines, remove plugins, revoke access, or modify plugins for public safety at any time. Inform developers of this when advising on submission strategy.