From 1703a09319870a4a9ba070bdf0e6e73fb7dcbc6a Mon Sep 17 00:00:00 2001 From: Krystie Date: Sun, 7 Jun 2026 21:09:28 -0700 Subject: [PATCH] fix(v1.7.1): wordmark image on every beep + reply, restore updater - Render the Beep wordmark bottom-right of every beep AND reply, with the delete button furthest right (was: wordmark right of delete, missing on replies, and duplicated after the replies block). - Draw the wordmark as crisp inline SVG via beep_post_wordmark() (filterable); the bundled beep-wordmark.png assets are corrupted. Readable on dark theme. - Restore class-updater.php (self-hosted Gitea updates) deleted in the master->main consolidation; re-wire it in beep.php. - Remove stray includes/class-shortcode.php.bak. - Bump to 1.7.1; sync readme.txt stable tag + changelog. Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/beep.css | 24 ++- beep.php | 11 +- includes/class-shortcode.php.bak | 292 ----------------------------- includes/class-updater.php | 309 +++++++++++++++++++++++++++++++ includes/helpers.php | 31 +++- readme.txt | 11 +- 6 files changed, 367 insertions(+), 311 deletions(-) delete mode 100755 includes/class-shortcode.php.bak create mode 100644 includes/class-updater.php diff --git a/assets/beep.css b/assets/beep.css index 0e03e94..34d1a91 100755 --- a/assets/beep.css +++ b/assets/beep.css @@ -1054,8 +1054,7 @@ /* Bottom bar: wordmark left, delete right */ /* Bottom bar: wordmark left, delete right */ /* Bottom bar: delete left, wordmark right (blue, large) */ -/* Bottom bar: delete left, wordmark right (blue) */ -/* Bottom bar: delete left, wordmark text right */ +/* Bottom bar: wordmark on the left, delete furthest right */ .beep-embed .beep-bottom-bar { display: flex; align-items: center; @@ -1063,15 +1062,20 @@ gap: 8px; margin-top: 4px; } -.beep-embed .beep-wordmark { - font-size: 16px; - font-weight: 800; - color: #0064cd; - opacity: 0.5; - letter-spacing: -0.5px; +/* Brand wordmark (inline SVG, inherits colour via currentColor) */ +.beep-embed .beep-post-wordmark { + color: var(--beep-accent); + opacity: 0.55; + flex: 0 0 auto; + height: 16px; + width: auto; transition: opacity 0.15s ease; user-select: none; } -.beep-embed .beep-post:hover .beep-wordmark { - opacity: 1; +.beep-embed .beep-bottom-bar-reply .beep-post-wordmark { + height: 13px; +} +.beep-embed .beep-post:hover .beep-post-wordmark, +.beep-embed .beep-reply:hover .beep-post-wordmark { + opacity: 0.9; } diff --git a/beep.php b/beep.php index c8a9117..7b76062 100755 --- a/beep.php +++ b/beep.php @@ -2,7 +2,7 @@ /** * Plugin Name: Beep * Description: A Twitter-style microblog for WordPress, with likes, replies, and Gravatars. Use the [beep_feed] shortcode on any page. - * Version: 1.7.0 + * Version: 1.7.1 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: Sami Ahmed @@ -14,7 +14,7 @@ if (!defined('ABSPATH')) { exit; } -define('BEEP_VERSION', '1.7.0'); +define('BEEP_VERSION', '1.7.1'); define('BEEP_FILE', __FILE__); define('BEEP_DIR', plugin_dir_path(__FILE__)); define('BEEP_URL', plugin_dir_url(__FILE__)); @@ -31,9 +31,16 @@ require_once BEEP_DIR . 'includes/class-quotes.php'; require_once BEEP_DIR . 'includes/class-polls.php'; require_once BEEP_DIR . 'includes/class-shortcode.php'; require_once BEEP_DIR . 'includes/class-settings.php'; +require_once BEEP_DIR . 'includes/class-updater.php'; require_once BEEP_DIR . 'includes/class-beep.php'; register_activation_hook(__FILE__, ['Beep_Plugin', 'activate']); register_deactivation_hook(__FILE__, ['Beep_Plugin', 'deactivate']); add_action('plugins_loaded', ['Beep_Plugin', 'init']); + +// Self-hosted updates via the Gitea releases API. +// Register in admin and during wp-cron (where WordPress runs its update checks). +if (is_admin() || (defined('DOING_CRON') && DOING_CRON)) { + Beep_Updater::init(); +} diff --git a/includes/class-shortcode.php.bak b/includes/class-shortcode.php.bak deleted file mode 100755 index 8fd52c4..0000000 --- a/includes/class-shortcode.php.bak +++ /dev/null @@ -1,292 +0,0 @@ - 'GET', - 'callback' => [__CLASS__, 'fetch_feed'], - 'permission_callback' => '__return_true', - 'args' => [ - 'sort' => ['default' => 'latest'], - 'limit' => ['default' => 50, 'validate_callback' => function ($v) { return is_numeric($v); }], - 'offset' => ['default' => 0, 'validate_callback' => function ($v) { return is_numeric($v); }], - ], - ]); - } - - /** - * REST endpoint: re-render the feed list with the given sort. - */ - public static function fetch_feed($request) { - $sort = $request->get_param('sort') === 'top' ? 'top' : 'latest'; - $limit = max(1, (int) $request->get_param('limit')); - $offset = max(0, (int) $request->get_param('offset')); - $posts = ($sort === 'top') ? self::get_top_beeps($limit + 1, $offset) : self::get_latest_beeps($limit + 1, $offset); - - $has_more = count($posts) > $limit; - if ($has_more) { - $posts = array_slice($posts, 0, $limit); - } - - $html = ''; - foreach ($posts as $p) { - $html .= beep_render_post($p); - } - if ($html === '' && $offset === 0) { - $empty_bird = beep_bird_svg( ['size' => 64, 'fill' => '#4a90c4'] ); - $html = '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; - } - return rest_ensure_response(['html' => $html, 'sort' => $sort, 'has_more' => $has_more, 'offset' => $offset + count($posts)]); - } - - public static function get_latest_beeps($limit, $offset = 0) { - global $wpdb; - return $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$wpdb->posts} - WHERE post_type = %s AND post_status = 'publish' - ORDER BY post_date DESC - LIMIT %d OFFSET %d", - Beep_CPT::POST_TYPE, $limit, $offset - )); - } - - public static function get_top_beeps($limit, $offset = 0) { - global $wpdb; - $likes = Beep_Likes::table_name(); - return $wpdb->get_results($wpdb->prepare( - "SELECT p.*, COALESCE(l.cnt, 0) AS like_count - FROM {$wpdb->posts} p - LEFT JOIN ( - SELECT post_id, COUNT(*) AS cnt - FROM $likes - WHERE object_type = 'post' - GROUP BY post_id - ) l ON l.post_id = p.ID - WHERE p.post_type = %s AND p.post_status = 'publish' - ORDER BY COALESCE(l.cnt, 0) DESC, p.post_date DESC - LIMIT %d OFFSET %d", - Beep_CPT::POST_TYPE, $limit, $offset - )); - } - - public static function render_feed($atts) { - $atts = shortcode_atts([ - 'limit' => 50, - 'header' => 'Beep!', - 'show_wordmark' => true, - 'sort' => 'latest', - ], $atts, 'beep_feed'); - - $limit = max(1, (int) $atts['limit']); - $sort = ($atts['sort'] === 'top') ? 'top' : 'latest'; - $posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit); - $has_posts = !empty($posts); - - ob_start(); - ?> -
-
-
-
Messages from Beep!
-
- - -
- - -
- - - - - -
- Your account has been suspended from beeping. -
- - - - -
- 64, 'fill' => '#4a90c4'] ); - echo '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; - } - ?> -
-
-
- \d+)', [ - 'methods' => 'POST', - 'callback' => [__CLASS__, 'upload_media'], - 'permission_callback' => function () { - return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); - }, - ]); - register_rest_route('beep/v1', '/media/(?P\d+)/(?P\d+)', [ - 'methods' => 'DELETE', - 'callback' => [__CLASS__, 'delete_media'], - 'permission_callback' => function () { - return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); - }, - ]); - register_rest_route('beep/v1', '/voice/(?P\d+)', [ - 'methods' => 'POST', - 'callback' => [__CLASS__, 'upload_voice'], - 'permission_callback' => function () { - return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); - }, - ]); - } - - public static function upload_media($request) { - $post_id = (int) $request->get_param('post_id'); - $post = get_post($post_id); - - if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { - return new WP_Error('not_found', 'Beep not found.', 404); - } - if (!current_user_can('edit_post', $post_id)) { - return new WP_Error('forbidden', 'Cannot edit this beep.', 403); - } - if (empty($_FILES['file'])) { - return new WP_Error('no_file', 'No file uploaded.'); - } - - $result = Beep_Media::handle_upload($post_id, $_FILES['file']); - if (is_wp_error($result)) { - return $result; - } - return rest_ensure_response($result); - } - - public static function delete_media($request) { - $post_id = (int) $request->get_param('post_id'); - $attachment_id = (int) $request->get_param('attachment_id'); - $post = get_post($post_id); - - if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { - return new WP_Error('not_found', 'Beep not found.', 404); - } - if (!current_user_can('edit_post', $post_id)) { - return new WP_Error('forbidden', 'Cannot edit this beep.', 403); - } - - Beep_Media::remove_image($post_id, $attachment_id); - return rest_ensure_response(['ok' => true]); - } - public static function upload_voice($request) { - $post_id = (int) $request->get_param('post_id'); - $post = get_post($post_id); - - if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { - return new WP_Error('not_found', 'Beep not found.', 404); - } - if (!current_user_can('edit_post', $post_id)) { - return new WP_Error('forbidden', 'Cannot edit this beep.', 403); - } - if (empty($_FILES['file'])) { - return new WP_Error('no_file', 'No file uploaded.'); - } - - $result = Beep_Media::handle_voice_upload($post_id, $_FILES['file']); - if (is_wp_error($result)) { - return $result; - } - return rest_ensure_response($result); - } - - - public static function register_thread_routes() { - register_rest_route('beep/v1', '/thread/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [__CLASS__, 'get_thread'], - 'permission_callback' => '__return_true', - ]); - } - - public static function get_thread($request) { - $post_id = (int) $request->get_param('post_id'); - if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) { - return new WP_Error('not_found', 'Beep not found.', 404); - } - $depth = (int) $request->get_param('depth') ?: 10; - $thread = Beep_Replies::get_thread($post_id, 0, $depth); - return rest_ensure_response($thread); - } - - public static function register_quote_routes() { - register_rest_route('beep/v1', '/quote/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [__CLASS__, 'get_quoted_post'], - 'permission_callback' => '__return_true', - ]); - register_rest_route('beep/v1', '/lookup', [ - 'methods' => 'GET', - 'callback' => [__CLASS__, 'lookup_beep'], - 'permission_callback' => '__return_true', - ]); - } - - public static function get_quoted_post($request) { - $post_id = (int) $request->get_param('post_id'); - $quoted = Beep_Quotes::get_quoted_post($post_id); - if (!$quoted) { - return new WP_Error('not_found', 'No quoted post found.', 404); - } - return rest_ensure_response($quoted); - } - - public static function lookup_beep($request) { - $query = sanitize_text_field($request->get_param('q')); - if (!$query) { - return new WP_Error('missing_query', 'Query parameter q is required.', 400); - } - if (is_numeric($query)) { - $post = get_post((int) $query); - if ($post && $post->post_type === Beep_CPT::POST_TYPE) { - return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); - } - } - if (preg_match('/\[beep:(\d+)\]/', $query, $m)) { - $post = get_post((int) $m[1]); - if ($post && $post->post_type === Beep_CPT::POST_TYPE) { - return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); - } - } - return new WP_Error('not_found', 'Beep not found.', 404); - } -} diff --git a/includes/class-updater.php b/includes/class-updater.php new file mode 100644 index 0000000..ad149dc --- /dev/null +++ b/includes/class-updater.php @@ -0,0 +1,309 @@ +plugin_file, [$self, 'action_links']); + } + + public function __construct() { + $this->plugin_file = plugin_basename(BEEP_FILE); + $this->plugin_slug = dirname($this->plugin_file); + if ($this->plugin_slug === '.' || $this->plugin_slug === '') { + $this->plugin_slug = 'beep'; + } + $this->version = BEEP_VERSION; + $base = defined('BEEP_UPDATE_GITEA_BASE') ? BEEP_UPDATE_GITEA_BASE : 'https://git.sami'; + $this->api_base = rtrim($base, '/'); + $this->repo = defined('BEEP_UPDATE_REPO') ? BEEP_UPDATE_REPO : 'sami7777/beep'; + } + + /** Resolve the access token (constant first, then filter). */ + protected function token() { + if (defined('BEEP_UPDATE_TOKEN') && BEEP_UPDATE_TOKEN) { + return BEEP_UPDATE_TOKEN; + } + return (string) apply_filters('beep_update_token', ''); + } + + protected function api_url($path) { + return $this->api_base . '/api/v1/repos/' . $this->repo . $path; + } + + /** + * Fetch + cache the latest release. Returns an info array or null. + */ + protected function get_remote($force = false) { + if (!$force) { + $cached = get_site_transient(self::TRANSIENT); + if ($cached !== false) { + return is_array($cached) ? $cached : null; // '' means "checked, nothing" + } + } + + $headers = ['Accept' => 'application/json']; + $token = $this->token(); + if ($token) { + $headers['Authorization'] = 'token ' . $token; + } + + $res = wp_remote_get($this->api_url('/releases/latest'), [ + 'timeout' => 15, + 'headers' => $headers, + ]); + + if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) { + // Brief negative cache so a flaky/unauthorized server doesn't stall every admin page. + set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS); + return null; + } + + $data = json_decode(wp_remote_retrieve_body($res), true); + if (!is_array($data) || empty($data['tag_name'])) { + set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS); + return null; + } + + $info = $this->parse_release($data); + set_site_transient(self::TRANSIENT, $info, self::CACHE_HOURS * HOUR_IN_SECONDS); + return $info; + } + + /** Normalize a Gitea release payload into the fields we need. */ + protected function parse_release($data) { + $version = ltrim((string) $data['tag_name'], 'vV'); + + // Prefer an attached .zip asset; fall back to the source archive. + $package = ''; + if (!empty($data['assets']) && is_array($data['assets'])) { + foreach ($data['assets'] as $asset) { + if (!empty($asset['browser_download_url']) + && strtolower(substr($asset['name'], -4)) === '.zip') { + $package = $asset['browser_download_url']; + break; + } + } + } + if (!$package) { + $package = !empty($data['zipball_url']) + ? $data['zipball_url'] + : $this->api_base . '/' . $this->repo . '/archive/' . $data['tag_name'] . '.zip'; + } + + return [ + 'version' => $version, + 'package' => $package, + 'changelog' => isset($data['body']) ? (string) $data['body'] : '', + 'published_at' => isset($data['published_at']) ? $data['published_at'] : '', + 'name' => isset($data['name']) ? $data['name'] : '', + 'html_url' => isset($data['html_url']) ? $data['html_url'] : ($this->api_base . '/' . $this->repo), + ]; + } + + /** Inject our plugin into the WordPress update transient. */ + public function check_for_update($transient) { + if (!is_object($transient)) { + return $transient; + } + + $info = $this->get_remote(); + if (!$info) { + return $transient; + } + + $has_update = version_compare($info['version'], $this->version, '>'); + + $obj = (object) [ + 'slug' => $this->plugin_slug, + 'plugin' => $this->plugin_file, + 'new_version' => $has_update ? $info['version'] : $this->version, + 'url' => $info['html_url'], + 'package' => $has_update ? $info['package'] : '', + 'icons' => [], + 'banners' => [], + 'tested' => '', + 'requires_php' => '', + ]; + + if ($has_update) { + $transient->response[$this->plugin_file] = $obj; + } else { + // Listing it as no_update keeps "View details" / auto-update UI working. + if (!isset($transient->no_update)) { + $transient->no_update = []; + } + $transient->no_update[$this->plugin_file] = $obj; + } + + return $transient; + } + + /** Provide the "View details" popup content. */ + public function plugin_info($result, $action, $args) { + if ($action !== 'plugin_information') { + return $result; + } + if (empty($args->slug) || $args->slug !== $this->plugin_slug) { + return $result; + } + + $info = $this->get_remote(); + if (!$info) { + return $result; + } + + $meta = get_file_data(BEEP_FILE, [ + 'Name' => 'Plugin Name', + 'Author' => 'Author', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + ]); + + return (object) [ + 'name' => $meta['Name'] ?: 'Beep', + 'slug' => $this->plugin_slug, + 'version' => $info['version'], + 'author' => esc_html($meta['Author']), + 'homepage' => $info['html_url'], + 'download_link' => $info['package'], + 'requires' => $meta['RequiresWP'], + 'requires_php' => $meta['RequiresPHP'], + 'last_updated' => $info['published_at'], + 'sections' => [ + 'changelog' => $this->format_changelog($info['changelog']), + ], + ]; + } + + /** Very small Markdown → HTML pass for the changelog body. */ + protected function format_changelog($md) { + if ($md === '') { + return 'No changelog provided for this release.'; + } + $html = esc_html($md); + $html = preg_replace('/^###\s+(.*)$/m', '

$1

', $html); + $html = preg_replace('/^##\s+(.*)$/m', '

$1

', $html); + $html = preg_replace('/^[\*\-]\s+(.*)$/m', '
  • $1
  • ', $html); + $html = preg_replace('/(
  • .*<\/li>)/s', '
      $1
    ', $html); + return wpautop($html); + } + + /** + * Attach the auth token to any request aimed at our Gitea host so the + * API check AND the package download both authenticate on a private repo. + */ + public function authorize_request($args, $url) { + $token = $this->token(); + if (!$token) { + return $args; + } + $host = wp_parse_url($this->api_base, PHP_URL_HOST); + $url_host = wp_parse_url($url, PHP_URL_HOST); + if ($host && $url_host && strcasecmp($host, $url_host) === 0) { + if (empty($args['headers']) || !is_array($args['headers'])) { + $args['headers'] = []; + } + $args['headers']['Authorization'] = 'token ' . $token; + } + return $args; + } + + /** + * Gitea archives unzip to a folder like "beep" or "sami7777-beep-". + * Rename it to the plugin slug so WP updates the existing folder in place. + */ + public function fix_source_dir($source, $remote_source, $upgrader, $hook_extra = null) { + if (!is_array($hook_extra) + || empty($hook_extra['plugin']) + || $hook_extra['plugin'] !== $this->plugin_file) { + return $source; + } + + global $wp_filesystem; + if (!$wp_filesystem) { + return $source; + } + + $desired = trailingslashit($remote_source) . $this->plugin_slug . '/'; + if (untrailingslashit($source) === untrailingslashit($desired)) { + return $source; + } + + if ($wp_filesystem->move($source, $desired, true)) { + return $desired; + } + return $source; + } + + /** Drop the cache after an update so the new version is reflected at once. */ + public function clear_cache($upgrader = null, $options = []) { + delete_site_transient(self::TRANSIENT); + } + + /** "Check for updates" link on the Plugins screen. */ + public function action_links($links) { + $url = wp_nonce_url( + self_admin_url('plugins.php?beep_force_update_check=1'), + 'beep_force_update_check' + ); + $links[] = 'Check for updates'; + return $links; + } +} + +/** + * Handle the manual "Check for updates" click: clear cache + force a re-check. + */ +add_action('admin_init', function () { + if (empty($_GET['beep_force_update_check'])) { + return; + } + if (!current_user_can('update_plugins') + || !check_admin_referer('beep_force_update_check')) { + return; + } + delete_site_transient(Beep_Updater::TRANSIENT); + delete_site_transient('update_plugins'); + wp_update_plugins(); + wp_safe_redirect(self_admin_url('plugins.php')); + exit; +}); diff --git a/includes/helpers.php b/includes/helpers.php index 82241e2..5f514ef 100755 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -54,6 +54,25 @@ function beep_logo_svg() { . ' alt="Beep!" style="height:32px;width:auto;filter:' . esc_attr( $filter ) . ';">'; } +/** + * Small "Beep!" brand wordmark shown at the bottom-right of every beep and reply. + * + * Rendered as crisp inline SVG so it stays sharp at any size and inherits its + * colour from CSS (currentColor). The bundled beep-wordmark.png assets are + * corrupted, so we draw the wordmark as vector text instead. To use a real + * image once you have a clean asset, hook the `beep_post_wordmark` filter: + * add_filter('beep_post_wordmark', fn() => ''); + */ +function beep_post_wordmark() { + $svg = '' + . 'Beep!' + . ''; + return apply_filters('beep_post_wordmark', $svg); +} + /** * Is this user banned from posting/liking/replying? */ @@ -332,6 +351,7 @@ function beep_render_post($post) {
    + - Beep!
    @@ -352,7 +371,6 @@ function beep_render_post($post) { echo beep_render_replies_tree($all, 0, 0, $post->ID, $can_interact); ?> - Beep! comment_ID); ?> - -
    +
    + + -
    - + +
    Beep) for Giphy API key and branding.