Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/.github export-ignore
/tests export-ignore
/tools export-ignore
/translations export-ignore
/phpcs.xml.dist export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/README.md export-ignore
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ jobs:
run: find . -name '*.php' -not -path './.git/*' -print0 | xargs -0 -n1 php -l
- run: php tests/run.php

phpcs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
coverage: none
tools: composer
- name: install WPCS
run: |
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer global require --no-interaction wp-coding-standards/wpcs:^3 phpcompatibility/phpcompatibility-wp:"*"
- name: phpcs
run: |
"$(composer global config bin-dir --absolute --quiet)/phpcs"

js:
runs-on: ubuntu-latest
steps:
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,16 @@ Filters: `hibira_live_post` (turn live features off per post),
## Develop

```sh
php tests/run.php # HTTP API signing, channel auth, requests (no WordPress needed)
python3 tools/compile-mo.py # languages/*.po → .mo
php tests/run.php # HTTP API signing, channel auth, requests (no WordPress needed)
phpcs # WordPress coding standards (phpcs.xml.dist)
```

Translations: English, 繁體中文, 简体中文, 日本語, 한국어, ไทย, Tiếng Việt
(`languages/`). CI runs the tests on PHP 7.4–8.4 and the WordPress Plugin
Check.
Strings are in `languages/hibira.pot`. The plugin does not ship .po/.mo
files: WordPress.org builds and delivers them from
[translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/hibira/).
`translations/` keeps our 繁體中文, 简体中文, 日本語, 한국어, ไทย and Tiếng Việt
.po files for importing there; they are left out of the plugin zip. CI runs the
tests on PHP 7.4–8.4, PHPCS and the WordPress Plugin Check.

## License

Expand Down
1 change: 0 additions & 1 deletion assets/vendor/pusher.min.js

Large diffs are not rendered by default.

11 changes: 2 additions & 9 deletions hibira.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Hibira Realtime
* Plugin URI: https://github.com/hibira-io/wordpress
* Description: Live comments, "this post was updated" notices, a readers-now count and live WooCommerce order status, on Hibira's Pusher-compatible realtime service.
* Version: 0.1.1
* Version: 0.1.2
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Hibira
Expand All @@ -18,7 +18,7 @@

defined( 'ABSPATH' ) || exit;

define( 'HIBIRA_VERSION', '0.1.1' );
define( 'HIBIRA_VERSION', '0.1.2' );
define( 'HIBIRA_FILE', __FILE__ );
define( 'HIBIRA_DIR', plugin_dir_path( __FILE__ ) );
define( 'HIBIRA_URL', plugin_dir_url( __FILE__ ) );
Expand All @@ -32,13 +32,6 @@
require_once HIBIRA_DIR . 'includes/class-hibira-admin-notices.php';
require_once HIBIRA_DIR . 'includes/class-hibira-woocommerce.php';

add_action(
'init',
function () {
load_plugin_textdomain( 'hibira', false, dirname( plugin_basename( HIBIRA_FILE ) ) . '/languages' );
}
);

Hibira_Settings::init();
Hibira_Auth::init();
Hibira_Posts::init();
Expand Down
82 changes: 73 additions & 9 deletions includes/class-hibira-auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,96 @@

