=== WebPro Activity Logs ===
Contributors: webprolancer
Tags: activity log, audit trail, security, user tracking, monitoring
Requires at least: 6.2
Tested up to: 7.1
Requires PHP: 8.1
Stable tag: 4.1.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Audit trail for WordPress: records every meaningful action, with a security dashboard, live feed, sessions, scheduled reports and email alerts.

== Description ==

WebPro Activity Logs records who did what on your site, and when. Every login, post edit, plugin change, theme change, settings change, user change and file modification is written to a searchable audit trail you can filter, export and act on.

= Core logging =

* 50+ event types across users, posts, media, comments, menus, widgets, plugins, themes, settings and updates
* Full context per event — IP, browser, OS, device, session, URL, referrer, HTTP method
* Before/after diffs for option changes, with credentials automatically redacted
* WooCommerce and Elementor support
* Server-side pagination, prepared statements and cached statistics

= Security =

* Security dashboard — failed logins, permission denials, role changes, file modifications
* **Hardening switches** — four things a site owner usually pays someone to do, as checkboxes: disable XML-RPC, block user enumeration, hide the WordPress version, and send security response headers. All four ship **off**; nothing changes on your site until you turn one on.
* **Verified from outside, not asserted.** After you save, the plugin requests its own front page over HTTP and checks what actually came back — including whether your server or CDN was already doing the job without it, in which case it says so instead of claiming credit. If that request cannot complete, the screen says "could not verify", never "protected".
* Brute-force detection — correlates many failed logins into one incident instead of hundreds of separate warnings
* File Integrity Monitor — SHA-256 baseline detects changes made over FTP, SSH or by malware, not just the WordPress editor
* REST API monitoring and application-password authentication events
* Performance monitoring — slow requests and PHP fatal errors

= Reports =

* Scheduled security reports — weekly or monthly, built from the period's own log data
* 20 kinds of issue it looks for — confirmed break-ins, brute-force campaigns, privilege escalation, unexplained file changes, plugins added or removed, a security plugin disappearing, pending security updates, PHP fatals, degraded WP-Cron and more — plus an explicit all-clear when there is nothing to report
* A 0–100 security posture score with a plain-language verdict, and the change since the previous period
* Emailed as a real multipart message — HTML with a plain-text alternative — or read on the Reports screen
* Every report is stored as structured data and re-rendered on demand, so history stays readable after a template change
* An honest empty report: "nothing was looked at" and "nothing was wrong" never render the same way

= Dashboard =

* Live activity feed with configurable auto-refresh
* User sessions with login, logout, duration and location
* Advanced search across 15+ fields, plus one-click quick filters
* Charts you can use — hover any bar for its exact value, click a bar on the hourly chart to filter the log to that hour, and remove the filter from the chip it leaves behind
* CSV and JSON export
* Post restoration from log entries
* Dashboard widget, dark mode, responsive layout

= Access =

By default every administrator can open the dashboard. You may optionally restrict it to a single email address under Settings → Dashboard Access. Activity for **all** users is recorded either way.

= Links =

The plugin's admin menu contains one link to an external site: **Site Audit**, which opens https://webprolancer.com/site-audit/ in a new tab. It is a link and nothing more — clicking it sends no data from your site, and the plugin makes no request to that address on its own.

== Installation ==

1. Upload the `webpro-activity-logs` folder to `/wp-content/plugins/`, or install the ZIP through Plugins → Add New → Upload Plugin.
2. Activate the plugin through the Plugins screen.
3. Open **Activity Logs** in the admin menu. Logging starts immediately.
4. Visit **Activity Logs → Settings** to set log retention, email alerts and (optionally) geolocation.

== Frequently Asked Questions ==

= Does this slow my site down? =

Logging is a single indexed INSERT per event and nothing is written on cached front-end page views. Geolocation, which is the only part that makes an outbound request, is off by default and runs asynchronously through WP-Cron when enabled.

= How large does the log table get? =

That depends on your traffic. Set a retention period under Settings — 30, 90, 180 or 365 days — and a daily cron deletes anything older. "Never" keeps everything.

= Can other administrators see the logs? =

Yes, by default. `manage_options` is required. If you want only one account to have access, enter its email under Settings → Dashboard Access.

