*/ public static function all() { if ( null !== self::$cache ) { return self::$cache; } $docs = [ /* ---------------------------------------------------------- */ /* Core */ /* ---------------------------------------------------------- */ 'dashboard' => [ 'summary' => __( 'Your security overview: the current protection status, activity from the last 7 days, recent events and the result of the most recent scan.', 'gemsecurity' ), 'does' => [ __( 'Summarises which protections are on and which are still off.', 'gemsecurity' ), __( 'Charts blocked requests, failed logins and bans over the last 7 days.', 'gemsecurity' ), __( 'Lists the newest activity-log events so problems surface early.', 'gemsecurity' ), __( 'Shows when the site was last scanned and what the scan found.', 'gemsecurity' ), ], 'setup' => [ __( 'Open Modules and turn on the feature areas this site needs — everything else stays dormant.', 'gemsecurity' ), __( 'Work through Firewall, Login Security and Scanner first; they cover the most common attacks.', 'gemsecurity' ), __( 'Set an alert address in Settings → Alerts & notifications so you hear about incidents without logging in.', 'gemsecurity' ), __( 'Run a first scan from the Scanner page to get a clean baseline to compare against later.', 'gemsecurity' ), ], 'notes' => [ __( 'Counters only cover events recorded while the relevant module was active.', 'gemsecurity' ), __( 'A quiet dashboard is not proof of safety — check the Scanner and Logs pages periodically.', 'gemsecurity' ), ], ], 'modules' => [ 'summary' => __( 'Turn feature areas on or off. A module that is off loads no code, registers no REST routes and shows no menu entry, so unused features cost nothing.', 'gemsecurity' ), 'does' => [ __( 'Activation is the coarse gate; the individual settings inside a module stay as fine-grained switches.', 'gemsecurity' ), __( 'Turning a module off immediately removes its page from both the plugin nav and the WordPress menu.', 'gemsecurity' ), __( 'Core modules (Dashboard, Modules, Logs, Settings) are always on and cannot be turned off.', 'gemsecurity' ), __( 'Pro modules show as locked until the Pro add-on is active; then they toggle like any other module.', 'gemsecurity' ), ], 'setup' => [ __( 'Switch on a module to reveal its page, then open that page and configure it.', 'gemsecurity' ), __( 'Leave modules you do not use switched off — it keeps the menu short and skips their code entirely.', 'gemsecurity' ), ], 'notes' => [ __( 'Turning a module off does not erase its settings; switching it back on restores them exactly as they were.', 'gemsecurity' ), __( 'On upgrade, activation was derived from your existing settings, so a module whose features were all off may start hidden.', 'gemsecurity' ), ], ], 'logs' => [ 'summary' => __( 'A tamper-evident audit trail of everything that matters: logins, user and role changes, plugin and theme activity, settings changes, blocks and bans.', 'gemsecurity' ), 'does' => [ __( 'Records security-relevant events with actor, IP, severity and timestamp.', 'gemsecurity' ), __( 'Lets you choose which activity is recorded — user accounts, plugins and themes, settings changes, content — while security events are always logged.', 'gemsecurity' ), __( 'Filters by severity, event type, user or IP, and exports the result as CSV.', 'gemsecurity' ), __( 'Sends real-time alerts for events above the severity you choose.', 'gemsecurity' ), __( 'Pro: hash-chains each entry so deleted or edited rows can be detected, and can forward events to a SIEM or syslog collector.', 'gemsecurity' ), ], 'setup' => [ __( 'Set how long entries are kept in Settings → General (log retention, in days).', 'gemsecurity' ), __( 'Use "What gets logged" at the top of this page to switch off any source you do not need — content is the noisiest on an active site.', 'gemsecurity' ), __( 'Choose an alert channel and minimum severity on the Notifications page.', 'gemsecurity' ), __( 'Pro: enable the hash chain in Settings → Compliance & SIEM, then use Verify to prove the log is intact.', 'gemsecurity' ), ], 'notes' => [ __( 'Retention trims old rows on a schedule — set it long enough to cover an investigation, short enough to keep the table small.', 'gemsecurity' ), __( 'Log verification only proves the chain from the point you enabled it, not events recorded before.', 'gemsecurity' ), ], ], 'notifications' => [ 'summary' => __( 'Where alerts go, what raises one, and the mail that arrives on a schedule. Every module feeds into this page.', 'gemsecurity' ), 'does' => [ __( 'Delivery: one security email address and one set of channels — Slack, Telegram or a generic webhook — used by every alert, report and failure notice the plugin sends, with a minimum-severity filter.', 'gemsecurity' ), __( 'Triggers: every event a module can alert on, in one list — administrator sign-ins, new devices, rogue and hidden administrators, and application passwords.', 'gemsecurity' ), __( 'Scheduled: a morning report listing every administrator and active plugin with anything that changed since yesterday, a weekly digest, and the daily security scan that finds most of it.', 'gemsecurity' ), ], 'setup' => [ __( 'Set the security email once — monitoring warnings, admin alerts and the daily report all use it. Then set the minimum severity so routine noise stays out of your inbox.', 'gemsecurity' ), __( 'Work down "What you get told about" and switch off anything you do not want to hear. A trigger whose module is switched off is not listed at all.', 'gemsecurity' ), __( 'Turn on the daily report to get a morning inventory of administrators and active plugins, with new ones called out — then use "Send a report now" to see what it looks like.', 'gemsecurity' ), ], 'notes' => [ __( 'Alerts are only as reliable as your site mail. If WordPress email is unreliable here, use Slack, Telegram or a webhook instead — use "Send a test email" to find out before an incident does.', 'gemsecurity' ), __( 'Each trigger also appears on the page of the module that raises it. Both write the same setting, so the two views can never disagree.', 'gemsecurity' ), __( 'Turning a trigger off does not stop the event being recorded — it is still in the activity log on the Logs page, you just are not mailed about it.', 'gemsecurity' ), __( 'Deactivating GemSecurity emails the alert address with a critical warning naming who did it, and pushes the same message to Slack, Telegram or your webhook — switching security off is the first move in most intrusions.', 'gemsecurity' ), __( 'Every email names the site and its domain in the subject, after an emoji for the event type — so alerts from several installs stay apart in one inbox.', 'gemsecurity' ), __( 'The first daily report has no baseline, so it lists the inventory without claiming everything is new. Comparisons start the next morning.', 'gemsecurity' ), ], ], 'settings' => [ 'summary' => __( 'Plugin-wide preferences: log retention, compliance and SIEM forwarding, maintenance lockdown, and settings import/export.', 'gemsecurity' ), 'does' => [ __( 'Compliance: log hash-chaining, SIEM/syslog forwarding and a severity floor for what gets shipped.', 'gemsecurity' ), __( 'Maintenance: site lockdown with a custom message, and (with the GemBackup plugin) taking a backup before plugin or theme updates.', 'gemsecurity' ), __( 'Settings import/export (Pro): move a whole configuration, including which modules are on, between sites.', 'gemsecurity' ), ], 'setup' => [ __( 'Set log retention to suit the site — a busy install fills the log quickly, and 0 keeps entries forever.', 'gemsecurity' ), __( 'Use lockdown while you investigate an incident: visitors see your message, administrators keep full access.', 'gemsecurity' ), ], 'notes' => [ __( 'Alerts, the daily report and the weekly digest are configured on the Notifications page, not here.', 'gemsecurity' ), __( 'What gets written to the activity log is configured on the Logs page, not here.', 'gemsecurity' ), __( 'Lockdown blocks the front end for visitors — remember to switch it back off.', 'gemsecurity' ), ], ], /* ---------------------------------------------------------- */ /* Firewall & threats */ /* ---------------------------------------------------------- */ 'firewall' => [ 'summary' => __( 'The front door: an IP blocklist, the request firewall and its rules, rate limiting, bot control, the classic WordPress hardening switches, browser security headers and a security.txt file.', 'gemsecurity' ), 'does' => [ __( 'Blocks individual IPs and CIDR ranges before WordPress does any real work.', 'gemsecurity' ), __( 'Inspects requests for SQL injection, XSS, path traversal and remote code execution, and can also inspect POST bodies.', 'gemsecurity' ), __( 'Rate limits front-end requests per IP and throttles 404 floods from vulnerability scanners.', 'gemsecurity' ), __( 'Hardening: disable XML-RPC, disable the theme/plugin file editor, hide the WordPress version.', 'gemsecurity' ), __( 'Username enumeration: block ?author=N scans, hide the REST users endpoint and return one generic login error.', 'gemsecurity' ), __( 'Sends security headers — X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy, HSTS and CSP — and collects CSP violation reports.', 'gemsecurity' ), __( 'Bot control: block known-bad scanners and empty user agents, block AI crawlers, and verify self-declared search engines by reverse DNS so fake Googlebots are turned away.', 'gemsecurity' ), __( 'Publishes /.well-known/security.txt and audits file permissions for world-writable paths.', 'gemsecurity' ), __( 'With Pro, the hardening pack adds an admin IP allowlist, sensitive-file blocking, no PHP execution in uploads and forced HTTPS.', 'gemsecurity' ), ], 'setup' => [ __( 'Turn the firewall on, then enable the hardening switches one at a time and check the site still works after each.', 'gemsecurity' ), __( 'Turn the WAF on and leave the four rule families enabled; add POST body inspection only if you can watch for false positives on your own forms.', 'gemsecurity' ), __( 'Only enable the admin IP allowlist if your address is static, and add it before saving.', 'gemsecurity' ), __( 'If the site sits behind Cloudflare or another proxy, enable the matching option under Real visitor IP detection — otherwise every visitor looks like the proxy and blocks or rate limits hit everyone at once.', 'gemsecurity' ), __( 'Enable security headers, leave CSP in report-only mode, then read Recent CSP violations for a few days before enforcing it.', 'gemsecurity' ), __( 'Fill in a contact address under security.txt so researchers know where to report problems.', 'gemsecurity' ), ], 'notes' => [ __( 'Disabling XML-RPC breaks the classic WordPress mobile app, Jetpack-style remote publishing and some pingback workflows.', 'gemsecurity' ), __( 'HSTS tells browsers to refuse plain HTTP for the max-age you set — only enable it once HTTPS works on every subdomain you include, and treat preload as permanent.', 'gemsecurity' ), __( 'A CSP that is enforced too early will silently break embeds, analytics and page builders. Report-only first.', 'gemsecurity' ), __( 'Never trust proxy headers unless the site really is behind that proxy — spoofed headers would let an attacker rotate their apparent IP at will.', 'gemsecurity' ), __( 'A dynamic home IP plus an admin IP allowlist means locking yourself out the next time your router reconnects.', 'gemsecurity' ), __( 'Comment controls live on the Anti-Spam page, and application passwords on API & Access.', 'gemsecurity' ), ], ], 'anti-spam' => [ 'summary' => __( 'Every comment and registration control in one place: close comments entirely, or stop spam with invisible bot checks, an optional CAPTCHA and a heuristic score for anything that gets through.', 'gemsecurity' ), 'does' => [ __( 'Closes comments and pingbacks site-wide if the site does not use them.', 'gemsecurity' ), __( 'Adds a hidden honeypot field and a minimum time-to-submit, both invisible to real visitors.', 'gemsecurity' ), __( 'Optionally adds the CAPTCHA configured on the CAPTCHA page to the comment form as well.', 'gemsecurity' ), __( 'Scores comments on link count and keyword matches, holding or rejecting the worst.', 'gemsecurity' ), ], 'setup' => [ __( 'If the site does not take comments, disable them entirely — nothing else on this page then matters.', 'gemsecurity' ), __( 'Otherwise turn on silent bot protection — no keys or accounts needed.', 'gemsecurity' ), __( 'Leave the minimum submit time at a few seconds; raise it only if spam still gets through.', 'gemsecurity' ), __( 'Enable heuristic filtering, set the maximum number of links you consider normal, and add keywords you keep seeing in spam.', 'gemsecurity' ), ], 'notes' => [ __( 'Aggressive keyword lists cause false positives — check the held queue for a while after adding new terms.', 'gemsecurity' ), __( 'Heavily cached sites can serve a stale form timestamp; if legitimate comments are refused, lower the minimum submit time.', 'gemsecurity' ), __( 'The silent checks stop nearly all automated spam on their own. Add the comment CAPTCHA only if they are not enough — it costs every real visitor a puzzle.', 'gemsecurity' ), ], ], 'threats' => [ 'summary' => __( 'Active defence: a request firewall that inspects traffic for attack patterns, rate limits and 404 throttling, escalating auto-bans, and live traffic visibility.', 'gemsecurity' ), 'does' => [ __( 'Inspects requests for SQL injection, XSS, local file inclusion and remote code execution patterns, optionally including the request body.', 'gemsecurity' ), __( 'Rate limits requests and throttles 404 floods — the usual signature of a vulnerability scanner.', 'gemsecurity' ), __( 'Blocks known-bad bots, empty user agents and your own custom patterns.', 'gemsecurity' ), __( 'Escalates repeat offenders from strikes to a temporary ban and then a permanent one, all in a ban ledger you can edit.', 'gemsecurity' ), __( 'Detects request anomalies and credential-stuffing bursts, and shows requests as they arrive in Live traffic.', 'gemsecurity' ), ], 'setup' => [ __( 'Enable the firewall and leave all four attack classes on unless a legitimate integration trips one.', 'gemsecurity' ), __( 'Enable the early-load loader so banned requests are rejected before other plugins run.', 'gemsecurity' ), __( 'Set the auto-ban threshold, window and duration to match your traffic — start lenient and tighten later.', 'gemsecurity' ), __( 'Turn on Live traffic while tuning, then turn it off again; it records every request and adds write load.', 'gemsecurity' ), ], 'notes' => [ __( 'Body inspection reads POST payloads, which costs a little performance on write-heavy sites.', 'gemsecurity' ), __( 'Rate limits apply per IP — verify the proxy settings on the Firewall page first, or all traffic will share one bucket.', 'gemsecurity' ), __( 'The early loader fails open: if it cannot run, requests are allowed rather than the site going down.', 'gemsecurity' ), __( 'The early loader doubles as a watchdog: it is removed on a normal deactivation, so finding it still installed while GemSecurity is inactive means the plugin was disabled outside WordPress — the database edited, or the folder renamed — and it emails a warning about exactly that.', 'gemsecurity' ), __( 'Whitelist your own address before tightening thresholds so a testing burst cannot lock you out.', 'gemsecurity' ), ], ], 'plugin-guard' => [ 'summary' => __( 'An allowlist for code: only plugins verified against WordPress.org or approved by you can run or be installed.', 'gemsecurity' ), 'does' => [ __( 'Checks every installed plugin against the official WordPress.org checksums for its version — a lookalike with a familiar folder name but edited or extra PHP files fails.', 'gemsecurity' ), __( 'Shows which plugins and themes are on WordPress.org and which come from somewhere else.', 'gemsecurity' ), __( 'With enforcement on, an untrusted plugin switched on through the database is stopped before it loads, deactivated, logged and reported by a DANGER email.', 'gemsecurity' ), __( 'Unknown or edited must-use plugins are moved to quarantine before WordPress runs them.', 'gemsecurity' ), __( 'Installing a plugin or theme that is not from WordPress.org is refused until you pre-approve its folder slug.', 'gemsecurity' ), ], 'setup' => [ __( 'Open Plugin Guard and press Re-check to verify everything.', 'gemsecurity' ), __( 'Turn on enforcement, tick the premium/custom plugins and must-use plugins you recognise, and confirm. Untick anything you do not recognise.', 'gemsecurity' ), __( 'Before uploading a new premium plugin, add its folder slug under Pre-approve.', 'gemsecurity' ), ], 'notes' => [ __( 'Plugins are trusted by folder slug, so a premium plugin keeps its trust across updates. Must-use plugins are trusted by content hash, so any edit removes their trust.', 'gemsecurity' ), __( 'Themes are matched by name only — WordPress.org publishes no theme checksums.', 'gemsecurity' ), __( 'Emergency off switch: add define( \'GEMSECURITY_PLUGIN_GUARD_OFF\', true ); to wp-config.php.', 'gemsecurity' ), __( 'Plugins activated from WP-CLI are still checked on the next page load.', 'gemsecurity' ), ], ], 'virtual-patch' => [ 'summary' => __( 'Blocks exploitation of known plugin and theme vulnerabilities at the request level, so you are covered in the gap between a disclosure and the vendor’s fix.', 'gemsecurity' ), 'does' => [ __( 'Matches mitigation rules against the plugins and versions actually installed here.', 'gemsecurity' ), __( 'Applies only the relevant rules, so an unrelated vulnerability costs nothing.', 'gemsecurity' ), __( 'Lists every mitigation that is switched on and blocking attacks right now.', 'gemsecurity' ), ], 'setup' => [ __( 'Turn virtual patching on; matching rules apply immediately, with no configuration per vulnerability.', 'gemsecurity' ), __( 'Review Active mitigations after each plugin update — a mitigation disappearing usually means you updated past the vulnerable version.', 'gemsecurity' ), ], 'notes' => [ __( 'A virtual patch buys time; it is not a substitute for updating the vulnerable plugin.', 'gemsecurity' ), __( 'The list is what is protecting you, not what is wrong. Two generic rules — SQL injection and path traversal — apply to every request on every site and are always present; a severity of "critical" describes the attack they block, not a problem found here.', 'gemsecurity' ), __( 'Developers can add site-specific rules through the gemsecurity/vpatch_rules filter.', 'gemsecurity' ), ], ], /* ---------------------------------------------------------- */ /* Login & access */ /* ---------------------------------------------------------- */ 'login-security' => [ 'summary' => __( 'Everything around the login form: brute-force lockouts, password rules, session limits, a renamed login URL and admin login alerts.', 'gemsecurity' ), 'does' => [ __( 'Locks out an IP after a number of failed attempts, for a duration you set.', 'gemsecurity' ), __( 'Emails you when an administrator signs in, and ends administrator sessions after a set number of hours.', 'gemsecurity' ), __( 'Enforces a password policy: minimum length, complexity, a breached-password check and optional expiry.', 'gemsecurity' ), __( 'Moves wp-login.php to a URL of your choosing and 404s the old one, and logs idle users out.', 'gemsecurity' ), __( 'Pro: remembers trusted devices and offers magic-link recovery.', 'gemsecurity' ), ], 'setup' => [ __( 'Start with brute-force protection — a limit of 5 attempts and a 15–30 minute lockout suits most sites.', 'gemsecurity' ), __( 'If you enable a custom login URL, save the new address somewhere safe before logging out.', 'gemsecurity' ), __( 'Set the password policy before onboarding new users; existing passwords are only checked at the next change.', 'gemsecurity' ), __( 'Add a bot challenge on the CAPTCHA page as well — lockouts stop repeat guesses, a CAPTCHA stops the automation making them.', 'gemsecurity' ), ], 'notes' => [ __( 'A custom login URL is obscurity, not authentication — keep brute-force protection and two-factor on as well.', 'gemsecurity' ), __( 'Short idle timeouts on a site with long editing sessions will cost people their unsaved work.', 'gemsecurity' ), __( 'Lockouts are per IP, so several users behind one office connection share a bucket.', 'gemsecurity' ), ], ], 'captcha' => [ 'summary' => __( 'A bot challenge in front of the forms strangers can submit: login, registration, lost password and comments. Supports Google reCAPTCHA v2 and v3, hCaptcha and Cloudflare Turnstile.', 'gemsecurity' ), 'does' => [ __( 'Renders the provider widget on each form you select and verifies the token server-side before the submission is accepted.', 'gemsecurity' ), __( 'Scores rather than challenges when reCAPTCHA v3 is selected, rejecting anything below the threshold you set.', 'gemsecurity' ), __( 'Records every failed challenge in the activity log.', 'gemsecurity' ), ], 'setup' => [ __( 'Pick a provider, then follow the steps shown for it to create a site key and secret key.', 'gemsecurity' ), __( 'Paste both keys in and use Test CAPTCHA keys — it renders the real widget and verifies a real token, which is the only way to prove the pair before it reaches the login form.', 'gemsecurity' ), __( 'Choose which forms the challenge guards. Login and registration are the ones bots actually hammer.', 'gemsecurity' ), __( 'For the comment form, turn the CAPTCHA on under Anti-Spam, where the rest of the comment controls live.', 'gemsecurity' ), ], 'notes' => [ __( 'The key pairs are not interchangeable between providers, or between reCAPTCHA v2 and v3 — switching provider means new keys.', 'gemsecurity' ), __( 'reCAPTCHA v3 scores rather than challenges: lower the threshold if real users get rejected.', 'gemsecurity' ), __( 'If the provider rejects the secret key outright, verification is skipped and a critical entry is written to the log rather than locking every user — administrators included — out of a form nobody can pass.', 'gemsecurity' ), __( 'A CAPTCHA costs every real visitor a puzzle. On the comment form, the silent checks under Anti-Spam usually suffice on their own.', 'gemsecurity' ), ], ], 'two-factor' => [ 'summary' => __( 'A second factor at login: app-based one-time codes with an email fallback, one-time backup codes, and enforcement per role.', 'gemsecurity' ), 'does' => [ __( 'Supports authenticator apps (TOTP) and email one-time codes.', 'gemsecurity' ), __( 'Issues single-use backup codes for when the phone is not to hand.', 'gemsecurity' ), __( 'Requires two-factor for the roles you select, falling back to email codes so nobody is locked out.', 'gemsecurity' ), ], 'setup' => [ __( 'Enable the two-factor system, then set up your own account first under My two-factor.', 'gemsecurity' ), __( 'Scan the QR code with an authenticator app and confirm one generated code to finish enrolment.', 'gemsecurity' ), __( 'Download the backup codes and store them outside the site.', 'gemsecurity' ), __( 'Only once your own login works, require two-factor for administrators and any other roles.', 'gemsecurity' ), ], 'notes' => [ __( 'TOTP depends on a correct server clock; a badly drifted clock rejects every valid code.', 'gemsecurity' ), __( 'Emailed codes last 10 minutes and allow five guesses; after that the code is discarded and a new one must be requested. The message says which of those happened rather than always reporting an invalid code.', 'gemsecurity' ), __( 'Email fallback is only as secure as the mailbox — use the app method for administrators.', 'gemsecurity' ), ], ], 'social-login' => [ 'summary' => __( 'Lets people sign in with Google, Facebook, GitHub or X instead of a password, with optional account creation.', 'gemsecurity' ), 'does' => [ __( 'Adds provider buttons to the login form using OAuth 2.0.', 'gemsecurity' ), __( 'Links a social account to an existing user by email address, or creates one with a default role.', 'gemsecurity' ), __( 'Fires the normal WordPress login hooks, so two-factor and login alerts still apply.', 'gemsecurity' ), ], 'setup' => [ __( 'Create an OAuth app with the provider and copy its client ID and secret.', 'gemsecurity' ), __( 'Paste the redirect URI shown on this page into the provider’s allowed-callback list exactly as displayed.', 'gemsecurity' ), __( 'Enable the provider, then decide whether new visitors may register and which role they receive.', 'gemsecurity' ), __( 'Test in a private window before announcing it.', 'gemsecurity' ), ], 'notes' => [ __( 'Linking by email trusts the provider’s email — only enable it for providers that verify addresses.', 'gemsecurity' ), __( 'If sign-in fails, the message now names the cause: "the provider refused" with its own error (redirect_uri_mismatch means the URI above is not registered exactly, invalid_client means the secret is wrong), while "expired or already used" means the attempt took too long or the browser dropped the cookie. Every failure is also written to the activity log.', 'gemsecurity' ), __( 'Never grant a role above Subscriber by default to self-registered accounts.', 'gemsecurity' ), ], ], 'magic-login' => [ 'summary' => __( 'Lets people sign in without a password by opening a one-time link sent to their email address.', 'gemsecurity' ), 'does' => [ __( 'Adds an "Email me a sign-in link" option to the login form.', 'gemsecurity' ), __( 'Emails a single-use link that expires after the time you set; only a hash of it is stored.', 'gemsecurity' ), __( 'Asks for confirmation before signing in, so email scanners that open links cannot use them up.', 'gemsecurity' ), __( 'Always answers the same way, so it cannot be used to find out which accounts exist, and limits requests per account and per address.', 'gemsecurity' ), ], 'setup' => [ __( 'Turn magic login on and choose how long a link stays valid.', 'gemsecurity' ), __( 'Optionally limit it to certain roles. Administrator-level accounts are excluded unless you allow them.', 'gemsecurity' ), __( 'Leave "Require two-factor" on so users with two-factor still get the second prompt.', 'gemsecurity' ), __( 'Check that your site sends email reliably before relying on it.', 'gemsecurity' ), ], 'notes' => [ __( 'Anyone who can read a user’s mailbox can sign in as them — keep administrators off unless their email is protected by two-factor.', 'gemsecurity' ), __( 'The two-factor requirement only applies while the Two-Factor system is switched on.', 'gemsecurity' ), ], ], 'temp-access' => [ 'summary' => __( 'Gives an existing user a role for a limited time and takes it back by itself, so nobody keeps access they no longer need.', 'gemsecurity' ), 'does' => [ __( 'Changes the role of a user you pick until the time you choose; their previous roles are stored and put back when it ends.', 'gemsecurity' ), __( 'Ends access at the expiry time on every request, not just on a schedule, and ends the user’s sessions when access expires or is revoked.', 'gemsecurity' ), __( 'Never creates accounts and never signs anyone in: the user already exists and signs in the normal way, with every login protection you have turned on.', 'gemsecurity' ), __( 'Records grants, extensions, expiry and revocation in each user’s history and in the Logs.', 'gemsecurity' ), ], 'setup' => [ __( 'Add the person under Users → Add New first, with a role that has no special access.', 'gemsecurity' ), __( 'Open Temporary Access, choose Grant access, then search for the user and pick a role and duration.', 'gemsecurity' ), __( 'Pick the least-privileged role that gets the job done; roles that can change the site need an explicit confirmation.', 'gemsecurity' ), __( 'Choose which events email the security notification address.', 'gemsecurity' ), ], 'notes' => [ __( 'Users who already have full administrator access cannot be given temporary access.', 'gemsecurity' ), __( 'While access is active the user cannot create, promote or delete users or switch GemSecurity off.', 'gemsecurity' ), __( 'Ended grants are kept for the audit trail.', 'gemsecurity' ), ], ], 'geolocation' => [ 'summary' => __( 'Country-level access control: allow logins only from countries you expect, optionally block the whole site, and pin individual users to their own countries.', 'gemsecurity' ), 'does' => [ __( 'Resolves visitor country from CDN headers, falling back to a lookup API with a 24-hour cache.', 'gemsecurity' ), __( 'Applies your country list as a login gate, a whole-site block, or both.', 'gemsecurity' ), __( 'Locks chosen users to specific countries and can email-verify a login from a new one.', 'gemsecurity' ), ], 'setup' => [ __( 'Add the countries your team and customers actually log in from — include your own, and any you travel to.', 'gemsecurity' ), __( 'Start with the login gate only. Blocking the whole site also blocks customers and search engines.', 'gemsecurity' ), __( 'Decide what happens to unknown locations; blocking them is safe for a login gate, risky site-wide.', 'gemsecurity' ), ], 'notes' => [ __( 'Lookups fail open: if the country cannot be resolved, the visitor is allowed rather than locked out.', 'gemsecurity' ), __( 'VPNs and mobile networks routinely resolve to another country — expect occasional surprises.', 'gemsecurity' ), __( 'The GEMSEC_GEO_DISABLE constant is the emergency exit if you lock yourself out.', 'gemsecurity' ), ], ], 'admin-guard' => [ 'summary' => __( 'Watches the administrator role itself: only approved accounts stay administrators, and hidden administrators are surfaced.', 'gemsecurity' ), 'does' => [ __( 'Keeps an approved-administrator list and demotes or deletes any account that appears outside it.', 'gemsecurity' ), __( 'Refuses the sign-in of an administrator who is not on the list, and ends an existing session belonging to one — so an account added straight to the database, or created while this module was off, still cannot get in.', 'gemsecurity' ), __( 'Sweeps the administrators that already exist — hourly and whenever you change the whitelist — so an unapproved account is removed even if it never signs in.', 'gemsecurity' ), __( 'Detects administrators hidden from the users list — a classic backdoor left behind after a breach.', 'gemsecurity' ), __( 'Emails you when either fires.', 'gemsecurity' ), ], 'setup' => [ __( 'Review the approved list and confirm every account on it is one you recognise.', 'gemsecurity' ), __( 'Choose what happens to a rogue administrator: demote is the safe default, delete is irreversible.', 'gemsecurity' ), __( 'Use "Check for unapproved admins" before relying on it: the preview lists exactly who would be removed, which is worth seeing while the action is set to delete.', 'gemsecurity' ), __( 'Enable both notifications so you hear about it even if you are not logged in.', 'gemsecurity' ), ], 'notes' => [ __( 'Add legitimate new administrators to the list before creating them, or they will be demoted straight away.', 'gemsecurity' ), __( 'Some membership and staffing plugins create elevated accounts on purpose — check before switching to delete.', 'gemsecurity' ), __( 'Login enforcement needs a non-empty list: while the list is empty every administrator would count as unapproved, so the check stays off rather than locking you out.', 'gemsecurity' ), __( 'User ID 1, the site-owner address and multisite super admins are never blocked, whatever the list says.', 'gemsecurity' ), ], ], 'api-hardening' => [ 'summary' => __( 'Closes the paths that bypass the login form: REST API rate limiting and application-password governance.', 'gemsecurity' ), 'does' => [ __( 'Rate limits REST requests per IP within a window you choose. Front-end request rate limiting is on the Firewall page.', 'gemsecurity' ), __( 'Lists every existing application password, can disable them entirely, alerts when a new one is created, and flags ones older than your maximum age.', 'gemsecurity' ), ], 'setup' => [ __( 'Review the existing application passwords and revoke any you do not recognise.', 'gemsecurity' ), __( 'If nothing here uses application passwords, disable them outright.', 'gemsecurity' ), __( 'Turn on REST rate limiting, then confirm your site builder, mobile app or headless front end still works.', 'gemsecurity' ), ], 'notes' => [ __( 'Application passwords are how most external integrations authenticate — check before disabling them.', 'gemsecurity' ), __( 'REST limits apply to logged-in editors too, so keep the ceiling above what your own admin work generates.', 'gemsecurity' ), ], ], /* ---------------------------------------------------------- */ /* Scanning */ /* ---------------------------------------------------------- */ 'scanner' => [ 'summary' => __( 'Finds what should not be there: modified core files, foreign or suspicious files, changed files, risky outbound links, known vulnerabilities and auto-executed code — plus, with Pro, the malware suite and auto-update enforcement.', 'gemsecurity' ), 'does' => [ __( 'Verifies core files against the official WordPress.org checksums and lists unexpected or modified ones.', 'gemsecurity' ), __( 'Keeps one file-change baseline over the active theme, must-use plugins and drop-ins, and flags anything added, edited or removed. Pro widens the same baseline to core, all plugins and all themes.', 'gemsecurity' ), __( 'With Pro, auto-updates plugins and themes — either only the ones the vulnerability scan flagged, or everything.', 'gemsecurity' ), __( 'Scans content for outbound links to known-bad destinations.', 'gemsecurity' ), __( 'Checks installed plugins and themes against known vulnerabilities and against what WordPress.org actually ships.', 'gemsecurity' ), __( 'Inspects mu-plugins and drop-ins — code that runs on every request without appearing on the Plugins screen.', 'gemsecurity' ), __( 'Pro: full malware scanning with quarantine and a recovery toolkit.', 'gemsecurity' ), __( 'The malware scan covers the whole install — the web root, wp-includes, wp-admin and wp-content — and reads PHP, JavaScript and .htaccess, not PHP alone.', 'gemsecurity' ), ], 'setup' => [ __( 'Run a full scan now to establish a clean baseline.', 'gemsecurity' ), __( 'Enable automatic scanning so it repeats on a schedule instead of when you remember.', 'gemsecurity' ), __( 'Turn on the file-change watcher and the vulnerability check.', 'gemsecurity' ), __( 'Work through the findings, then rescan so the list reflects the fixed state.', 'gemsecurity' ), ], 'notes' => [ __( 'Scans read every file and are I/O heavy — schedule them for a quiet hour on large sites.', 'gemsecurity' ), __( 'Premium plugins are not on WordPress.org, so integrity checks cannot confirm them; that is expected, not a finding.', 'gemsecurity' ), __( 'Caching and backup plugins legitimately write files constantly; expect noise from them in the change watcher.', 'gemsecurity' ), __( 'Take a backup before deleting or quarantining anything a scan flags.', 'gemsecurity' ), __( 'An index.php holding only "// Silence is golden." is skipped automatically — it has no executable code, and reporting it would only teach you to ignore the list. Anything else you have checked yourself can be marked safe from the findings table.', 'gemsecurity' ), __( 'A safe mark covers the exact contents you reviewed. If that file is later modified it is reported again, so marking something can never become a permanent blind spot.', 'gemsecurity' ), ], ], 'payment-integrity' => [ 'summary' => __( 'Watches the scripts on your checkout pages so a skimmer injected into payment forms is caught, as PCI DSS 4.0 requires.', 'gemsecurity' ), 'does' => [ __( 'Builds an inventory of every script loaded on the pages you nominate.', 'gemsecurity' ), __( 'Compares each page load against that authorised baseline and reports anything new.', 'gemsecurity' ), __( 'Flags unauthorised scripts so you can review them before customers are affected.', 'gemsecurity' ), ], 'setup' => [ __( 'Enable the monitor and select your checkout and payment pages.', 'gemsecurity' ), __( 'Visit those pages so the inventory populates, including any scripts that only load at checkout.', 'gemsecurity' ), __( 'Review the inventory and authorise each script you recognise; anything left over is worth investigating.', 'gemsecurity' ), __( 'Re-check after adding an analytics, chat or A/B testing tool — they add scripts by design.', 'gemsecurity' ), ], 'notes' => [ __( 'Tag managers load scripts dynamically, so expect entries you did not add by hand.', 'gemsecurity' ), __( 'This detects and reports; it does not block. Treat an unexpected script as an incident.', 'gemsecurity' ), ], ], 'incident-response' => [ 'summary' => __( 'The page for the worst day: a forensic timeline of what happened, file quarantine, and the recovery actions that stop an attack in progress.', 'gemsecurity' ), 'does' => [ __( 'Builds a timeline from the audit log so you can see the sequence of an intrusion.', 'gemsecurity' ), __( 'Quarantines suspicious files out of the web root instead of deleting them, so they can be restored or examined.', 'gemsecurity' ), __( 'Logs every user out at once, which invalidates stolen session cookies.', 'gemsecurity' ), __( 'Regenerates the wp-config.php keys and salts, so every auth cookie on the site stops working at once.', 'gemsecurity' ), __( 'Rolls the site back to the most recent completed backup (needs the GemBackup plugin).', 'gemsecurity' ), __( 'Shows the whole site’s quarantine, including anything the malware scanner moved, so nothing is stranded in a list you cannot see.', 'gemsecurity' ), ], 'setup' => [ __( 'Nothing to configure in advance — but do make sure logging and alerts are on, because the timeline is built from them.', 'gemsecurity' ), __( 'During an incident: log everyone out, regenerate the keys and salts, quarantine what the scanner flagged, then read the timeline from the earliest suspicious event.', 'gemsecurity' ), __( 'Reset passwords and revoke application passwords once you know how they got in.', 'gemsecurity' ), ], 'notes' => [ __( 'Logging everyone out includes you; keep your credentials to hand.', 'gemsecurity' ), __( 'Quarantined files stay on disk in a protected folder — keep them until the investigation is finished.', 'gemsecurity' ), __( 'The timeline can only show what was recorded, which is why retention matters before an incident, not after.', 'gemsecurity' ), ], ], /* ---------------------------------------------------------- */ /* Maintenance */ /* ---------------------------------------------------------- */ 'monitoring' => [ 'summary' => __( 'Watches the things that break quietly: uptime, TLS certificate expiry, DNS changes and SPF/DMARC email authentication.', 'gemsecurity' ), 'does' => [ __( 'Checks the site is reachable and reports when it is not.', 'gemsecurity' ), __( 'Warns before the TLS certificate expires rather than after.', 'gemsecurity' ), __( 'Notices DNS record changes, which often precede or follow a domain hijack.', 'gemsecurity' ), __( 'Verifies SPF and DMARC records so your mail keeps arriving and is harder to spoof.', 'gemsecurity' ), ], 'setup' => [ __( 'Enable monitoring. Warnings go to the security email on the Notifications page.', 'gemsecurity' ), __( 'Review the Status panel after the first daily run and fix anything already flagged.', 'gemsecurity' ), ], 'notes' => [ __( 'Checks run on a daily schedule, so this is early warning rather than second-by-second uptime monitoring.', 'gemsecurity' ), __( 'Set the security email to an address on a different domain, so a mail problem on this site does not swallow its own alert.', 'gemsecurity' ), ], ], ]; /** * Filter the in-product documentation. Add-ons that register their own * modules through `gemsecurity/modules` should add a matching entry * here so their page gets a help drawer too. * * @param array $docs Keyed by module id. */ self::$cache = apply_filters( 'gemsecurity/docs', $docs ); return self::$cache; } /** * Setup guides for the individual fields that need something fetched from * somewhere else — an OAuth client, a CAPTCHA key pair, a Slack webhook. * * A page can say *what* a field is; only a guide can say where the value * comes from. These render inline, next to the fields they describe, as a * collapsed "How to get these credentials" panel. * * Keyed by guide id. An entry is: * - title string Panel heading. * - steps string[] Ordered instructions, written against the provider's * own UI wording. * - link array Optional { url, label } to the provider's console. * - notes string[] Optional caveats shown under the steps. * * @return array */ public static function guides() { $guides = [ /* ---- Social login providers ------------------------------- */ 'social.google' => [ 'title' => __( 'How to get a Google client ID and secret', 'gemsecurity' ), 'link' => [ 'url' => 'https://console.cloud.google.com/apis/credentials', 'label' => __( 'Google Cloud console → Credentials', 'gemsecurity' ) ], 'steps' => [ __( 'Open the Google Cloud console and select an existing project, or create one for this site.', 'gemsecurity' ), __( 'Go to APIs & Services → OAuth consent screen, choose External, and fill in the app name, support email and developer contact.', 'gemsecurity' ), __( 'Go to APIs & Services → Credentials → Create credentials → OAuth client ID, and choose Web application.', 'gemsecurity' ), __( 'Under Authorised redirect URIs, add the redirect URI shown at the top of this page — exactly, including https and any trailing path.', 'gemsecurity' ), __( 'Create the client, then copy the Client ID and Client secret into the fields here and save.', 'gemsecurity' ), __( 'Use Test credentials below to confirm the pair before saving — a wrong secret only shows up when a real person tries to sign in.', 'gemsecurity' ), ], 'notes' => [ __( 'While the consent screen is in Testing, only the test users you list can sign in. Publish it before going live.', 'gemsecurity' ), ], ], 'social.facebook' => [ 'title' => __( 'How to get a Facebook app ID and secret', 'gemsecurity' ), 'link' => [ 'url' => 'https://developers.facebook.com/apps/', 'label' => __( 'Meta for Developers → My Apps', 'gemsecurity' ) ], 'steps' => [ __( 'Open Meta for Developers → My Apps → Create App, and pick the Consumer (or Other → Consumer) type.', 'gemsecurity' ), __( 'Add the Facebook Login product to the app and choose the Web platform, entering your site URL.', 'gemsecurity' ), __( 'In Facebook Login → Settings, paste the redirect URI from the top of this page into Valid OAuth Redirect URIs.', 'gemsecurity' ), __( 'Open App settings → Basic and copy the App ID into Client ID, and the App secret into Client secret.', 'gemsecurity' ), __( 'Use Test credentials below to confirm the pair before saving — a wrong secret only shows up when a real person tries to sign in.', 'gemsecurity' ), ], 'notes' => [ __( 'Facebook only returns an email address if the user granted the email permission — accounts without one cannot be linked by email.', 'gemsecurity' ), __( 'The app must be switched from Development to Live before anyone outside your app roles can sign in.', 'gemsecurity' ), ], ], 'social.github' => [ 'title' => __( 'How to get a GitHub client ID and secret', 'gemsecurity' ), 'link' => [ 'url' => 'https://github.com/settings/developers', 'label' => __( 'GitHub → Developer settings → OAuth Apps', 'gemsecurity' ) ], 'steps' => [ __( 'Open GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.', 'gemsecurity' ), __( 'Set Homepage URL to your site, and Authorization callback URL to the redirect URI shown at the top of this page.', 'gemsecurity' ), __( 'Register the application, then copy the Client ID.', 'gemsecurity' ), __( 'Choose Generate a new client secret and copy it immediately — GitHub shows it only once.', 'gemsecurity' ), __( 'Use Test credentials below to confirm the pair before saving — a wrong secret only shows up when a real person tries to sign in.', 'gemsecurity' ), ], 'notes' => [ __( 'A GitHub account can hide its email address; those users are created with the account they signed in as rather than linked by email.', 'gemsecurity' ), ], ], 'social.twitter' => [ 'title' => __( 'How to get X (Twitter) OAuth 2.0 credentials', 'gemsecurity' ), 'link' => [ 'url' => 'https://developer.x.com/en/portal/dashboard', 'label' => __( 'X Developer Portal', 'gemsecurity' ) ], 'steps' => [ __( 'Open the X Developer Portal and create a project and an app inside it.', 'gemsecurity' ), __( 'In the app’s User authentication settings, enable OAuth 2.0 and set App type to Web App.', 'gemsecurity' ), __( 'Set the Callback URI to the redirect URI shown at the top of this page, and fill in a website URL.', 'gemsecurity' ), __( 'Save, then copy the OAuth 2.0 Client ID and Client Secret into the fields here.', 'gemsecurity' ), __( 'Use Test credentials below to confirm the pair before saving — a wrong secret only shows up when a real person tries to sign in.', 'gemsecurity' ), ], 'notes' => [ __( 'Use the OAuth 2.0 client credentials, not the older API key and API secret — they are different values on the same screen.', 'gemsecurity' ), __( 'X rarely returns an email address, so new accounts get a placeholder you can change later.', 'gemsecurity' ), ], ], /* ---- CAPTCHA providers ------------------------------------ */ 'captcha.recaptcha_v2' => [ 'title' => __( 'How to get reCAPTCHA v2 keys', 'gemsecurity' ), 'link' => [ 'url' => 'https://www.google.com/recaptcha/admin/create', 'label' => __( 'Google reCAPTCHA admin console', 'gemsecurity' ) ], 'steps' => [ __( 'Open the reCAPTCHA admin console and register a new site.', 'gemsecurity' ), __( 'Choose reCAPTCHA v2 → "I\'m not a robot" Checkbox.', 'gemsecurity' ), __( 'Add your domain — without https:// and without a trailing slash. Add localhost too if you test locally.', 'gemsecurity' ), __( 'Submit, then copy the Site key and Secret key into the fields here.', 'gemsecurity' ), __( 'Then use Test CAPTCHA keys below: it renders the real widget and verifies the token, which is the only way to prove both keys before they reach the login form.', 'gemsecurity' ), ], 'notes' => [ __( 'The two keys are not interchangeable: the site key is public and rendered in the page, the secret key is never sent to the browser.', 'gemsecurity' ), ], ], 'captcha.recaptcha_v3' => [ 'title' => __( 'How to get reCAPTCHA v3 keys', 'gemsecurity' ), 'link' => [ 'url' => 'https://www.google.com/recaptcha/admin/create', 'label' => __( 'Google reCAPTCHA admin console', 'gemsecurity' ) ], 'steps' => [ __( 'Open the reCAPTCHA admin console and register a new site.', 'gemsecurity' ), __( 'Choose reCAPTCHA v3 — v2 keys will not work here, and vice versa.', 'gemsecurity' ), __( 'Add your domain, submit, and copy the Site key and Secret key into the fields here.', 'gemsecurity' ), __( 'Leave the score threshold at the default, then watch the admin console’s score distribution before changing it.', 'gemsecurity' ), __( 'Then use Test CAPTCHA keys below: it renders the real widget and verifies the token, which is the only way to prove both keys before they reach the login form.', 'gemsecurity' ), ], 'notes' => [ __( 'v3 never shows a challenge: it scores each request from 0.0 (bot) to 1.0 (human). A threshold that is too high rejects real people silently.', 'gemsecurity' ), ], ], 'captcha.hcaptcha' => [ 'title' => __( 'How to get hCaptcha keys', 'gemsecurity' ), 'link' => [ 'url' => 'https://dashboard.hcaptcha.com/sites', 'label' => __( 'hCaptcha dashboard → Sites', 'gemsecurity' ) ], 'steps' => [ __( 'Create an hCaptcha account and open the dashboard.', 'gemsecurity' ), __( 'Add a new site and enter your domain as the hostname.', 'gemsecurity' ), __( 'Copy the Sitekey into Site key here.', 'gemsecurity' ), __( 'Open Settings and copy your account Secret key into Secret key here — it is account-wide, not per site.', 'gemsecurity' ), __( 'Then use Test CAPTCHA keys below: it renders the real widget and verifies the token, which is the only way to prove both keys before they reach the login form.', 'gemsecurity' ), ], ], 'captcha.turnstile' => [ 'title' => __( 'How to get Cloudflare Turnstile keys', 'gemsecurity' ), // Href the user clicks to open Cloudflare's own dashboard — // nothing is fetched from this host at render time. 'link' => [ 'url' => 'https://dash.cloudflare.com/?to=/:account/turnstile', 'label' => __( 'Cloudflare dashboard → Turnstile', 'gemsecurity' ) ], // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent 'steps' => [ __( 'Open the Cloudflare dashboard → Turnstile → Add site.', 'gemsecurity' ), __( 'Enter your domain and choose the Managed widget unless you have a reason not to.', 'gemsecurity' ), __( 'Copy the Site Key and Secret Key into the fields here.', 'gemsecurity' ), __( 'Then use Test CAPTCHA keys below: it renders the real widget and verifies the token, which is the only way to prove both keys before they reach the login form.', 'gemsecurity' ), ], 'notes' => [ __( 'Your domain does not have to use Cloudflare’s DNS or proxy for Turnstile to work.', 'gemsecurity' ), ], ], /* ---- Alert channels --------------------------------------- */ 'alerts.slack' => [ 'title' => __( 'How to get a Slack incoming webhook URL', 'gemsecurity' ), 'link' => [ 'url' => 'https://api.slack.com/apps', 'label' => __( 'Slack API → Your Apps', 'gemsecurity' ) ], 'steps' => [ __( 'Open Slack API → Your Apps → Create New App → From scratch, and pick the workspace to post into.', 'gemsecurity' ), __( 'Open Incoming Webhooks and turn the feature on.', 'gemsecurity' ), __( 'Choose Add New Webhook to Workspace and select the channel that should receive alerts.', 'gemsecurity' ), __( 'Copy the generated URL (it starts with https://hooks.slack.com/services/) into the field here.', 'gemsecurity' ), ], 'notes' => [ __( 'The webhook URL is a credential — anyone holding it can post to that channel. Do not commit it anywhere public.', 'gemsecurity' ), ], ], 'alerts.telegram' => [ 'title' => __( 'How to get a Telegram bot token and chat ID', 'gemsecurity' ), 'link' => [ 'url' => 'https://t.me/BotFather', 'label' => __( 'Telegram → BotFather', 'gemsecurity' ) ], 'steps' => [ __( 'Message @BotFather on Telegram and send /newbot, then follow the prompts to name the bot.', 'gemsecurity' ), __( 'Copy the token BotFather returns into Bot token here.', 'gemsecurity' ), __( 'Send your new bot a message (or add it to the group that should receive alerts, and post there).', 'gemsecurity' ), __( 'Open https://api.telegram.org/bot/getUpdates in a browser and copy the chat id from the response into Chat ID.', 'gemsecurity' ), ], 'notes' => [ __( 'A bot cannot start a conversation. If nobody has messaged it, getUpdates comes back empty and alerts will not deliver.', 'gemsecurity' ), __( 'Group chat IDs are negative numbers — include the minus sign.', 'gemsecurity' ), ], ], 'alerts.webhook' => [ 'title' => __( 'How the generic webhook works', 'gemsecurity' ), 'steps' => [ __( 'Point this at any URL that accepts an HTTP POST — an automation platform, a chat bridge, or your own endpoint.', 'gemsecurity' ), __( 'GemSecurity posts a JSON body describing the event: type, severity, message, IP, user and timestamp.', 'gemsecurity' ), __( 'Set the minimum severity above so routine events do not flood the receiver.', 'gemsecurity' ), ], 'notes' => [ __( 'Delivery is fire-and-forget: a receiver that is down loses those alerts rather than queueing them.', 'gemsecurity' ), ], ], 'siem.endpoint' => [ 'title' => __( 'How to forward logs to a SIEM', 'gemsecurity' ), 'steps' => [ __( 'Create an HTTP collector (HEC or equivalent) in your SIEM and copy its ingest URL here.', 'gemsecurity' ), __( 'Alternatively enable syslog output to ship to a local collector instead of over HTTP.', 'gemsecurity' ), __( 'Set the minimum severity to control the volume — everything at or above it is forwarded.', 'gemsecurity' ), __( 'Trigger a test event (a failed login will do) and confirm it arrives before relying on it.', 'gemsecurity' ), ], 'notes' => [ __( 'Forwarding is in addition to the local log, not instead of it; retention here still applies.', 'gemsecurity' ), ], ], ]; /** * Filter the inline field-level setup guides. * * @param array $guides Keyed by guide id. */ return apply_filters( 'gemsecurity/docs_guides', $guides ); } /** * A single module's guide, normalised so consumers can rely on every key. * * @param string $id Module id. * @return array|null Null when the module has no documentation. */ public static function get( $id ) { $all = self::all(); if ( empty( $all[ $id ] ) ) { return null; } return array_merge( [ 'summary' => '', 'does' => [], 'setup' => [], 'notes' => [] ], $all[ $id ] ); } /** * Documentation for the React app, keyed by page slug (what the admin * knows a page by) and carrying the module title for the drawer header. * Only visible modules are included — a module you cannot open needs no * help page. * * @return array */ public static function for_js() { $out = []; $labels = Registry::labels(); foreach ( Registry::all() as $id => $module ) { if ( ! Registry::is_visible( $id ) ) { continue; } $doc = self::get( $id ); if ( ! $doc ) { continue; } $out[ $module['page_slug'] ] = [ 'id' => $id, 'title' => isset( $labels[ $id ]['title'] ) ? $labels[ $id ]['title'] : $id, 'is_pro' => $module['is_pro'], ] + $doc; } return $out; } }