'', 'email_only' => '', 'title' => '' ), (array) $atts, 'dashbread_login' );
self::style();
$to = '' !== $a['redirect'] ? self::target( (string) $a['redirect'] ) : Settings::app_url();
if ( is_user_logged_in() ) {
$u = wp_get_current_user();
return '
';
}
$form = wp_login_form(
array(
'echo' => false,
'redirect' => $to,
'form_id' => 'dashbread-login-' . wp_rand( 100, 999 ),
'label_username' => '1' === (string) $a['email_only'] ? __( 'Email address', 'dashbread' ) : __( 'Email or username', 'dashbread' ),
'label_log_in' => __( 'Sign in', 'dashbread' ),
'remember' => true,
)
);
// The Dashbread nonce lets a failed sign-in come back to the app's login screen with the redirect kept.
$form = str_replace( '', \Dashbread\Auth\Auth::nonce_field( 'login' ) . '', $form );
$links = '' . esc_html__( 'Forgot your password?', 'dashbread' ) . '';
if ( get_option( 'users_can_register' ) ) {
$links .= ' · ' . esc_html__( 'Create an account', 'dashbread' ) . '';
}
$links .= '
';
$title = '' !== $a['title'] ? '' . esc_html( (string) $a['title'] ) . '
' : '';
return '' . $title . self::login_form_kses( $form ) . $links . '
';
}
/** [dashbread_app_link label="Open the app" page="profile" label_out="Sign in" button="1"] */
public function app_link( $atts ): string {
$a = shortcode_atts( array( 'label' => __( 'Open the app', 'dashbread' ), 'page' => '', 'label_out' => __( 'Sign in', 'dashbread' ), 'button' => '1' ), (array) $atts, 'dashbread_app_link' );
$to = Settings::app_url( sanitize_text_field( (string) $a['page'] ) );
$cls = '1' === (string) $a['button'] ? 'sui-sc-btn' : 'sui-sc-link';
if ( '1' === (string) $a['button'] ) {
self::style();
}
if ( ! is_user_logged_in() ) {
return '' . esc_html( (string) $a['label_out'] ) . '';
}
return '' . esc_html( (string) $a['label'] ) . '';
}
/** [dashbread_logged_in]…[/dashbread_logged_in] */
public function logged_in( $atts, $content = '' ): string {
return is_user_logged_in() ? self::enclosed( $content ) : '';
}
/** [dashbread_logged_out]…[/dashbread_logged_out] */
public function logged_out( $atts, $content = '' ): string {
return is_user_logged_in() ? '' : self::enclosed( $content );
}
/**
* [dashbread_protect roles="subscriber,editor" message=""]…[/dashbread_protect] — a members-only gate:
* signed-out visitors get a sign-in prompt; signed-in people outside the roles get the message.
*/
public function protect( $atts, $content = '' ): string {
$a = shortcode_atts( array( 'roles' => '', 'message' => '' ), (array) $atts, 'dashbread_protect' );
if ( is_user_logged_in() && self::role_match( (string) $a['roles'] ) ) {
return self::enclosed( $content );
}
self::style();
$here = get_permalink() ?: home_url( '/' );
if ( ! is_user_logged_in() ) {
$msg = '' !== $a['message'] ? (string) $a['message'] : __( 'This content is for members. Sign in to see it.', 'dashbread' );
return '';
}
$msg = '' !== $a['message'] ? (string) $a['message'] : __( 'Your account does not have access to this content.', 'dashbread' );
return '';
}
/** [dashbread_role is="editor,author" not="subscriber"]…[/dashbread_role] — shows nothing to anyone else. */
public function role( $atts, $content = '' ): string {
$a = shortcode_atts( array( 'is' => '', 'not' => '' ), (array) $atts, 'dashbread_role' );
if ( ! is_user_logged_in() ) {
return '';
}
if ( '' !== $a['is'] && ! self::role_match( (string) $a['is'] ) ) {
return '';
}
if ( '' !== $a['not'] && self::role_match( (string) $a['not'] ) ) {
return '';
}
return self::enclosed( $content );
}
/** [dashbread_show from="2026-10-01" until="2026-12-31 17:00"]…[/dashbread_show] (alias [dashbread_expire]) — site time zone. */
public function show( $atts, $content = '' ): string {
$a = shortcode_atts( array( 'from' => '', 'until' => '' ), (array) $atts, 'dashbread_show' );
$now = time();
$tz = wp_timezone();
try {
if ( '' !== $a['from'] && $now < ( new \DateTimeImmutable( (string) $a['from'], $tz ) )->getTimestamp() ) {
return '';
}
if ( '' !== $a['until'] ) {
$until = new \DateTimeImmutable( (string) $a['until'], $tz );
if ( ! preg_match( '/\d{1,2}:\d{2}/', (string) $a['until'] ) ) {
$until = $until->setTime( 23, 59, 59 ); // A bare date runs through the end of that day.
}
if ( $now > $until->getTimestamp() ) {
return '';
}
}
} catch ( \Exception $e ) {
return current_user_can( 'edit_posts' ) ? '' : '';
}
return self::enclosed( $content );
}
/** [dashbread_user field="display_name|first_name|last_name|email|username" fallback=""] — the signed-in person's own details. */
public function user( $atts ): string {
$a = shortcode_atts( array( 'field' => 'display_name', 'fallback' => '' ), (array) $atts, 'dashbread_user' );
if ( ! is_user_logged_in() ) {
return esc_html( (string) $a['fallback'] );
}
$u = wp_get_current_user();
$map = array(
'display_name' => $u->display_name,
'first_name' => $u->first_name,
'last_name' => $u->last_name,
'email' => $u->user_email,
'username' => $u->user_login,
);
$val = (string) ( $map[ sanitize_key( (string) $a['field'] ) ] ?? '' );
return esc_html( '' !== $val ? $val : (string) $a['fallback'] );
}
/* --------------------------------------------------------------- helpers */
/**
* Content between an enclosing tag's open and close: limited to post-safe HTML first, then any
* nested shortcodes run (each escapes its own output).
*
* @param mixed $content
*/
private static function enclosed( $content ): string {
return do_shortcode( wp_kses_post( (string) $content ) );
}
/** wp_login_form() markup, escaped at output with the form elements it uses allowed. */
private static function login_form_kses( string $html ): string {
$attrs = array( 'id' => true, 'class' => true, 'name' => true, 'type' => true, 'value' => true, 'for' => true, 'action' => true, 'method' => true, 'size' => true, 'autocomplete' => true, 'required' => true, 'checked' => true, 'placeholder' => true, 'aria-describedby' => true, 'spellcheck' => true, 'data-*' => true, 'aria-label' => true );
return wp_kses( $html, array( 'form' => $attrs, 'p' => $attrs, 'label' => $attrs, 'input' => $attrs, 'button' => $attrs, 'span' => $attrs, 'div' => $attrs, 'a' => $attrs + array( 'href' => true ) ) );
}
/** Does the current user hold one of these roles? Empty list = any signed-in user. */
public static function role_match( string $roles ): bool {
$want = array_filter( array_map( 'sanitize_key', explode( ',', $roles ) ) );
if ( ! $want ) {
return is_user_logged_in();
}
return (bool) array_intersect( $want, (array) wp_get_current_user()->roles );
}
/** A redirect attribute: "profile" or "/app/profile" stays in the app; a full same-site URL is kept; anything else → the app. */
private static function target( string $to ): string {
if ( preg_match( '#^https?://#i', $to ) ) {
return wp_validate_redirect( $to, Settings::app_url() );
}
$to = trim( $to, '/' );
$r = Settings::route();
if ( $to === $r || str_starts_with( $to, $r . '/' ) ) {
$to = substr( $to, strlen( $r ) );
}
return Settings::app_url( $to );
}
/**
* For tags that render app components (islands): inside the app they render; on a theme page a
* signed-in member gets a link to open it in the app, and visitors get nothing.
*/
public static function app_only( string $label, string $page = '' ): string {
if ( Router::is_app() ) {
return '';
}
if ( ! is_user_logged_in() ) {
return '';
}
self::style();
return '' . esc_html( sprintf( /* translators: %s: feature name */ __( 'Open %s in the app →', 'dashbread' ), $label ) ) . '
';
}
/* ------------------------------------------------------------- reference */
/**
* Every Dashbread shortcode, grouped for the Help screen. Items: code, close (enclosing tags), note,
* guide (knowledge-base slug the tag belongs to), feature (null = Core), available, requires.
*
* @return array>
*/
public static function reference(): array {
$core = array(
array( 'code' => '[dashbread_login]', 'note' => __( 'The sign-in form on any page. Signed-in people see "Open the app" and "Sign out" instead. Optional redirect="profile" (an app page or a same-site URL), email_only="1", title="".', 'dashbread' ), 'guide' => 'sign-in-and-registration' ),
array( 'code' => '[dashbread_app_link]', 'note' => __( 'A button into the app — or a Sign in button for visitors, returning them to the same place. Optional label="", label_out="", page="profile", button="0" for a plain link.', 'dashbread' ), 'guide' => 'the-app-route' ),
array( 'code' => '[dashbread_logged_in]', 'close' => '[/dashbread_logged_in]', 'note' => __( 'Show the enclosed content only to signed-in people.', 'dashbread' ), 'guide' => 'shortcodes' ),
array( 'code' => '[dashbread_logged_out]', 'close' => '[/dashbread_logged_out]', 'note' => __( 'Show the enclosed content only to signed-out visitors.', 'dashbread' ), 'guide' => 'shortcodes' ),
array( 'code' => '[dashbread_protect]', 'close' => '[/dashbread_protect]', 'note' => __( 'Members-only gate: visitors get a sign-in prompt instead of the content. Optional roles="subscriber,editor" and message="".', 'dashbread' ), 'guide' => 'shortcodes' ),
array( 'code' => '[dashbread_role is="editor"]', 'close' => '[/dashbread_role]', 'note' => __( 'Show the enclosed content only to these roles (comma-separated). not="" hides it from roles instead. Everyone else sees nothing.', 'dashbread' ), 'guide' => 'shortcodes' ),
array( 'code' => '[dashbread_show from="2026-10-01" until="2026-12-31"]', 'close' => '[/dashbread_show]', 'note' => __( 'Show content inside a date window (site time zone; a bare date runs to the end of that day). Alias [dashbread_expire].', 'dashbread' ), 'guide' => 'shortcodes' ),
array( 'code' => '[dashbread_user field="first_name"]', 'note' => __( 'The signed-in person\'s own display_name, first_name, last_name, email or username. fallback="" for visitors.', 'dashbread' ), 'guide' => 'profile' ),
);
$pro = array(
array( 'code' => '[dashbread_has_plan plan="gold"]', 'close' => '[/dashbread_has_plan]', 'note' => __( 'Show content only to members with an active membership on these plans (slugs or ids, comma-separated). Leave plan="" for any plan.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ),
array( 'code' => '[dashbread_no_plan plan="gold"]', 'close' => '[/dashbread_no_plan]', 'note' => __( 'The opposite: everyone without that plan, including signed-out visitors — good for "Upgrade" prompts.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ),
array( 'code' => '[dashbread_plans]', 'note' => __( 'Plan cards (price, features, Get button) for a pricing page. Optional plans="gold,silver" and columns="3". Members see "Your plan" on the one they have.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ),
array( 'code' => '[dashbread_field key="company_size"]', 'note' => __( 'The signed-in member\'s own answer to a custom field (choices show their labels, name/address fields join their parts). fallback="".', 'dashbread' ), 'guide' => 'field_builder', 'feature' => 'field_builder' ),
array( 'code' => '[dashbread_company field="name"]', 'note' => __( 'The signed-in member\'s primary company: name, city, state, website, phone or email. fallback="".', 'dashbread' ), 'guide' => 'companies', 'feature' => 'companies' ),
);
$pmas = array(
array( 'code' => '[dashbread_graph source="members_joined"]', 'note' => __( 'Data & Graphs — a chart inside a hosted page. Optional kind="stat|line|bar|pie", range="7|30|90|365", ranges="1", title="". Site sources show only to staff; "My …" sources to each member.', 'dashbread' ), 'guide' => 'data-graphs', 'feature' => 'data_graphs' ),
array( 'code' => '[dashbread_tasks]', 'note' => __( 'Tasks — the member\'s open tasks with checkboxes, inside a hosted page. Optional limit="5", add="1" to let them add their own, title="".', 'dashbread' ), 'guide' => 'tasks', 'feature' => 'tasks' ),
array( 'code' => '[dashbread_offers]', 'note' => __( 'Offers — the live offers aimed at the signed-in member, as cards, inside a hosted page (every offer, whatever its dashboard or flyout setting). Optional limit="3" (up to 6), title="".', 'dashbread' ), 'guide' => 'offers', 'feature' => 'offers' ),
);
$pro_name = Brand::name() . ' Pro';
$labels = array( 'plans' => $pro_name, 'field_builder' => $pro_name, 'companies' => $pro_name, 'data_graphs' => 'Data & Graphs', 'tasks' => 'Tasks', 'offers' => 'Offers' );
$sections = array(
array( 'key' => 'core', 'label' => __( 'Core (free)', 'dashbread' ), 'note' => __( 'Ship with Dashbread — work on theme pages and on hosted pages inside the app.', 'dashbread' ), 'items' => $core ),
array( 'key' => 'pro', 'label' => __( 'Pro', 'dashbread' ), 'note' => __( 'Unlock with a Dashbread Pro license and read from the Pro modules.', 'dashbread' ), 'items' => $pro ),
array( 'key' => 'pmas', 'label' => __( 'Pro Module Add-ons', 'dashbread' ), 'note' => __( 'Each unlocks with its add-on. They render app components, so place them on a hosted page (on a theme page, members get a link into the app).', 'dashbread' ), 'items' => $pmas ),
);
/**
* Add a section (or items) to the Help screen's shortcode reference.
*
* @param array> $sections {key,label,note,items[{code,close?,note,guide?,feature?}]}
*/
$sections = (array) apply_filters( 'dashbread_shortcode_reference', $sections );
foreach ( $sections as &$s ) {
foreach ( $s['items'] as &$i ) {
$f = $i['feature'] ?? null;
$i['close'] = $i['close'] ?? '';
$i['guide'] = $i['guide'] ?? '';
$i['available'] = null === $f || Features::has( (string) $f );
$i['requires'] = null === $f ? '' : ( $labels[ $f ] ?? '' );
unset( $i['feature'] );
}
unset( $i );
}
unset( $s );
return array_values( $sections );
}
}