Repository navigation
Releases: Yoko-Co/yoko-link-checker
Release list
v1.2.0
Release Notes — Yoko Link Checker 1.2.0
Monthly maintenance: the admin screen moves, the numbers finally agree, and four security findings are closed.
Why this release
The plugin reported a different number of broken links depending on where you looked — the dashboard, the report screen and the exported spreadsheet each gave a different answer for the same site. There was no way to tell which one was right, which made the tool unusable for telling a client how much work a site needed.
That turned out not to be a bug in a query. Two screens were counting two different things and both were right: the dashboard counted unique URLs, the report counted link occurrences. One broken URL used in twelve posts is legitimately 1 and 12. They also disagreed on filters, which is what made them drift apart for good.
What's new
The counts agree, and they say what they count
Every figure now comes from the same queries, and every screen states both units — "12 broken URLs across 34 links". A URL is the problem count: you fix it once. An occurrence is the work count: each is a place someone has to open and edit. Both matter when scoping work, so neither is hidden.
Moved to Tools → Yoko Link Checker
One menu entry instead of a top-level section, with Dashboard, Reports and Settings as tabs on a single screen. Named in full throughout so it is clear whose tool it is.
WP-CLI
wp yoko-lc counts # every status, in both units
wp yoko-lc verify # assert the counts are consistent; non-zero exit if not
wp yoko-lc prune # delete link rows whose source post is gone
wp yoko-lc export # same export as the admin screen, same filtersAn uninstall setting
Deleting the plugin no longer has to delete your scan data. uninstall.php had always read this option and defaulted to deleting everything, but nothing ever wrote it — there was no way to answer the question until now.
Fixed
- "Last scan" was wrong on every site not set to UTC. Datetimes are stored in site-local time but were read as UTC, adding the site's offset to every age — a scan that had just finished read "4 hours ago" on US Eastern. The duration beside it was correct, because subtracting two equally wrong timestamps cancels the error, which is exactly what made the wrong number look credible.
- A just-started scan could be failed as stale, from the same misreading applied to the 30-minute staleness check.
- Searching the report advertised empty pages — the count query ignored the search term that the row query applied.
- The
errorstatus was invisible — counted in the dashboard total but unreachable in the UI;?status=errorsilently fell back to Broken. - Dashboard cards did not add up. Blocked, timeout and error were in the total but had no card. They now share a Needs Review card, kept separate from Broken because most blocked responses are bot protection rather than a dead link.
- Ignoring a link desynchronised the screens permanently — the dashboard ignored the ignored flag entirely, so its numbers never moved when the report's did.
- CSV export ignored your filters, was headed "Broken URL" while containing every status, and left Source URL and Source Title blank for pages and every custom post type.
- Scan progress could not reach 100% — the denominator counted URLs the checker never fetches, and the completed-state branch was unreachable.
- Link rows are pruned when a post is permanently deleted and when links are removed from content, so occurrence counts stop drifting upward forever.
Security
- Redirects are validated at every hop. The checker validated only the URL it was given, then handed the request to the transport with up to three redirects followed unchecked — so an innocuous external link responding
302 Location: http://169.254.169.254/…was fetched. This is the class of issue WordPress fixed in core's URL validation in 7.0.3; the plugin did not inherit that fix because it bypassedwp_http_validate_url(). Four related gaps closed at the same time: DNS resolution failure was treated as safe rather than blocked,http://[::1]/bypassed the check entirely, there was no scheme allow-list (leavingfile://andgopher://open), and the DNS cache lived for the whole process. - Every AJAX endpoint has its own nonce. All ten shared one, so a token leaked from any plugin page authorised
clear_data— which truncates all three tables — exactly as readily as a status poll.clear_datanow requiresmanage_options. - Status polling can no longer spawn cron on demand. That endpoint needs only view capability but called
spawn_cron()on every poll; it is now rate-limited. - Settings saving uses Post/Redirect/Get. The save ran during render, so a refresh silently re-submitted the form, including the cron reschedule.
- Uninstall always clears scheduled events now. It previously returned early when data was being kept, leaving
yoko_lc_auto_scanfiring forever with no handler once the plugin was gone.
WordPress 7.0.2 and 7.0.4 were reviewed and need no plugin-side action — 7.0.2 is REST and WP_Query internals, 7.0.4 is the Imagick/Ghostscript image path, neither of which this plugin touches.
Upgrade notes
Three things will look like bugs if you are not expecting them:
- Anyone with a plugin page open across the upgrade gets one "Security check failed" on their next action — their page holds nonces in the old format. Reloading fixes it. Unavoidable when nonce actions change.
- Old admin bookmarks stop working. The screen moved from
admin.php?page=yoko-link-checker*totools.php?page=yoko-link-checker&tab=…. The menu slug itself is unchanged, so stored data and capabilities are unaffected. ylcAdmin.nonceis nowylcAdmin.nonces, keyed by action. Only affects bespoke integrations calling the AJAX endpoints directly.
Breaking change — yoko_lc_internal_http_args is removed. Internal URL checks used to run through a separate HTTP path with their own arguments and SSL verification disabled; they now go through the same client as every other request, so there is one SSRF gate rather than two. Use yoko_lc_http_request_args instead, plus yoko_lc_allow_private_urls where a private address must be reachable.
Take a database snapshot before the first scan. Running a scan deletes stale and orphaned link rows, and wp yoko-lc prune does so explicitly. Those rows are orphaned by definition and removing them is the intended fix for counts drifting upward — but the deletions are not undone by rolling back the code.
There is no schema change and no migration, so the code itself reverts cleanly.
Requires: WordPress 6.0+ (tested to 7.0) · PHP 8.0+
v1.1.1
v1.1.1
Release Date: March 4, 2026
Resolves 17 code review findings from Round 4 codebase review (Wave 4).
Critical Fix (P1)
- Polling race condition —
scheduleNextPollnow fires in AJAX complete callback, preventing permanent scan status stalls
Important Fixes (P2)
- check_timeout mismatch — Default corrected from 8 to 30, matching Activator/AdminController
- register_menu capability — Uses fixed custom strings instead of
current_user_can()at registration time - Export handler — Moved from
admin_inittoload-{$hook_suffix}for page-specific execution - Cron reschedule — Uses
time()instead oftime() + interval - strtotime() validation — Return values validated in DashboardPage/LinksListTable
- Media extension guard — Added for
attachment_url_to_postid() - N+1 query optimization — Batch URL hash lookups (
find_by_hashes()) and link existence checks (find_existing_batch())
Code Quality (P3)
- Added
ylcAdminundefined guard, enforced Yoda conditions - Added null check on
get_edit_post_link(), fixedsanitize_csv_value()return type - Removed duplicate
do_actioncalls in ResultsPage ignore/unignore - Moved inline settings script to
admin.jsbindEvents() - Cron schedules filter registered before activation with explicit 0 args
url_idnow passed in ignore/unignore hooks- Removed redundant
count_posts()per batch; uses scan record total
Removed
- Dead modal code (HTML, JS
closeModal/Escape handlers, CSS rules) - 8 dead repository methods (~166 LOC) from LinkRepository and ScanRepository
- Unused
PROBLEM_STATUSESconstant next_checkcolumn and index from schema
Database
- Schema version bumped to 1.2.0
v1.1.0
v1.1.0
Release Date: March 3, 2026
Addresses all findings from comprehensive multi-agent code review (5 P1 Critical, 16 P2 Important, 13 P3 Nice-to-Have).
Critical Fixes (P1)
- Ignore/unignore targeting — Now correctly updates
urls.is_ignored, notlinkstable - AJAX status poll — Removed batch processing; delegated to WP-Cron to prevent timeout issues
- Stale scan recovery — 30-minute timeout auto-fails stuck scans
- Schema version mismatch — Unified version tracking between
Plugin.phpandActivator.php - Ghost property — Fixed
$url->redirect_url→$url->final_url
Important Improvements (P2)
- Parallel HTTP requests — External URLs checked via
Requests::request_multiple()for 5-10x faster scans - Dashboard optimization — Single
GROUP BYquery replaces 16+ individualCOUNTqueries - SSRF protection — Blocks requests to private IP ranges (127.x, 10.x, 192.168.x, 169.254.x, fc00::/7)
- Concurrent batch locks — Transient-based protection prevents duplicate batch processing
- Race condition fix — TOCTOU handling in
find_or_create()URL logic - Cascading deletes — Associated links cleaned up on URL deletion
- Capability checks — Added
manage_optionsfallback for settings access - Uninstall cleanup — Fixed stale options list in
uninstall.php - Exception sanitization — Sensitive data removed from AJAX error responses
- Persistent cron filter —
cron_schedulesregistered inboot()method - JavaScript i18n — All hard-coded English strings localized via
wp_localize_script() - Timer cleanup — Polling intervals cleared on page unload
- Streaming CSV — Chunked queries for constant memory usage on large exports
- Settings cleanup — Removed dead grouped
yoko_lc_settingsoption - Post cache priming — N+1 queries eliminated in discovery phase
Code Quality (P3)
- Deleted unused
UrlValidatorclass (205 LOC) - Consolidated 30+ inline debug blocks into Logger calls
- Removed unused
ExtractorRegistry/Interfacemethods - Consolidated duplicate URL-skip logic (3 locations → 1)
- Injected
StatusClassifierintoBatchProcessor(proper DI) - Removed count method aliases from
UrlRepository - Removed speculative code for unbuilt features
- Added composite index
(status, is_ignored, id) - Primed post caches in
LinksListTable - Added AJAX response validation in JavaScript
- Added
set_service()for test dependency injection - Moved raw SQL from
DashboardPagetoLinkRepository - Wrapped TRUNCATE in transaction with error handling
- Full PSR-2 coding standards compliance
Files Changed
| Category | Files |
|---|---|
| Core | yoko-link-checker.php, src/Plugin.php, src/Activator.php |
| Admin | src/Admin/AjaxHandler.php, src/Admin/DashboardPage.php, src/Admin/ResultsPage.php, src/Admin/LinksListTable.php |
| Scanning | src/Scanner/BatchProcessor.php, src/Scanner/ScanOrchestrator.php, src/Scanner/ContentDiscovery.php |
| Checking | src/Checker/UrlChecker.php, src/Checker/CheckResult.php, src/Checker/StatusClassifier.php, src/Checker/HttpClient.php |
| Repository | src/Repository/UrlRepository.php, src/Repository/LinkRepository.php |
| Utilities | src/Util/UrlNormalizer.php, src/Util/Logger.php |
| Assets | assets/js/admin.js |
| Install | uninstall.php |
Upgrade Notes
This release includes database schema changes. The plugin will automatically run dbDelta() on update to add the new composite index. No manual action required.
v1.0.8
v1.0.8
Fixed
- Internal 404s now correctly flagged as broken — URLs that WordPress functions couldn't verify (custom routes, plugin pages, nested archives) were incorrectly marked as "warning" even when they returned 404. Now uses HTTP fallback to get actual status.
Added
check_internal_url_via_http()method for fallback HTTP verification of internal URLsyoko_lc_internal_http_argsfilter for customizing internal HTTP request arguments- Short timeout (3 seconds) for internal HTTP checks to prevent delays
Technical Notes
- WordPress function verification (
url_to_postid, taxonomy lookups, etc.) still runs first for performance - HTTP fallback only triggers for URLs that can't be resolved via WordPress (~5-10% of internal links)
- Uses HEAD requests to minimize server load
v1.0.7
1.0.7
Changed
- Reports page - Renamed from "Broken Links" to "Reports" for broader reporting
- Link Text - Renamed from "Anchor Text" for better clarity
- CSV Export - Now includes:
Source URL- The actual permalink where the broken link was foundBroken URL- The problematic destination URLSource Title- Post/page title containing the linkSource Type- Post type (post, page, etc.)Error Details- Specific error messageLink Text- The clickable text of the linkLast Checked- When the link was last verified
Removed
- Bulk actions removed to simplify the interface for MVP
v1.0.6
v1.0.5
1.0.5
Fixed
- CSV Export now outputs proper CSV data instead of HTML page content
- Bulk actions (Ignore, Un-ignore, Recheck) now work correctly without white screen errors
Added
- Status tooltips - hover over any status badge to see a description of what it means:
- Warning: Server returned a response that may indicate a problem (could be temporary or blocked)
- Blocked: Destination server blocked the request (common with social media sites)
- Timeout: Request timed out waiting for response
- Error: Connection error (DNS, SSL, or network issue)
- Redirect: URL redirects to a different location
- Error messages from the server are now shown in status tooltips
v1.0.4
1.0.4
Fixed
- Export to CSV now works correctly on the results page
- Clear All Data button now properly removes all scan data
Added
- CSV export includes URL, status, HTTP code, source post, and anchor text with proper UTF-8 encoding
- Info notice in scan section reminding users to keep the page open during scans
Changed
- Refreshed card styling with blue accent border and modern shadow
- Stat cards now have a subtle hover animation
- Updated border radius for a cleaner appearance
v1.0.3
v1.0.3 - Internal Link Validation
Properly validates internal URLs for broken links without causing server timeouts.
Fixed
- Internal URLs now checked - Previously skipped in v1.0.2, now properly validated
- Detects broken internal links - Finds links to deleted, draft, or unpublished posts
Changed
- WordPress-native validation - Uses
url_to_postid(),get_post(), and term/attachment functions instead of HTTP requests - No PHP worker deadlocks - Database queries replace self-referential HTTP calls
- Unverifiable URLs flagged - Custom routes and archives marked as warnings for manual review
v1.0.2
v1.0.2 - Production Stability
Fixes critical issues causing timeouts and warnings during link scanning.
Fixed
- Undefined property warning - Fixed
CheckResult::$redirect_urlshould be$final_url - Upstream timeouts - Internal URLs (same domain) were causing PHP worker deadlocks when the server made HTTP requests to itself
Changed
- Internal URLs auto-validated - Links pointing to the same site are now automatically marked as valid without making HTTP requests
- Prevents self-referential request blocking on limited-resource hosts (e.g., Kinsta)
- Internal links are assumed valid since they're part of the same WordPress installation