initial
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: wp-plugin-development
|
||||
description: "Use when developing WordPress plugins: architecture and hooks, activation/deactivation/uninstall, admin UI and Settings API, data storage, cron/tasks, security (nonces/capabilities/sanitization/escaping), and release packaging."
|
||||
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
|
||||
---
|
||||
|
||||
# WP Plugin Development
|
||||
|
||||
## When to use
|
||||
|
||||
Use this skill for plugin work such as:
|
||||
|
||||
- creating or refactoring plugin structure (bootstrap, includes, namespaces/classes)
|
||||
- adding hooks/actions/filters
|
||||
- activation/deactivation/uninstall behavior and migrations
|
||||
- adding settings pages / options / admin UI (Settings API)
|
||||
- security fixes (nonces, capabilities, sanitization/escaping, SQL safety)
|
||||
- packaging a release (build artifacts, readme, assets)
|
||||
|
||||
## Inputs required
|
||||
|
||||
- Repo root + target plugin(s) (path to plugin main file if known).
|
||||
- Where this plugin runs: single site vs multisite; WP.com conventions if applicable.
|
||||
- Target WordPress + PHP versions (affects available APIs and placeholder support in `$wpdb->prepare()`).
|
||||
|
||||
## Procedure
|
||||
|
||||
### 0) Triage and locate plugin entrypoints
|
||||
|
||||
1. Run triage:
|
||||
- `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
|
||||
2. Detect plugin headers (deterministic scan):
|
||||
- `node skills/wp-plugin-development/scripts/detect_plugins.mjs`
|
||||
|
||||
If this is a full site repo, pick the specific plugin under `wp-content/plugins/` or `mu-plugins/` before changing code.
|
||||
|
||||
### 1) Follow a predictable architecture
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Keep a single bootstrap (main plugin file with header).
|
||||
- Avoid heavy side effects at file load time; load on hooks.
|
||||
- Prefer a dedicated loader/class to register hooks.
|
||||
- Keep admin-only code behind `is_admin()` (or admin hooks) to reduce frontend overhead.
|
||||
|
||||
See:
|
||||
- `references/structure.md`
|
||||
|
||||
### 2) Hooks and lifecycle (activation/deactivation/uninstall)
|
||||
|
||||
Activation hooks are fragile; follow guardrails:
|
||||
|
||||
- register activation/deactivation hooks at top-level, not inside other hooks
|
||||
- flush rewrite rules only when needed and only after registering CPTs/rules
|
||||
- uninstall should be explicit and safe (`uninstall.php` or `register_uninstall_hook`)
|
||||
|
||||
See:
|
||||
- `references/lifecycle.md`
|
||||
|
||||
### 3) Settings and admin UI (Settings API)
|
||||
|
||||
Prefer Settings API for options:
|
||||
|
||||
- `register_setting()`, `add_settings_section()`, `add_settings_field()`
|
||||
- sanitize via `sanitize_callback`
|
||||
|
||||
See:
|
||||
- `references/settings-api.md`
|
||||
|
||||
### 4) Security baseline (always)
|
||||
|
||||
Before shipping:
|
||||
|
||||
- Validate/sanitize input early; escape output late.
|
||||
- Use nonces to prevent CSRF *and* capability checks for authorization.
|
||||
- Avoid directly trusting `$_POST` / `$_GET`; use `wp_unslash()` and specific keys.
|
||||
- Use `$wpdb->prepare()` for SQL; avoid building SQL with string concatenation.
|
||||
|
||||
See:
|
||||
- `references/security.md`
|
||||
|
||||
### 5) Data storage, cron, migrations (if needed)
|
||||
|
||||
- Prefer options for small config; custom tables only if necessary.
|
||||
- For cron tasks, ensure idempotency and provide manual run paths (WP-CLI or admin).
|
||||
- For schema changes, write upgrade routines and store schema version.
|
||||
|
||||
See:
|
||||
- `references/data-and-cron.md`
|
||||
|
||||
## Verification
|
||||
|
||||
- Plugin activates with no fatals/notices.
|
||||
- Settings save and read correctly (capability + nonce enforced).
|
||||
- Uninstall removes intended data (and nothing else).
|
||||
- Run repo lint/tests (PHPUnit/PHPCS if present) and any JS build steps if the plugin ships assets.
|
||||
|
||||
## Failure modes / debugging
|
||||
|
||||
- Activation hook not firing:
|
||||
- hook registered incorrectly (not in main file scope), wrong main file path, or plugin is network-activated
|
||||
- Settings not saving:
|
||||
- settings not registered, wrong option group, missing capability, nonce failure
|
||||
- Security regressions:
|
||||
- nonce present but missing capability checks; or sanitized input not escaped on output
|
||||
|
||||
See:
|
||||
- `references/debugging.md`
|
||||
|
||||
## Escalation
|
||||
|
||||
For canonical detail, consult the Plugin Handbook and security guidelines before inventing patterns.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Data storage, cron, and upgrades
|
||||
|
||||
Use this file when adding persistent storage, background jobs, or upgrade routines.
|
||||
|
||||
## Data storage
|
||||
|
||||
- Prefer Options API for small config/state.
|
||||
- Use custom tables only when needed; store schema version and provide upgrade paths.
|
||||
|
||||
## Cron
|
||||
|
||||
- Ensure tasks are idempotent (may run late or multiple times).
|
||||
- Provide a manual trigger path for debugging (WP-CLI or admin-only action).
|
||||
|
||||
## Database safety note
|
||||
|
||||
If using `$wpdb->prepare()`, avoid building queries with concatenated user input.
|
||||
Recent WordPress versions support identifier placeholders (`%i`) but you must not assume it exists without checking capabilities or target versions.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Debugging quick routes
|
||||
|
||||
## Plugin doesn’t load / fatal errors
|
||||
|
||||
- Confirm correct plugin main file and header.
|
||||
- Check PHP error logs and `WP_DEBUG_LOG`.
|
||||
- If the repo is a site repo, confirm you edited the correct plugin under `wp-content/plugins/`.
|
||||
|
||||
## Activation hook surprises
|
||||
|
||||
- Hooks must be registered at top-level.
|
||||
- Activation runs in a special context; avoid assuming other hooks already ran.
|
||||
|
||||
## Settings not saving
|
||||
|
||||
- Confirm `register_setting()` is called.
|
||||
- Confirm the option group matches the form.
|
||||
- Confirm capability checks and nonces.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Activation, deactivation, uninstall
|
||||
|
||||
Use this file for lifecycle changes and data cleanup.
|
||||
|
||||
## Activation / deactivation hooks
|
||||
|
||||
- `register_activation_hook( __FILE__, 'callback' )`
|
||||
- `register_deactivation_hook( __FILE__, 'callback' )`
|
||||
|
||||
Guardrails:
|
||||
|
||||
- These hooks must be registered at top-level (not inside other hooks).
|
||||
- If you flush rewrite rules, ensure rules are registered first (often via a shared function called both on `init` and activation).
|
||||
|
||||
Upstream reference:
|
||||
|
||||
- https://developer.wordpress.org/plugins/plugin-basics/activation-deactivation-hooks/
|
||||
|
||||
## Uninstall
|
||||
|
||||
Preferred approaches:
|
||||
|
||||
- `uninstall.php` (runs only on uninstall)
|
||||
- `register_uninstall_hook()`
|
||||
|
||||
Guardrails:
|
||||
|
||||
- Check `WP_UNINSTALL_PLUGIN` before running destructive cleanup.
|
||||
|
||||
Upstream reference:
|
||||
|
||||
- https://developer.wordpress.org/plugins/plugin-basics/uninstall-methods/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Security guardrails (plugin work)
|
||||
|
||||
Use this file when making security fixes or when handling any input/output.
|
||||
|
||||
## Nonces + permissions
|
||||
|
||||
- Nonces help prevent CSRF, not authorization.
|
||||
- Always pair nonces with capability checks (`current_user_can()` or a more specific capability).
|
||||
|
||||
Upstream reference:
|
||||
|
||||
- https://developer.wordpress.org/apis/security/nonces/
|
||||
|
||||
## Sanitization and escaping
|
||||
|
||||
Golden rule:
|
||||
|
||||
- sanitize/validate on input, escape on output.
|
||||
|
||||
Practical rules:
|
||||
|
||||
- never process the entire `$_POST` / `$_GET` array; read explicit keys
|
||||
- use `wp_unslash()` before sanitizing when needed
|
||||
- use prepared statements for SQL; avoid interpolating user input into queries
|
||||
|
||||
Common review guidance:
|
||||
|
||||
- https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Settings API (admin options)
|
||||
|
||||
Use this file when adding settings pages or storing user-configurable options.
|
||||
|
||||
Core APIs:
|
||||
|
||||
- `register_setting()`
|
||||
- `add_settings_section()`
|
||||
- `add_settings_field()`
|
||||
|
||||
Upstream references:
|
||||
|
||||
- Settings API overview: https://developer.wordpress.org/plugins/settings/settings-api/
|
||||
- Register settings: https://developer.wordpress.org/plugins/settings/registration/
|
||||
- Add settings fields: https://developer.wordpress.org/plugins/settings/settings-fields/
|
||||
|
||||
Practical guardrails:
|
||||
|
||||
- Use `sanitize_callback` to validate/sanitize data.
|
||||
- Use capability checks (commonly `manage_options`) for settings screens and saves.
|
||||
- Escape values on output (`esc_attr`, `esc_html`, etc.).
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Plugin structure and loading
|
||||
|
||||
Use this file when introducing or refactoring a plugin architecture.
|
||||
|
||||
## Core concepts
|
||||
|
||||
- Main plugin file contains the plugin header and bootstraps the plugin.
|
||||
- Prefer predictable init:
|
||||
- minimal boot file
|
||||
- a loader/class that registers hooks
|
||||
- admin-only code behind admin hooks
|
||||
|
||||
Upstream reference:
|
||||
|
||||
- https://developer.wordpress.org/plugins/plugin-basics/
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_IGNORES = new Set([
|
||||
".git",
|
||||
"node_modules",
|
||||
"vendor",
|
||||
"dist",
|
||||
"build",
|
||||
"coverage",
|
||||
".next",
|
||||
".turbo",
|
||||
]);
|
||||
|
||||
function statSafe(p) {
|
||||
try {
|
||||
return fs.statSync(p);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readFileSafe(p, maxBytes = 128 * 1024) {
|
||||
try {
|
||||
const buf = fs.readFileSync(p);
|
||||
if (buf.byteLength > maxBytes) return buf.subarray(0, maxBytes).toString("utf8");
|
||||
return buf.toString("utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findFilesRecursive(repoRoot, predicate, { maxFiles = 6000, maxDepth = 10 } = {}) {
|
||||
const results = [];
|
||||
const queue = [{ dir: repoRoot, depth: 0 }];
|
||||
let visited = 0;
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { dir, depth } = queue.shift();
|
||||
if (depth > maxDepth) continue;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ent of entries) {
|
||||
const fullPath = path.join(dir, ent.name);
|
||||
if (ent.isDirectory()) {
|
||||
if (DEFAULT_IGNORES.has(ent.name)) continue;
|
||||
queue.push({ dir: fullPath, depth: depth + 1 });
|
||||
continue;
|
||||
}
|
||||
if (!ent.isFile()) continue;
|
||||
|
||||
visited += 1;
|
||||
if (visited > maxFiles) return { results, truncated: true };
|
||||
if (predicate(fullPath)) results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return { results, truncated: false };
|
||||
}
|
||||
|
||||
function parsePluginHeader(contents) {
|
||||
// WordPress reads plugin headers from the top of the file. We only need key fields.
|
||||
const header = {};
|
||||
const pairs = [
|
||||
["Plugin Name", "name"],
|
||||
["Plugin URI", "uri"],
|
||||
["Description", "description"],
|
||||
["Version", "version"],
|
||||
["Author", "author"],
|
||||
["Author URI", "authorUri"],
|
||||
["Text Domain", "textDomain"],
|
||||
["Domain Path", "domainPath"],
|
||||
];
|
||||
for (const [label, key] of pairs) {
|
||||
const m = contents.match(new RegExp(`^\\s*${label}:\\s*(.+)\\s*$`, "im"));
|
||||
if (m) header[key] = m[1].trim();
|
||||
}
|
||||
if (!header.name) return null;
|
||||
return header;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const repoRoot = process.cwd();
|
||||
|
||||
const { results: phpFiles, truncated } = findFilesRecursive(repoRoot, (p) => p.toLowerCase().endsWith(".php"), {
|
||||
maxFiles: 5000,
|
||||
maxDepth: 10,
|
||||
});
|
||||
|
||||
const plugins = [];
|
||||
|
||||
for (const phpPath of phpFiles) {
|
||||
const txt = readFileSafe(phpPath);
|
||||
if (!txt) continue;
|
||||
if (!/Plugin Name:/i.test(txt)) continue;
|
||||
const header = parsePluginHeader(txt);
|
||||
if (!header) continue;
|
||||
plugins.push({
|
||||
pluginFile: path.relative(repoRoot, phpPath),
|
||||
...header,
|
||||
});
|
||||
}
|
||||
|
||||
const report = {
|
||||
tool: { name: "detect_plugins", version: "0.1.0" },
|
||||
repoRoot,
|
||||
truncated,
|
||||
count: plugins.length,
|
||||
plugins,
|
||||
};
|
||||
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
Reference in New Issue
Block a user