= Are passwords or API keys ever stored in the log? =

No. Passwords and authentication tokens are never logged, the auth cookie is only ever stored as a hash, and option values whose names look like credentials (`*_key`, `*password*`, `*token*`, `*secret*`, `*license*`, and similar) are redacted before the row is written. You can extend that list with the `wpal_is_sensitive_option` filter.

== External services ==

This plugin can connect to one third-party service. It is optional and disabled until you turn it on.

**IP geolocation — ip-api.com** *(optional, off by default)*
Resolves a visitor IP address to a country, region, city, timezone and ISP so log entries can show a location. Sends only the IP address being looked up, and only for public addresses — private and reserved ranges are skipped. Enabled under Settings → Geolocation. Results are cached for 24 hours. The free tier of this service is HTTP-only; use the `wpal_geo_api_url` filter to point the lookup at a different provider.
Terms: https://ip-api.com/docs/legal — Privacy: https://ip-api.com/docs/legal

That is the only third party this plugin ever contacts. It does not phone home, and does not contact WordPress.org, Google or any other outside endpoint.

The plugin does make one other kind of HTTP request, and it never leaves your own site. When you save a hardening switch, or press "Re-check now", it requests **your own** front page, your own `/wp-json/wp/v2/users` and your own `xmlrpc.php` — six requests — and reads what came back, so the switch can report what your site actually does rather than assume the code ran. The addresses come from `home_url()` and `rest_url()`, so they are always your site. Nothing is sent anywhere else, and nothing is checked on a normal page load: the result you see is cached from the last check.

== Screenshots ==

1. Activity log with quick filters and advanced search
2. Security dashboard
3. Live activity feed
4. User sessions
5. Security reports
6. Settings, including the security hardening switches and what the outside check found

== Changelog ==

= 4.1.4 =

* **New: copy me on WordPress's recovery email.** When a fatal error happens, WordPress emails a *recovery link* to the address in Settings → General. A new checkbox under Settings → Email Alerts adds your alert address to that email as a Cc, so the link reaches the inbox you actually watch.
* **Off by default, and deliberately.** That link signs whoever opens it into recovery mode with no password. Only one address is ever added — the alert address already configured on that screen — the link is never written to the log or stored anywhere, and if the two addresses are the same nothing is added at all.
* The setting explains what WordPress will and will not do, because none of it is under this plugin's control: core does not send that email while `WP_DEBUG` is `true`; it sends at most one a day per error; and it sends only for fatals on wp-admin, the login screen, WP-Cron or the REST API — never for one a visitor hits on the front of the site. The plugin's own **PHP Fatal Error** alert has none of those limits, which is the point of having both.

= 4.1.3 =

* **New alert: PHP Fatal Error.** The plugin already recorded fatal errors; it just had no way to tell you about one. "PHP Fatal Error" now appears in Settings → Email Alerts alongside the other nine. Off by default.
* At most one fatal email per hour, however many times the site crashes, and the hour is claimed before the message is sent — so a site broken badly enough that sending mail also fails does not retry on every request. The whole alert path is wrapped: an alert that throws while reporting a crash would turn one broken page into a broken page with no record of why.
* Note that this is **not** the same as WordPress's own "Your site is experiencing a technical issue" recovery email. That one is sent by WordPress core, goes to the address in Settings → General, and core does not send it at all while `WP_DEBUG` is on.

= 4.1.2 =

Two faults reported from a live site, both fixed.

* **Saving settings showed a blank white screen** — while saving the settings correctly. The save ran inside the page render, by which point WordPress had already sent the page's `<head>`, so the redirect that follows it could not set its header ("Cannot modify header information — headers already sent") and the `exit` after it cut the half-written page off mid-tag. The save now runs on `admin_init`, before any output, so the redirect works and you land on the settings screen with "Settings saved".
* **`_load_textdomain_just_in_time` notice** for this plugin's text domain, on every request including the front end. One caller wanted the four hardening option *names* and took them from a map that also carries their translated labels, which called `__()` during `plugins_loaded` — before `init`, which is what WordPress 6.7 objects to. The names now come from an untranslated list; the labels are still translated, but only where the settings screen renders them, long after `init`.
* The outside hardening check no longer runs when you save a setting that has nothing to do with it — only when a hardening switch actually moves, or when you press "Re-check now". It is six HTTP requests; changing the retention dropdown should not pay for them.
* That check is now bounded to 15 seconds in total. Six requests at the old 8-second timeout could reach 48 seconds inside one POST, and shared hosts commonly stop a request at 30. A host whose loopback hangs rather than refusing took the settings save down with it. When the budget runs out the remaining probes report **"could not verify"** — never a pass, which is the rule this feature is built on.

