Files
rangerhq-logbook/inc/wp-notes-updater.php
ranger ead5bbcd2c release: 3.3.3 → 3.3.4 — rename Gitea repo a-wp-notes-v3 → a-logbook
The repo's old name was a holdover from the v3-of-A-WP-Notes
archival era. With the plugin firmly identifying as "Logbook" now,
the repo and the local working folder should match.

CHANGES IN-CODE
- inc/wp-notes-updater.php: WP_NOTES_GITEA_REPO constant
  a-wp-notes-v3 → a-logbook. Update checker now hits
  https://git.davidtkeane.com/api/v1/repos/ranger/a-logbook/...
  on every check. Override-able via define() in wp-config.php if
  the repo ever moves again.
- inc/wp-notes-about.php: "View the full CHANGELOG.md →" link on
  the version-history card now points at the new repo path.
- Local working folder on M3 renamed
  /Users/ranger/scripts/Gitea/a-wp-notes-v3-archive → a-logbook.
  .git/config survived; remote URL will be updated separately.

UNCHANGED (zero-migration commitment continues)
- Plugin slug 'wp-notes'.
- Plugin text domain 'a-wp-notes'.
- All wp_notes_* function names, WP_NOTES_* constants, DB option
  keys, user_meta keys, file names inside the plugin folder.
- Historical CHANGELOG references to a-wp-notes-v3 stay as
  truthful history of the v3.2.0 era.

VERSION BUMP
- wp-notes.php header Version: 3.3.3 → 3.3.4
- WP_NOTES_VERSION constant: 3.3.3 → 3.3.4
- About page version-history leads with v3.3.4; v3.3.3 demoted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:54:27 +01:00

273 lines
12 KiB
PHP

