326 lines
13 KiB
PHP
326 lines
13 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* The Sirius Press settings screen.
|
||
|
|
*
|
||
|
|
* One page, because there are only a handful of decisions to make and they
|
||
|
|
* are all consequential: which name this site is, which network, where the
|
||
|
|
* gateway is, and whether the server is allowed to hold the publishing key.
|
||
|
|
*
|
||
|
|
* The screen does one thing stock WordPress settings screens rarely do: it
|
||
|
|
* checks its own answers against reality. After a phrase is saved it derives
|
||
|
|
* the address, asks the gateway who currently owns the name, and says plainly
|
||
|
|
* whether those match. A mismatch caught here is a sentence of explanation; a
|
||
|
|
* mismatch caught later is a 403 in a cron log that nobody reads.
|
||
|
|
*
|
||
|
|
* @package SiriusPress
|
||
|
|
*/
|
||
|
|
|
||
|
|
defined( 'ABSPATH' ) || exit;
|
||
|
|
|
||
|
|
final class SP_Admin {
|
||
|
|
|
||
|
|
const PAGE = 'sirius-press';
|
||
|
|
|
||
|
|
public static function hooks() {
|
||
|
|
add_action( 'admin_menu', array( __CLASS__, 'menu' ) );
|
||
|
|
add_filter( 'plugin_action_links_' . plugin_basename( SIRIUS_PRESS_CORE_FILE ), array( __CLASS__, 'action_links' ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function action_links( $links ) {
|
||
|
|
array_unshift(
|
||
|
|
$links,
|
||
|
|
'<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Settings', 'sirius-press' ) . '</a>'
|
||
|
|
);
|
||
|
|
return $links;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function menu() {
|
||
|
|
add_menu_page(
|
||
|
|
__( 'Sirius Press', 'sirius-press' ),
|
||
|
|
__( 'Sirius Press', 'sirius-press' ),
|
||
|
|
'manage_options',
|
||
|
|
self::PAGE,
|
||
|
|
array( __CLASS__, 'render' ),
|
||
|
|
'dashicons-admin-site-alt3',
|
||
|
|
71
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function render() {
|
||
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||
|
|
wp_die( esc_html__( 'You are not allowed to configure this site.', 'sirius-press' ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
$errors = array();
|
||
|
|
$notes = array();
|
||
|
|
|
||
|
|
if ( isset( $_POST['sirius_press_settings'] ) && check_admin_referer( 'sirius_press_settings' ) ) {
|
||
|
|
list( $errors, $notes ) = self::save( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each field sanitised in save().
|
||
|
|
}
|
||
|
|
|
||
|
|
$name = SP_Settings::name();
|
||
|
|
$address = SP_Settings::publishing_address();
|
||
|
|
$mode = SP_Settings::mode();
|
||
|
|
$requirement = sirius_press_requirements_problem();
|
||
|
|
|
||
|
|
echo '<div class="wrap"><h1>' . esc_html__( 'Sirius Press', 'sirius-press' ) . '</h1>';
|
||
|
|
|
||
|
|
if ( '' !== $requirement ) {
|
||
|
|
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $requirement ) );
|
||
|
|
}
|
||
|
|
foreach ( $errors as $e ) {
|
||
|
|
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $e ) );
|
||
|
|
}
|
||
|
|
foreach ( $notes as $n ) {
|
||
|
|
printf( '<div class="notice notice-success"><p>%s</p></div>', wp_kses_post( $n ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
echo '<form method="post">';
|
||
|
|
wp_nonce_field( 'sirius_press_settings' );
|
||
|
|
echo '<input type="hidden" name="sirius_press_settings" value="1" />';
|
||
|
|
|
||
|
|
echo '<h2>' . esc_html__( 'Identity', 'sirius-press' ) . '</h2>';
|
||
|
|
echo '<table class="form-table" role="presentation">';
|
||
|
|
|
||
|
|
self::text_row(
|
||
|
|
'sirius_name',
|
||
|
|
__( 'BCNR name', 'sirius-press' ),
|
||
|
|
$name,
|
||
|
|
__( 'The name this site publishes under, exactly as registered — for example <code>example.bch</code>. Point that name\'s <code>p</code> or <code>ip</code> record at this server so visitors reach it.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
|
||
|
|
echo '<tr><th><label for="sirius_network">' . esc_html__( 'Network', 'sirius-press' ) . '</label></th><td>';
|
||
|
|
echo '<select name="sirius_network" id="sirius_network">';
|
||
|
|
foreach ( array(
|
||
|
|
'mainnet' => __( 'Mainnet (bitcoincash:…)', 'sirius-press' ),
|
||
|
|
'chipnet' => __( 'Chipnet (bchtest:…) — testing only', 'sirius-press' ),
|
||
|
|
) as $value => $label ) {
|
||
|
|
printf(
|
||
|
|
'<option value="%s"%s>%s</option>',
|
||
|
|
esc_attr( $value ),
|
||
|
|
selected( SP_Settings::network(), $value, false ),
|
||
|
|
esc_html( $label )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
echo '</select>';
|
||
|
|
echo '<p class="description">' . esc_html__( 'Accounts on this site must use addresses from this network. Changing it after people have signed up locks them out.', 'sirius-press' ) . '</p>';
|
||
|
|
echo '</td></tr>';
|
||
|
|
|
||
|
|
self::text_row(
|
||
|
|
'sirius_gateway',
|
||
|
|
__( 'Gateway', 'sirius-press' ),
|
||
|
|
SP_Settings::gateway(),
|
||
|
|
__( 'The BNS gateway that accepts static exports. Leave as the default unless you run your own.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
|
||
|
|
echo '</table>';
|
||
|
|
|
||
|
|
echo '<h2>' . esc_html__( 'Publishing', 'sirius-press' ) . '</h2>';
|
||
|
|
echo '<table class="form-table" role="presentation">';
|
||
|
|
|
||
|
|
echo '<tr><th>' . esc_html__( 'How exports are signed', 'sirius-press' ) . '</th><td><fieldset>';
|
||
|
|
printf(
|
||
|
|
'<label><input type="radio" name="sirius_mode" value="%s"%s> %s</label><p class="description" style="margin:4px 0 12px 24px">%s</p>',
|
||
|
|
esc_attr( SP_Settings::MODE_MANUAL ),
|
||
|
|
checked( $mode, SP_Settings::MODE_MANUAL, false ),
|
||
|
|
esc_html__( 'Manual — nothing is stored on this server', 'sirius-press' ),
|
||
|
|
esc_html__( 'Exports queue up and are signed from your browser when you visit the export screen. Safest, but a post published by a schedule sits in the queue until somebody signs in.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
printf(
|
||
|
|
'<label><input type="radio" name="sirius_mode" value="%s"%s> %s</label><p class="description" style="margin:4px 0 0 24px">%s</p>',
|
||
|
|
esc_attr( SP_Settings::MODE_SERVER ),
|
||
|
|
checked( $mode, SP_Settings::MODE_SERVER, false ),
|
||
|
|
esc_html__( 'Automatic — this server holds the publishing key', 'sirius-press' ),
|
||
|
|
esc_html__( 'Posts export the moment they go live, with no one present. The trade is real: the recovery phrase below is the phrase that owns the name and its funds, and anyone who can read this server\'s database and its wp-config.php can take both.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
echo '</fieldset></td></tr>';
|
||
|
|
|
||
|
|
echo '<tr><th><label for="sirius_phrase">' . esc_html__( 'Recovery phrase', 'sirius-press' ) . '</label></th><td>';
|
||
|
|
printf(
|
||
|
|
'<textarea name="sirius_phrase" id="sirius_phrase" rows="3" class="large-text code" autocomplete="off" spellcheck="false" placeholder="%s"></textarea>',
|
||
|
|
esc_attr__( 'twelve words, separated by spaces', 'sirius-press' )
|
||
|
|
);
|
||
|
|
if ( SP_Settings::has_phrase() ) {
|
||
|
|
echo '<p class="description">' . esc_html__( 'A phrase is stored. Leave this blank to keep it, or type a new one to replace it.', 'sirius-press' ) . '</p>';
|
||
|
|
echo '<p><label><input type="checkbox" name="sirius_forget_phrase" value="1"> ' . esc_html__( 'Forget the stored phrase', 'sirius-press' ) . '</label></p>';
|
||
|
|
} else {
|
||
|
|
echo '<p class="description">' . esc_html__( 'Only needed for automatic publishing. It is encrypted before it is written to the database — which protects a stolen database dump, and nothing more.', 'sirius-press' ) . '</p>';
|
||
|
|
}
|
||
|
|
echo '</td></tr>';
|
||
|
|
|
||
|
|
self::text_row(
|
||
|
|
'sirius_path',
|
||
|
|
__( 'Derivation path', 'sirius-press' ),
|
||
|
|
SP_Settings::derivation_path(),
|
||
|
|
__( 'Leave alone unless your wallet was created somewhere that used a different path. The default matches the Sirius portal wallet.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
|
||
|
|
echo '<tr><th>' . esc_html__( 'Publish on update', 'sirius-press' ) . '</th><td><label>';
|
||
|
|
printf(
|
||
|
|
'<input type="checkbox" name="sirius_auto_publish" value="1"%s> %s',
|
||
|
|
checked( SP_Settings::auto_publish(), true, false ),
|
||
|
|
esc_html__( 'Export a page to the name\'s storage whenever it is published or edited', 'sirius-press' )
|
||
|
|
);
|
||
|
|
echo '</label></td></tr>';
|
||
|
|
|
||
|
|
echo '</table>';
|
||
|
|
|
||
|
|
echo '<h2>' . esc_html__( 'Accounts', 'sirius-press' ) . '</h2>';
|
||
|
|
echo '<table class="form-table" role="presentation">';
|
||
|
|
echo '<tr><th>' . esc_html__( 'Registration', 'sirius-press' ) . '</th><td><label>';
|
||
|
|
printf(
|
||
|
|
'<input type="checkbox" name="sirius_open_registration" value="1"%s> %s',
|
||
|
|
checked( SP_Settings::open_registration(), true, false ),
|
||
|
|
esc_html__( 'Anyone who can sign with a wallet may create an account', 'sirius-press' )
|
||
|
|
);
|
||
|
|
echo '</label></td></tr>';
|
||
|
|
self::text_row(
|
||
|
|
'sirius_stub_domain',
|
||
|
|
__( 'Placeholder mail domain', 'sirius-press' ),
|
||
|
|
SP_Settings::stub_email_domain(),
|
||
|
|
__( 'Plugins that insist on an email address get one here. It must stay unroutable — keep the <code>.invalid</code> ending, which is reserved so it can never resolve.', 'sirius-press' )
|
||
|
|
);
|
||
|
|
echo '</table>';
|
||
|
|
|
||
|
|
submit_button();
|
||
|
|
echo '</form>';
|
||
|
|
|
||
|
|
self::render_status( $name, $address );
|
||
|
|
echo '</div>';
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The reality check: does the stored key actually own the name? */
|
||
|
|
private static function render_status( $name, $address ) {
|
||
|
|
echo '<h2>' . esc_html__( 'Status', 'sirius-press' ) . '</h2>';
|
||
|
|
echo '<table class="widefat striped" style="max-width:860px"><tbody>';
|
||
|
|
|
||
|
|
self::status_row( __( 'Name', 'sirius-press' ), '' !== $name ? $name : __( 'not set', 'sirius-press' ), '' !== $name );
|
||
|
|
self::status_row(
|
||
|
|
__( 'Publishing address', 'sirius-press' ),
|
||
|
|
'' !== $address ? $address : __( 'no phrase stored', 'sirius-press' ),
|
||
|
|
'' !== $address
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( '' === $name ) {
|
||
|
|
echo '</tbody></table>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$owner = SP_Gateway::owner_of_name();
|
||
|
|
if ( '' === $owner ) {
|
||
|
|
self::status_row(
|
||
|
|
__( 'On-chain owner', 'sirius-press' ),
|
||
|
|
__( 'the gateway did not answer — it may not have indexed this name yet', 'sirius-press' ),
|
||
|
|
null
|
||
|
|
);
|
||
|
|
} elseif ( '' === $address ) {
|
||
|
|
self::status_row( __( 'On-chain owner', 'sirius-press' ), $owner, null );
|
||
|
|
} elseif ( hash_equals( $owner, $address ) ) {
|
||
|
|
self::status_row(
|
||
|
|
__( 'On-chain owner', 'sirius-press' ),
|
||
|
|
__( 'matches the stored key — this site can publish', 'sirius-press' ),
|
||
|
|
true
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
self::status_row(
|
||
|
|
__( 'On-chain owner', 'sirius-press' ),
|
||
|
|
sprintf(
|
||
|
|
/* translators: %s: the address that currently owns the name. */
|
||
|
|
__( '%s — which is NOT the stored key. Exports will be refused until the phrase here is the one that owns the name.', 'sirius-press' ),
|
||
|
|
$owner
|
||
|
|
),
|
||
|
|
false
|
||
|
|
);
|
||
|
|
}
|
||
|
|
echo '</tbody></table>';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function status_row( $label, $value, $ok ) {
|
||
|
|
$mark = true === $ok ? '✓' : ( false === $ok ? '✗' : '·' );
|
||
|
|
$col = true === $ok ? '#007017' : ( false === $ok ? '#b32d2e' : '#646970' );
|
||
|
|
printf(
|
||
|
|
'<tr><th style="width:200px;text-align:left">%s</th><td><span style="color:%s;font-weight:700">%s</span> <code>%s</code></td></tr>',
|
||
|
|
esc_html( $label ),
|
||
|
|
esc_attr( $col ),
|
||
|
|
esc_html( $mark ),
|
||
|
|
esc_html( $value )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function text_row( $field, $label, $value, $description ) {
|
||
|
|
printf(
|
||
|
|
'<tr><th><label for="%1$s">%2$s</label></th><td><input type="text" name="%1$s" id="%1$s" value="%3$s" class="regular-text code"><p class="description">%4$s</p></td></tr>',
|
||
|
|
esc_attr( $field ),
|
||
|
|
esc_html( $label ),
|
||
|
|
esc_attr( $value ),
|
||
|
|
wp_kses_post( $description )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array{0:string[],1:string[]} errors, notices */
|
||
|
|
private static function save( $post ) {
|
||
|
|
$errors = array();
|
||
|
|
$notes = array();
|
||
|
|
|
||
|
|
$name = strtolower( sanitize_text_field( isset( $post['sirius_name'] ) ? $post['sirius_name'] : '' ) );
|
||
|
|
if ( '' !== $name && ! preg_match( '/^[a-z0-9][a-z0-9.\-]{0,200}\.[a-z0-9\-]+$/', $name ) ) {
|
||
|
|
$errors[] = __( 'That does not look like a BCNR name. A name is a label and a TLD, such as example.bch.', 'sirius-press' );
|
||
|
|
} else {
|
||
|
|
update_option( SP_Settings::OPT_NAME, $name );
|
||
|
|
}
|
||
|
|
|
||
|
|
update_option( SP_Settings::OPT_NETWORK, 'chipnet' === ( isset( $post['sirius_network'] ) ? $post['sirius_network'] : '' ) ? 'chipnet' : 'mainnet' );
|
||
|
|
|
||
|
|
$gateway = esc_url_raw( trim( isset( $post['sirius_gateway'] ) ? $post['sirius_gateway'] : '' ) );
|
||
|
|
if ( '' !== $gateway && 0 !== strpos( $gateway, 'http' ) ) {
|
||
|
|
$errors[] = __( 'The gateway must be a full URL.', 'sirius-press' );
|
||
|
|
} else {
|
||
|
|
update_option( SP_Settings::OPT_GATEWAY, '' === $gateway ? SP_Settings::DEFAULT_GATEWAY : $gateway );
|
||
|
|
}
|
||
|
|
|
||
|
|
update_option(
|
||
|
|
SP_Settings::OPT_MODE,
|
||
|
|
SP_Settings::MODE_SERVER === ( isset( $post['sirius_mode'] ) ? $post['sirius_mode'] : '' )
|
||
|
|
? SP_Settings::MODE_SERVER
|
||
|
|
: SP_Settings::MODE_MANUAL
|
||
|
|
);
|
||
|
|
update_option( SP_Settings::OPT_AUTOPUB, ! empty( $post['sirius_auto_publish'] ) );
|
||
|
|
update_option( SP_Settings::OPT_OPEN_REG, ! empty( $post['sirius_open_registration'] ) );
|
||
|
|
|
||
|
|
$path = sanitize_text_field( isset( $post['sirius_path'] ) ? $post['sirius_path'] : '' );
|
||
|
|
if ( '' !== $path && ! preg_match( "#^m(/\d+'?)*$#", $path ) ) {
|
||
|
|
$errors[] = __( 'That is not a derivation path. It looks like m/44\'/145\'/0\'/0/0.', 'sirius-press' );
|
||
|
|
} else {
|
||
|
|
update_option( SP_Settings::OPT_PATH, $path );
|
||
|
|
}
|
||
|
|
|
||
|
|
$stub = strtolower( sanitize_text_field( isset( $post['sirius_stub_domain'] ) ? $post['sirius_stub_domain'] : '' ) );
|
||
|
|
if ( '' !== $stub && ! preg_match( '/\.invalid$/', $stub ) ) {
|
||
|
|
$errors[] = __( 'The placeholder mail domain must end in .invalid, so that mail to it can never be delivered by accident.', 'sirius-press' );
|
||
|
|
} else {
|
||
|
|
update_option( SP_Settings::OPT_STUB_MAIL, $stub );
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! empty( $post['sirius_forget_phrase'] ) ) {
|
||
|
|
SP_Settings::set_phrase( '' );
|
||
|
|
$notes[] = __( 'The stored recovery phrase has been deleted. Automatic publishing is off until a phrase is provided again.', 'sirius-press' );
|
||
|
|
} elseif ( ! empty( $post['sirius_phrase'] ) ) {
|
||
|
|
$problem = SP_Settings::set_phrase( (string) $post['sirius_phrase'] );
|
||
|
|
if ( '' !== $problem ) {
|
||
|
|
$errors[] = $problem;
|
||
|
|
} else {
|
||
|
|
$notes[] = sprintf(
|
||
|
|
/* translators: %s: the derived CashAddress. */
|
||
|
|
__( 'Phrase stored. It publishes from <code>%s</code> — check that this is the address that owns the name.', 'sirius-press' ),
|
||
|
|
esc_html( SP_Settings::publishing_address() )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! $errors ) {
|
||
|
|
$notes[] = __( 'Settings saved.', 'sirius-press' );
|
||
|
|
}
|
||
|
|
return array( $errors, $notes );
|
||
|
|
}
|
||
|
|
}
|