= 4.1.1 =

**Fixes a bug that made every "today" number read 0.** It has been present since 3.4.2 and affects every install.

* "Today's Events", "Failed Logins" and "Security Events" on the dashboard widget, the matching cards on the Activity Log screen, and the **Today** quick filter all counted a window that began and ended at the same instant — midnight today to midnight today — so they matched nothing and always showed 0, no matter how much had happened. Every other figure on those screens was correct, which is why it looked like a display quirk rather than a broken query.
* The cause: a helper that returns a site-local time "N seconds ago" clamps a negative argument to zero. Three callers passed a negative meaning "this time tomorrow" and silently got "now" instead. Day boundaries are now calendar arithmetic on the site's local date, which cannot express that mistake, and which is also correct across daylight-saving changes where "now plus 24 hours" is not.
* No data was lost or mis-recorded. The rows were always there and the log itself always showed them; only these counters were reading the wrong range. After updating, the numbers are correct immediately — nothing to rebuild.

= 4.1.0 =

Nothing in this release changes how your site behaves on its own. The new hardening switches all ship **off**.

**New — security hardening switches**

Four toggles under Settings → Security Hardening. Each one is off until you turn it on:

* **Disable XML-RPC** — a POST to `xmlrpc.php` is answered 403 and no method list is returned. `add_filter( 'xmlrpc_enabled', '__return_false' )` alone does not do this: `system.listMethods` still answers 200 with the full list. The RSD link and the `X-Pingback` header stop being advertised too.
* **Block user enumeration** — `/wp-json/wp/v2/users` and `?author=N` are both closed. Closing one and leaving the other is a switch that claims to have done something it did not, because `?author=1` redirects to `/author/<login>/` and hands the username over in the URL.
* **Hide the WordPress version** — the `generator` meta tag, the `the_generator` filter output, and the `?ver=` query string on core stylesheets and scripts.
* **Send security response headers** — `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`. Added only if absent, including headers your server or CDN already sends, which never appear in PHP's own header list.

Each switch is verified by requesting your front page over HTTP and reading the response, rather than by assuming the code ran. Where the site already does the job without this plugin, the screen says so. Where the request cannot complete — a firewall that refuses loopback requests, for example — it says "could not verify". A refused request is never reported as a pass.

**New — charts that do something**

* Every chart redrawn: hover any bar or point for its exact value in a readable tooltip, on a hit map rather than a guess at the nearest pixel.
* Click a bar on the "By hour" chart to filter the log to that hour of the day. The filter appears as a chip you can click again to remove, and it survives a page reload.
* The log screen now reads its filters from the URL, so a filtered view can be bookmarked or sent to someone.
* Charts are drawn at your screen's real pixel density, so they are sharp on a Retina or high-DPI display and stay exactly the size they were drawn at. Previously a chart grew a little every time the pointer crossed it.

**Changed — database queries**

* Every table name in every query is now bound with the `%i` identifier placeholder instead of being written into the SQL string — 77 of them, across ten files. None was ever injectable: each is `$wpdb->prefix` plus a hardcoded suffix. The point is that a static analyser cannot tell that, and the WordPress.org guidelines are written for the analyser, not for the author's confidence.
* The four `CREATE TABLE` statements handed to `dbDelta()` still write the name directly, because `dbDelta()` parses the SQL itself and never goes through `prepare()`.
* One query in the breach test built its upper time bound as a fragment of SQL glued onto the string. It now has one shape and binds both bounds. The rows it returns are unchanged.
* Integer values read from a submitted form are cast with `intval()` rather than an `(int)` cast wrapped around a `??` expression. Identical behaviour for every input, including negative, fractional and non-numeric ones.

= 4.0.0 =

**Read this before upgrading. This release removes a feature and permanently deletes data.**

