connection = $connection;
}
/**
* Hooks the notice output.
*/
public function boot(): void {
add_action( 'admin_notices', array( $this, 'render' ) );
}
/**
* Prints the current notice on the Dashboard and Plugins screens.
*/
public function render(): void {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( null === $screen || ! in_array( $screen->id, self::SCREENS, true ) || ! current_user_can( Admin::CAPABILITY ) ) {
return;
}
// One read of the connection answers every question the notice asks.
$notice = ( new self( $this->connection->pinned() ) )->current( get_current_user_id() );
if ( null === $notice ) {
return;
}
self::enqueue_script();
printf(
'
%3$s
',
esc_attr( $notice['type'] ),
esc_attr( $notice['key'] ),
esc_html( $notice['text'] )
);
if ( 'reconnect' === $notice['action']['type'] ) {
// Starting the flow is a POST with a nonce, as on the settings screen.
echo '
';
} else {
printf( '
%2$s
', esc_url( $notice['action']['url'] ), esc_html( $notice['action']['label'] ) );
}
echo '
';
}
/**
* The notice to show this user now, or null.
*
* @param int $user_id The user.
* @return array{key:string,type:string,text:string,action:array{type:string,label:string,url:string}}|null
*/
public function current( int $user_id ): ?array {
$dismissed = self::dismissed( $user_id );
foreach ( $this->candidates() as $notice ) {
if ( ! in_array( self::scoped( $notice['key'] ), $dismissed, true ) ) {
return $notice;
}
}
return null;
}
/**
* Hides a notice instance for a user.
*
* @param int $user_id The user.
* @param string $key Instance key.
*/
public static function dismiss( int $user_id, string $key ): bool {
if ( ! preg_match( self::KEY_PATTERN, $key ) ) {
return false;
}
$dismissed = self::dismissed( $user_id );
$scoped = self::scoped( $key );
if ( ! in_array( $scoped, $dismissed, true ) ) {
$dismissed[] = $scoped;
}
update_user_meta( $user_id, self::META, array_slice( $dismissed, -self::MAX_DISMISSED ) );
return true;
}
/**
* The notices that apply now, highest first.
*
* @return array
*/
public function candidates(): array {
// The connection's own notices come first: they say what to do to keep posting.
$notices = $this->connection_notices();
// 1.0.2 replaced these constants with the showfm_environment filter.
if ( defined( 'SHOWFM_API_URL' ) || defined( 'SHOWFM_APP_URL' ) ) {
$notices[] = array(
'key' => 'constants:1',
'type' => 'warning',
'text' => __( 'SHOWFM_API_URL and SHOWFM_APP_URL no longer choose the show.fm environment. To use another one, return it from the showfm_environment filter.', 'showfm' ),
'action' => array(
'type' => 'link',
'label' => __( 'How to choose an environment', 'showfm' ),
'url' => 'https://github.com/ShowDotFM/showfm-wordpress#another-showfm-environment',
),
);
}
return $notices;
}
/**
* The connection's notices, highest first.
*
* @return array
*/
private function connection_notices(): array {
if ( Connection::STATE_DISCONNECTED === $this->connection->state() || $this->connection->is_unreadable() ) {
return array();
}
$notices = array();
$reconnect = array(
'type' => 'reconnect',
'label' => __( 'Reconnect', 'showfm' ),
'url' => '',
);
$expires = (int) $this->connection->expires_at();
$expired = $expires > 0 && $expires <= time();
if ( ! $this->connection->is_connected() ) {
if ( $this->connection->issued_elsewhere() ) {
$notices[] = array(
'key' => 'environment:' . substr( md5( Api_Client::base_url() ), 0, 12 ),
'type' => 'error',
'text' => __( 'This site was connected with a different show.fm environment, so its key isn’t used here. Reconnect to post new episodes again.', 'showfm' ),
'action' => $reconnect,
);
return $notices;
}
if ( ! $expired && Connection::refused_at() > 0 ) {
$notices[] = array(
'key' => 'refused:' . Connection::refused_at(),
'type' => 'error',
'text' => __( 'show.fm disconnected this site, for example because the account password changed.', 'showfm' ),
'action' => $reconnect,
);
}
return $notices;
}
$paused = $this->connection->paused_since();
if ( $paused > 0 ) {
$notices[] = array(
'key' => 'paused:' . $paused,
'type' => 'warning',
'text' => __( 'Auto-posting is paused. The show’s plan doesn’t include connected sites, so new episodes aren’t posted here.', 'showfm' ),
'action' => array(
'type' => 'link',
'label' => __( 'Check your plan', 'showfm' ),
'url' => self::plan_url(),
),
);
}
$problem = Health::sync_problem( $this->connection );
if ( '' !== $problem ) {
$texts = array(
'row_post_type' => __( 'New episodes aren’t being posted here because the chosen post type isn’t available.', 'showfm' ),
'row_author' => __( 'New episodes aren’t being posted here because the chosen author can’t publish posts.', 'showfm' ),
'invalid_feed' => __( 'New episodes aren’t being posted here because show.fm sent changes the plugin couldn’t read. It keeps trying.', 'showfm' ),
);
$notices[] = array(
'key' => 'sync:' . $problem . ':' . $this->connection->site_id(),
'type' => 'warning',
'text' => $texts[ $problem ],
'action' => 'invalid_feed' === $problem ? array(
'type' => 'link',
'label' => __( 'See what to fix', 'showfm' ),
'url' => admin_url( 'site-health.php' ),
) : array(
'type' => 'link',
'label' => __( 'Check the Publishing settings', 'showfm' ),
'url' => add_query_arg( 'tab', 'publishing', Connect::settings_url() ),
),
);
}
$days = self::days_left( $expires );
if ( null !== $days && $days <= 30 ) {
$notices[] = array(
'key' => ( $days <= 7 ? 'expiry7:' : 'expiry30:' ) . $expires,
'type' => 'warning',
/* translators: %d: number of days. */
'text' => sprintf( _n( 'Your show.fm connection expires in %d day.', 'Your show.fm connection expires in %d days.', $days, 'showfm' ), $days ),
'action' => $reconnect,
);
}
return $notices;
}
/**
* Whole days until the key expires, rounded up, or null when it never expires or has
* already expired.
*
* @param int $expires Expiry as a Unix timestamp, or 0.
*/
public static function days_left( int $expires ): ?int {
if ( $expires <= time() ) {
return null;
}
return (int) ceil( ( $expires - time() ) / DAY_IN_SECONDS );
}
/**
* Where to check or change a show's plan.
*/
public static function plan_url(): string {
return Connect::app_url() . '/pricing';
}
/**
* Loads the dismissal script.
*/
public static function enqueue_script(): void {
$asset = SHOWFM_DIR . '/build/notices.asset.php';
if ( ! is_readable( $asset ) ) {
return;
}
$asset = require $asset;
wp_enqueue_script( self::HANDLE, plugins_url( 'build/notices.js', SHOWFM_FILE ), $asset['dependencies'], $asset['version'], true );
}
/**
* The user's dismissed keys.
*
* @param int $user_id The user.
* @return string[]
*/
private static function dismissed( int $user_id ): array {
$keys = get_user_meta( $user_id, self::META, true );
return is_array( $keys ) ? array_values( array_filter( $keys, 'is_string' ) ) : array();
}
/**
* A key scoped to this site, since user meta is shared across a network.
*
* @param string $key Instance key.
*/
private static function scoped( string $key ): string {
return get_current_blog_id() . '|' . $key;
}
}