<?php
/**
* Logbook — self-hosted update checker against the Gitea repo.
*
* Polls the Gitea Releases API for the latest tagged release and
* compares its tag (e.g. "v3.3.0") against WP_NOTES_VERSION.
* Renders the result on the Settings page. NO auto-install — users
* download manually from Gitea. Safer for a self-hosted plugin and
* less surprising for admins.
*
* Result cached for 12h in a site transient so we don't hammer the
* Gitea host on every settings load. A manual "Check now" button
* force-refreshes the cache.
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
// Gitea repo coordinates — change here if the repo ever moves.
if ( ! defined( 'WP_NOTES_GITEA_HOST' ) ) { define( 'WP_NOTES_GITEA_HOST', 'https://git.davidtkeane.com' ); }
if ( ! defined( 'WP_NOTES_GITEA_OWNER' ) ) { define( 'WP_NOTES_GITEA_OWNER', 'ranger' ); }
if ( ! defined( 'WP_NOTES_GITEA_REPO' ) ) { define( 'WP_NOTES_GITEA_REPO', 'a-logbook' ); }
/**
* Convenience: full web URL of the repo / its releases page.
*/
function wp_notes_gitea_repo_url() {
return WP_NOTES_GITEA_HOST . '/' . WP_NOTES_GITEA_OWNER . '/' . WP_NOTES_GITEA_REPO;
}
function wp_notes_gitea_releases_url() {
return wp_notes_gitea_repo_url() . '/releases';
}
/**
* Hit Gitea's /api/v1/repos/<owner>/<repo>/releases/latest endpoint
* and return a normalised array, or null on irrecoverable error.
*
* Cached for 12h. Negative responses (404 = no releases yet) cached
* for 1h so a freshly-tagged release shows up quickly.
*
* @param bool $force_refresh skip the transient cache.
* @return array|null { version, html_url, download_url, body, published_at, error_code? }
*/
function wp_notes_fetch_latest_release( $force_refresh = false ) {
$cache_key = 'wp_notes_gitea_latest';
if ( ! $force_refresh ) {
$cached = get_site_transient( $cache_key );
if ( is_array( $cached ) ) { return $cached; }
}
$base_api = WP_NOTES_GITEA_HOST . '/api/v1/repos/' . WP_NOTES_GITEA_OWNER . '/' . WP_NOTES_GITEA_REPO;
// Try /releases/latest first — that's the canonical endpoint when David
// has published a formal Gitea Release via the web UI (with notes + zip
// assets attached).
$response = wp_remote_get( $base_api . '/releases/latest', array( 'timeout' => 8 ) );
if ( is_wp_error( $response ) ) {
return null;
}
$code = (int) wp_remote_retrieve_response_code( $response );
$body = ( $code === 200 ) ? json_decode( wp_remote_retrieve_body( $response ), true ) : null;
// Fallback: if no formal Release exists (typical for tag-only workflows
// where you `git tag v3.3.1 && git push --tags` without using the web
// UI), hit /tags instead and synthesise a release-like payload from the
// newest tag. Gitea's /tags endpoint sorts by creation order, so [0] is
// the most recent.
if ( $code !== 200 || ! is_array( $body ) || empty( $body['tag_name'] ) ) {
$tags_response = wp_remote_get( $base_api . '/tags?limit=1', array( 'timeout' => 8 ) );
if ( ! is_wp_error( $tags_response )
&& (int) wp_remote_retrieve_response_code( $tags_response ) === 200 ) {
$tags = json_decode( wp_remote_retrieve_body( $tags_response ), true );
if ( is_array( $tags ) && ! empty( $tags[0]['name'] ) ) {
$body = array(
'tag_name' => $tags[0]['name'],
'html_url' => wp_notes_gitea_repo_url() . '/src/tag/' . rawurlencode( $tags[0]['name'] ),
'body' => isset( $tags[0]['message'] ) ? $tags[0]['message'] : '',
'published_at' => isset( $tags[0]['commit']['created'] ) ? $tags[0]['commit']['created'] : null,
'assets' => array(),
);
$code = 200;
}
}
}
// Still nothing usable? Surface a friendly status and short-cache.
if ( $code !== 200 || ! is_array( $body ) || empty( $body['tag_name'] ) ) {
$info = array(
'version' => null,
'html_url' => wp_notes_gitea_releases_url(),
'download_url' => null,
'body' => '',
'published_at' => null,
'error_code' => $code,
);
set_site_transient( $cache_key, $info, HOUR_IN_SECONDS );
return $info;
}
// "v3.3.0" → "3.3.0" so version_compare() against WP_NOTES_VERSION works cleanly.
$version = ltrim( (string) $body['tag_name'], 'vV' );
// Prefer an explicit .zip asset attached to the release; fall back to
// Gitea's source-archive zip URL pattern.
$download_url = null;
if ( ! empty( $body['assets'] ) && is_array( $body['assets'] ) ) {
foreach ( $body['assets'] as $asset ) {
if ( isset( $asset['name'], $asset['browser_download_url'] )
&& substr( strtolower( $asset['name'] ), -4 ) === '.zip' ) {
$download_url = $asset['browser_download_url'];
break;
}
}
}
if ( ! $download_url ) {
$download_url = wp_notes_gitea_repo_url() . '/archive/' . rawurlencode( $body['tag_name'] ) . '.zip';
}
$info = array(
'version' => $version,
'html_url' => isset( $body['html_url'] ) ? esc_url_raw( $body['html_url'] ) : '',
'download_url' => esc_url_raw( $download_url ),
'body' => isset( $body['body'] ) ? wp_strip_all_tags( $body['body'] ) : '',
'published_at' => isset( $body['published_at'] ) ? $body['published_at'] : null,
);
set_site_transient( $cache_key, $info, 12 * HOUR_IN_SECONDS );
return $info;
}
/**
* Compare a fetched release against WP_NOTES_VERSION and return a
* status payload suitable for the Settings UI.
*
* @return array { status: 'available'|'up-to-date'|'unknown', current, latest?, message, ... }
*/
function wp_notes_update_status( $force_refresh = false ) {
$current = defined( 'WP_NOTES_VERSION' ) ? WP_NOTES_VERSION : '0.0.0';
$latest = wp_notes_fetch_latest_release( $force_refresh );
if ( ! $latest || empty( $latest['version'] ) ) {
$msg = 'No releases tagged on the Gitea repo yet. Once a release is published, this check will start returning a version.';
if ( $latest && ! empty( $latest['error_code'] ) && (int) $latest['error_code'] !== 404 ) {
$msg = 'Could not reach Gitea (HTTP ' . (int) $latest['error_code'] . '). Try again in a few minutes.';
}
return array(
'status' => 'unknown',
'current' => $current,
'message' => $msg,
'repo_url' => wp_notes_gitea_repo_url(),
);
}
if ( version_compare( $latest['version'], $current, '>' ) ) {
return array(
'status' => 'available',
'current' => $current,
'latest' => $latest['version'],
'html_url' => $latest['html_url'],
'download_url' => $latest['download_url'],
'published_at' => $latest['published_at'],
'body' => $latest['body'],
'message' => sprintf( 'A new version (v%s) is available — you are on v%s.', $latest['version'], $current ),
);
}
return array(
'status' => 'up-to-date',
'current' => $current,
'latest' => $latest['version'],
'message' => sprintf( 'You are up to date (v%s).', $current ),
'repo_url' => wp_notes_gitea_repo_url(),
);
}
/**
* AJAX: force a fresh check, return the status payload.
*/
add_action( 'wp_ajax_wp_notes_check_updates', 'wp_notes_ajax_check_updates' );
function wp_notes_ajax_check_updates() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient permissions.', 403 );
}
check_ajax_referer( 'wp_notes_check_updates', 'nonce' );
delete_site_transient( 'wp_notes_gitea_latest' );
wp_send_json_success( wp_notes_update_status( true ) );
}
/**
* Render the "Updates" panel on the Settings page. Called from the
* settings render function via wp_notes_settings_page().
*/
function wp_notes_render_updates_panel() {
$status = wp_notes_update_status( false ); // use the cached value on initial load
$nonce = wp_create_nonce( 'wp_notes_check_updates' );
$repo_url = wp_notes_gitea_repo_url();
$rel_url = wp_notes_gitea_releases_url();
?>
<div class="wp-notes-updates" style="max-width:720px; margin-top:24px; padding:18px 20px; background:#fff; border:1px solid #ccd0d4; border-radius:4px;">
<h2 style="margin-top:0;">Updates</h2>
<p style="margin:0 0 12px;">
Logbook is self-hosted on Gitea. Click <strong>Check now</strong> to ask the repo whether there's a newer release than the one you're running.
</p>
<p id="wp-notes-update-status" style="margin:0 0 12px;">
<strong>Status:</strong>
<span id="wp-notes-update-status-text"><?php echo esc_html( $status['message'] ); ?></span>
<?php if ( $status['status'] === 'available' && ! empty( $status['download_url'] ) ) : ?>
<br>
<a href="<?php echo esc_url( $status['download_url'] ); ?>" class="button button-primary" style="margin-top:8px;">
Download v<?php echo esc_html( $status['latest'] ); ?> (.zip)
</a>
<?php if ( ! empty( $status['html_url'] ) ) : ?>
<a href="<?php echo esc_url( $status['html_url'] ); ?>" target="_blank" rel="noopener" style="margin-left:8px;">View release notes →</a>
<?php endif; ?>
<?php endif; ?>
</p>
<p style="margin:0 0 4px;">
<button type="button" id="wp-notes-check-updates-btn" class="button" data-nonce="<?php echo esc_attr( $nonce ); ?>">
↻ Check now
</button>
<a href="<?php echo esc_url( $repo_url ); ?>" target="_blank" rel="noopener" class="button" style="margin-left:6px;">View on Gitea</a>
<a href="<?php echo esc_url( $rel_url ); ?>" target="_blank" rel="noopener" class="button" style="margin-left:6px;">View all releases</a>
</p>
<p style="margin:10px 0 0; color:#646970; font-size:12px;">
Manual update path: download the .zip, deactivate the plugin in WordPress, upload via Plugins → Add New → Upload, reactivate. Your notes are stored in <code>wp_options</code> and survive the upgrade.
</p>
</div>
<script>
(function () {
var btn = document.getElementById('wp-notes-check-updates-btn');
var statusText = document.getElementById('wp-notes-update-status-text');
if (!btn || !statusText) { return; }
btn.addEventListener('click', function () {
var nonce = btn.getAttribute('data-nonce');
btn.disabled = true;
var orig = btn.textContent;
btn.textContent = '↻ Checking…';
statusText.textContent = 'Asking Gitea…';
var fd = new FormData();
fd.append('action', 'wp_notes_check_updates');
fd.append('nonce', nonce);
fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: fd })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res || !res.success) {
statusText.textContent = (res && res.data) ? String(res.data) : 'Check failed.';
} else {
statusText.textContent = res.data.message || 'Check complete.';
if (res.data.status === 'available' && res.data.download_url) {
// Soft reload so the download / release-notes links render with the fresh data.
setTimeout(function () { window.location.reload(); }, 400);
}
}
})
.catch(function () { statusText.textContent = 'Network error — try again in a moment.'; })
.finally(function () {
btn.disabled = false;
btn.textContent = orig;
});
});
})();
</script>
<?php
}