* **The malware scanner has been removed.** Scanning, scheduled scans, trust lists, AI triage and Safe Browsing reputation checks are all gone. The **Malware Scanner** menu item is replaced by **Site Audit**, a link to an external audit service.
* **Upgrading DROPS four database tables** — `wp_malware_scans`, `wp_malware_findings`, `wp_malware_pages` and `wp_malware_outlinks` — and every scan and finding recorded in them.
* **Upgrading DELETES your stored API keys**, including the Google Gemini key used for AI triage and the Google Safe Browsing key. Anyone who wants those back must enter them again somewhere else; the plugin no longer has a field for them.
* **None of this can be undone. If your scan history or those keys matter to you, take a database backup before you update.**

Everything else in 4.0.0 is an addition:

**New — scheduled security reports**

* Weekly or monthly reports built from your own log data, stored as structured payloads and re-rendered on demand.
* 20 kinds of issue: confirmed break-in, distributed brute force, open brute-force incident, administrator created, role escalated, file edited from the editor, unexplained file changes, incomplete integrity runs, plugin added, plugin removed, security plugin removed, security updates pending, login attempts against real accounts, spraying of usernames that do not exist, PHP fatals, security settings changed, degraded WP-Cron, REST authentication bursts, permission-denial bursts and content destruction — plus an explicit all-clear.
* A 0–100 security posture score that deducts per issue and caps each severity, so a busy site cannot score worse than a breached one.
* Reports are emailed as genuine multipart messages — HTML with a plain-text alternative attached through `phpmailer_init` — or read on the Reports screen.

**New — security**

* Brute-force detection correlates many failed logins into a single incident instead of hundreds of separate warnings, with four guards against false "breach" alerts.
* One implementation of the client IP, with an explicit trusted-proxy setting; Cloudflare is auto-detected on upgrade.
* File Integrity Monitor records HOW a file changed, not just that it did.
* Sessions are keyed on the WordPress session token, so a logout row names the session it ended.
* The plugin now logs changes to its own alerting settings — redirecting the alert email is one of the first things an intruder does.

**Fixed**

* A duplicate copy of the plugin on disk no longer runs twice. The second copy stops before defining anything and shows an admin notice naming both paths.
* Report emails could no longer staple one report's plain-text body onto every later email in the request.
* Timezone-naive rolling windows, non-indexed `DATE(created_at)` filters and a redundant index that made MySQL ignore the wider one.

**Database**

* Schema version 3.4. Upgrades run automatically on the first page load after updating.

= 3.4.2 =
Security, compatibility and correctness release. Upgrading is strongly recommended.

**Fixed — the plugin now works on MySQL**
* The `activity_logs` and `malware_findings` tables declared a literal `DEFAULT ''` on TEXT/LONGTEXT columns. MySQL rejects this on every version (error 1101), so on MySQL hosts the tables were never created and nothing was ever logged. Only MariaDB accepted it.
* Shortened `uniq_scan_domain` so the index fits InnoDB's 767-byte limit on MySQL 5.6/5.7.

**Fixed — access control**
* The dashboard is no longer gated on a hardcoded email address. Any administrator can now open it. An optional single-email restriction is available under Settings → Dashboard Access, and a `WPAL_OWNER_EMAIL` constant in `wp-config.php` still takes precedence for existing installs.
* Admin menus now require the `manage_options` capability rather than the `administrator` role name.

**Removed**
* The deactivation lock, which prevented administrators from deactivating or deleting the plugin.
* The deactivation notification email.
* The must-use-plugin self-installation path.

**Fixed — timezone handling**
* Every rolling-window query compared site-local timestamps against the database server's `NOW()`. On negative UTC offsets the brute-force detector and failed-login alerts never fired at all; on positive offsets they fired constantly. All windows are now computed on one clock.
* The "Last Hour" statistic passed a timestamp where `current_time()` expects a `$gmt` flag, so it always reported zero.
* Chart and dashboard date boundaries were shifted by twice the site's UTC offset.
* Timestamps in the event detail modal were shifted by the UTC offset.