class Hibira_Auth {

/**
* Decisions made during this request, by request object.
*
* @var array
*/
private static $decisions = array();

/**
* Hooks the REST route.
*/
public static function init() {
add_action( 'rest_api_init', array( __CLASS__, 'routes' ) );
}

/**
* Registers POST /wp-json/hibira/v1/auth.
*/
public static function routes() {
register_rest_route(
'hibira/v1',
'/auth',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'auth' ),
'permission_callback' => '__return_true', // Each channel is checked in decide().
'permission_callback' => array( __CLASS__, 'permission' ),
'args' => array(
'socket_id' => array(
'type' => 'string',
'required' => true,
'type' => 'string',
'required' => true,
'validate_callback' => array( 'Hibira_Client', 'valid_socket_id' ),
),
'channel_name' => array(
'type' => 'string',
'required' => true,
'type' => 'string',
'required' => true,
'validate_callback' => array( 'Hibira_Client', 'valid_channel' ),
),
'order_key' => array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
),
'visitor' => array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
}

/**
* Lets the request through only for a channel the visitor may join. Pusher
* clients call this endpoint for visitors too (Readers now, guest order
* pages), so the check is per channel rather than per capability.
*
* @param WP_REST_Request $request The auth request.
* @return true|WP_Error
*/
public static function permission( WP_REST_Request $request ) {
if ( ! Hibira_Settings::configured() ) {
return new WP_Error( 'hibira_config', __( 'Hibira is not configured.', 'hibira' ), array( 'status' => 503 ) );
}
$decision = self::decision( $request );
if ( false === $decision || null === $decision ) {
return new WP_Error( 'hibira_forbidden', __( 'You cannot subscribe to this channel.', 'hibira' ), array( 'status' => 403 ) );
}
return true;
}

/**
* Signs the subscription.
*
* @param WP_REST_Request $request The auth request.
* @return WP_REST_Response|WP_Error
*/
public static function auth( WP_REST_Request $request ) {
$client = hibira();
if ( ! $client ) {
return new WP_Error( 'hibira_config', 'Hibira is not configured.', array( 'status' => 503 ) );
return new WP_Error( 'hibira_config', __( 'Hibira is not configured.', 'hibira' ), array( 'status' => 503 ) );
}
$channel = (string) $request['channel_name'];
$decision = self::decide( $channel, $request );
$decision = self::decision( $request );
if ( false === $decision || null === $decision ) {
return new WP_Error( 'hibira_forbidden', 'Forbidden', array( 'status' => 403 ) );
return new WP_Error( 'hibira_forbidden', __( 'You cannot subscribe to this channel.', 'hibira' ), array( 'status' => 403 ) );
}
$presence = null;
if ( 0 === strpos( $channel, 'presence-' ) ) {
if ( ! is_array( $decision ) || empty( $decision['user_id'] ) ) {
return new WP_Error( 'hibira_forbidden', 'Presence channels need a user ID.', array( 'status' => 403 ) );
return new WP_Error( 'hibira_forbidden', __( 'Presence channels need a user ID.', 'hibira' ), array( 'status' => 403 ) );
}
$presence = $decision;
}
Expand All @@ -71,6 +121,20 @@ public static function auth( WP_REST_Request $request ) {
return $response;
}

/**
* The decision for a request, made once and shared by permission() and auth().
*
* @param WP_REST_Request $request The auth request.
* @return bool|array|null
*/
private static function decision( WP_REST_Request $request ) {
$id = spl_object_hash( $request );
if ( ! array_key_exists( $id, self::$decisions ) ) {
self::$decisions[ $id ] = self::decide( (string) $request['channel_name'], $request );
}
return self::$decisions[ $id ];
}

/**
* Whether the current visitor may subscribe.
*
Expand Down
2 changes: 1 addition & 1 deletion includes/class-hibira-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public function authorize_channel( $socket_id, $channel, $presence = null ) {
$to_sign = $socket_id . ':' . $channel;
$out = array();
if ( null !== $presence ) {
$channel_data = wp_json_encode(
$channel_data = wp_json_encode(
array(
'user_id' => (string) $presence['user_id'],
'user_info' => isset( $presence['user_info'] ) ? (object) $presence['user_info'] : new stdClass(),
Expand Down
20 changes: 10 additions & 10 deletions includes/class-hibira-comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ public static function transition( $new_status, $old_status, $comment ) {
/** The event data: what a reader's browser needs to show the comment. */
public static function payload( WP_Comment $comment ) {
$data = array(
'id' => (int) $comment->comment_ID,
'parent' => (int) $comment->comment_parent,
'author' => get_comment_author( $comment ),
'url' => get_comment_author_url( $comment ),
'avatar' => get_avatar_url( $comment, array( 'size' => 64 ) ),
'date' => get_comment_date( '', $comment ) . ' ' . get_comment_time( '', false, true, $comment ),
'iso' => get_comment_date( 'c', $comment ),
'link' => get_comment_link( $comment ),
// Comment text is filtered by kses when saved; comment_text adds the formatting.
'html' => apply_filters( 'comment_text', get_comment_text( $comment ), $comment, array() ),
'id' => (int) $comment->comment_ID,
'parent' => (int) $comment->comment_parent,
'author' => get_comment_author( $comment ),
'url' => get_comment_author_url( $comment ),
'avatar' => get_avatar_url( $comment, array( 'size' => 64 ) ),
'date' => get_comment_date( '', $comment ) . ' ' . get_comment_time( '', false, true, $comment ),
'iso' => get_comment_date( 'c', $comment ),
'link' => get_comment_link( $comment ),
// Core's comment_text filter adds the formatting; kses again before it leaves the site.
'html' => wp_kses_post( apply_filters( 'comment_text', get_comment_text( $comment ), $comment, array() ) ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core filter.
);
/**
* Changes the data sent for a live comment. Keep it under 10 KB.
Expand Down
7 changes: 0 additions & 7 deletions includes/class-hibira-posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,3 @@ public static function readers_html() {
return '<span class="hibira-readers" data-hibira-readers hidden></span>';
}
}

/**
* Prints the readers-now count of the current post (when Readers now is on).
*/
function hibira_readers() {
echo Hibira_Posts::readers_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup.
}
27 changes: 25 additions & 2 deletions includes/class-hibira-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ public static function sanitize( $in ) {
foreach ( array( 'app_id', 'key', 'host' ) as $f ) {
$out[ $f ] = isset( $in[ $f ] ) ? sanitize_text_field( wp_unslash( $in[ $f ] ) ) : '';
}
$out['host'] = self::sanitize_host( $out['host'] );
if ( '' === $out['host'] ) {
$out['host'] = 'ws.hibira.io';
}
// An empty secret field keeps the saved secret; the form never shows it.
$secret = isset( $in['secret'] ) ? trim( (string) wp_unslash( $in['secret'] ) ) : '';
$secret = isset( $in['secret'] ) ? sanitize_text_field( wp_unslash( $in['secret'] ) ) : '';
$out['secret'] = '' !== $secret ? $secret : ( isset( $old['secret'] ) ? (string) $old['secret'] : '' );
$out['port'] = isset( $in['port'] ) ? max( 1, min( 65535, (int) $in['port'] ) ) : 443;
foreach ( array( 'tls', 'live_comments', 'post_updates', 'readers', 'woo_orders', 'admin_notices' ) as $f ) {
Expand All @@ -108,13 +109,35 @@ public static function sanitize( $in ) {
return $out;
}

/**
* A host name only, also from a pasted URL: no scheme, port or path.
*
* @param string $host What was typed.
* @return string
*/
public static function sanitize_host( $host ) {
$host = trim( (string) $host );
if ( false !== strpos( $host, '://' ) ) {
$host = (string) wp_parse_url( $host, PHP_URL_HOST );
}
$host = preg_replace( '#[/:?\#].*$#', '', $host );
return strtolower( preg_replace( '/[^A-Za-z0-9.\-]/', '', $host ) );
}

/** Sends a test event and lists channels, which checks the ID, key and secret. */
public static function test() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'hibira' ), 403 );
}
check_admin_referer( 'hibira_test' );
$result = hibira_trigger( 'hibira-test', 'test', array( 'from' => 'wordpress', 'at' => time() ) );
$result = hibira_trigger(
'hibira-test',
'test',
array(
'from' => 'wordpress',
'at' => time(),
)
);
if ( ! is_wp_error( $result ) ) {
$result = hibira()->get( '/channels' );
}
Expand Down
6 changes: 3 additions & 3 deletions includes/class-hibira-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class Hibira_WooCommerce {

public static function init() {
add_action( 'woocommerce_order_status_changed', array( __CLASS__, 'status_changed' ), 10, 4 );
add_action( 'woocommerce_order_status_changed', array( __CLASS__, 'status_changed' ), 10, 3 );
add_action( 'woocommerce_checkout_order_processed', array( __CLASS__, 'new_order' ), 10, 1 );
add_action( 'woocommerce_store_api_checkout_order_processed', array( __CLASS__, 'new_order' ), 10, 1 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue' ) );
Expand All @@ -42,7 +42,7 @@ public static function can_watch_order( $order_id, $user, $order_key ) {
return '' !== $order_key && hash_equals( (string) $order->get_order_key(), $order_key );
}

public static function status_changed( $order_id, $from, $to, $order = null ) {
public static function status_changed( $order_id, $from, $to ) {
if ( ! Hibira_Settings::enabled( 'woo_orders' ) ) {
return;
}
Expand Down Expand Up @@ -85,7 +85,7 @@ public static function enqueue() {
$key = '';
if ( is_order_received_page() ) {
$order_id = absint( get_query_var( 'order-received' ) );
$key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WooCommerce's own order received URL.
$key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- WooCommerce's own order received URL.
$order = wc_get_order( $order_id );
} elseif ( is_view_order_page() ) {
$order = wc_get_order( absint( get_query_var( 'view-order' ) ) );
Expand Down
27 changes: 17 additions & 10 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
* @return Hibira_Client|null
*/
function hibira() {
static $client = null;
static $client = null;
static $signature = null;
$s = Hibira_Settings::get();
$sig = md5( wp_json_encode( array( $s['app_id'], $s['key'], $s['secret'], $s['host'], $s['port'], $s['tls'] ) ) );
$s = Hibira_Settings::get();
$sig = md5( wp_json_encode( array( $s['app_id'], $s['key'], $s['secret'], $s['host'], $s['port'], $s['tls'] ) ) );
if ( $sig !== $signature ) {
$signature = $sig;
$client = Hibira_Settings::configured()
Expand Down Expand Up @@ -85,15 +85,15 @@ function hibira_register_client_script() {
'hibira',
'HibiraConfig',
array(
'key' => $s['key'],
'host' => $s['host'],
'port' => (int) $s['port'],
'tls' => (bool) $s['tls'],
'authUrl' => esc_url_raw( rest_url( 'hibira/v1/auth' ) ),
'key' => $s['key'],
'host' => $s['host'],
'port' => (int) $s['port'],
'tls' => (bool) $s['tls'],
'authUrl' => esc_url_raw( rest_url( 'hibira/v1/auth' ) ),
// Only signed-in users send a nonce: a cached page can hold a stale
// one, which the REST API refuses even for visitors.
'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
'i18n' => array(
'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
'i18n' => array(
'updated' => __( 'This post was updated.', 'hibira' ),
'reload' => __( 'Reload', 'hibira' ),
'newComment' => __( 'New comment', 'hibira' ),
Expand All @@ -112,3 +112,10 @@ function hibira_register_client_script() {
)
);
}

/**
* Prints the readers-now count of the current post (when Readers now is on).
*/
function hibira_readers() {
echo Hibira_Posts::readers_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup.
}
Binary file removed languages/hibira-ja.mo
Binary file not shown.
Binary file removed languages/hibira-ko_KR.mo
Binary file not shown.
Binary file removed languages/hibira-th.mo
Binary file not shown.
Binary file removed languages/hibira-vi.mo
Binary file not shown.
Binary file removed languages/hibira-zh_CN.mo
Binary file not shown.
Binary file removed languages/hibira-zh_TW.mo
Binary file not shown.
Loading
Loading