**Fixed — events that were never being logged**
* Comment moderation. The hook name was built from the wrong array element, registering hooks such as `comment_comment_approved` which do not exist.
* User deletion. `before_delete_user` is not a WordPress hook; core fires `delete_user`.
* Theme and plugin file edits. Since WordPress 4.9 the editor saves over AJAX, so the `admin_action_` registration never fired. The handler now also verifies the nonce properly instead of only checking that one was present, and the plugin file path is no longer built with a duplicated directory segment.
* Core updates were logged twice, once with a stale version number.

**Fixed — security**
* Option before/after diffs no longer copy other plugins' API keys, SMTP passwords or licence keys into the log table and its exports. Values whose option name looks like a credential are redacted; extend with the `wpal_is_sensitive_option` filter.
* `Sitemap:` directives in `robots.txt` were fetched without validation, letting a compromised site point the scanner at internal or cloud-metadata addresses. Only same-host, non-private URLs are followed now.
* `sslverify => false` removed everywhere.
* Google API keys are sent in the `x-goog-api-key` header instead of the URL query string, where they were being written to proxy and debug logs.
* Twelve places where server data was inserted into the admin UI as HTML are now escaped, including the toast notifier and the diagnostics panel.
* `recheck_finding()` now contains its file read inside the WordPress installation root.

**Fixed — interface**
* The Settings page "Clear Activity Log" panel never worked: its code sat outside the jQuery wrapper and threw a `ReferenceError` under noConflict.
* The Live Feed never displayed anything, for the same class of scope error.
* Toast notifications were discarded on five of six pages because the container only existed on one.

**Fixed — malware scanner accuracy**
* The keylogger signature's alternation was unparenthesised, reducing it to a bare `fetch(` match. Every site using `fetch()` anywhere in its HTML was reported as critically infected with a score of zero. Two further over-broad or duplicated signatures were removed.
* An unreadable core file (permissions, `open_basedir`) was reported as a modified core file. It is now skipped.
* `TRUNCATE` now falls back to `DELETE` on hosts that revoke the `DROP` privilege, where clearing previously failed silently while reporting success.

**Performance**
* Statistics and quick filters used `DATE(created_at)`, which made every one of them a full table scan. All are now range comparisons that use the index.
* Added indexes on `actor_type` and `(action, created_at)`.

**Housekeeping**
* Removed the empty `Report` class, three registered-but-uncalled AJAX endpoints, three unused public methods, two dead UI elements and one dead jQuery guard.
* `uninstall.php` now removes the Safe Browsing key explicitly and clears scheduled events per blog on multisite.
* Added `load_plugin_textdomain()` and a `/languages` directory.

= 2.0.3 =
* FIX: `Sessions::touch_session()` creates a session row when none exists.
* FIX: Logger insert errors are written to the PHP error log with the failed action name.
* FIX: `transition_post_status` and `post_updated` capture the acting user at hook time.

= 2.0.0 =
* Security dashboard, live feed, sessions, user timeline
* File editor monitoring with SHA-256 hashes
* Settings diffs, geolocation, email alerts
* REST API and performance monitoring
* Optimised schema with new columns and indexes

= 1.0.0 =
* Initial release

== Upgrade Notice ==

= 4.1.4 =
Adds an optional Cc of your alert address onto WordPress's own recovery-link email. Off by default; that link is a password-free way into the site, so read the note beside the checkbox. Includes everything in 4.1.2 and 4.1.3.

= 4.1.3 =
Adds an optional "PHP Fatal Error" email alert, off by default and limited to one message per hour. Includes everything in 4.1.2.

= 4.1.2 =
Fixes a blank screen after saving settings (the settings were saved; the page was not) and a _load_textdomain_just_in_time notice on every request. Also stops the outside hardening check from running when no hardening switch changed, and bounds it to 15 seconds.

= 4.1.1 =
Fixes "Today's Events", "Failed Logins", "Security Events" and the Today quick filter, which have shown 0 on every install since 3.4.2 regardless of activity. No data was affected; the counts are correct as soon as you update.

= 4.1.0 =
Adds optional security hardening switches, working charts, and internal query hardening. The new switches are all OFF after upgrading — your site behaves exactly as it did until you turn one on. No database changes.

= 3.4.2 =
Fixes a schema defect that prevented the log table from being created on MySQL, restores dashboard access for all administrators, removes the deactivation lock, and corrects timezone handling that disabled brute-force detection on half of all installs. Upgrade immediately.
