' . esc_html( $message ) . '

' : ''; } private static function missing_slug( string $shortcode ): string { return self::admin_notice( sprintf( /* translators: %s: the shortcode, e.g. [vimine_enroll] */ __( '%s: the association’s slug is missing. Add slug="…" to the shortcode, or fill in “Association slug” in the block.', 'vimine' ), $shortcode ) ); } /** @param array|string $atts */ public static function signin( $atts ): string { wp_enqueue_style( 'vimine' ); $a = shortcode_atts( [ 'label' => '', 'label_signed_in' => '', 'return_to' => '', 'scope' => '' ], is_array( $atts ) ? $atts : [], 'vimine_signin' ); if ( ! Vimine_Settings::configured() ) { return self::admin_notice( __( 'Vimine: set up the client in Settings → Vimine.', 'vimine' ) ); } // Empty — `return_to=""`, or the block's field left blank — is this page, as when the attribute is missing. $return_to = '' !== $a['return_to'] ? $a['return_to'] : self::current_path(); if ( Vimine_Session::is_signed_in() ) { Vimine_Session::personal(); $label = '' !== $a['label_signed_in'] ? $a['label_signed_in'] : __( 'Sign out', 'vimine' ); return '' . esc_html( $label ) . ''; } $label = '' !== $a['label'] ? $a['label'] : __( 'Sign in with Vimine', 'vimine' ); return '' . esc_html( $label ) . ''; } /** @param array|string $atts */ public static function card( $atts ): string { wp_enqueue_style( 'vimine' ); $a = shortcode_atts( [ 'signin' => '1' ], is_array( $atts ) ? $atts : [], 'vimine_card' ); if ( ! Vimine_Settings::configured() ) { return ''; } $token = Vimine_Session::access_token(); if ( null === $token ) { return '

' . esc_html__( 'Sign in with Vimine to see your membership card.', 'vimine' ) . '

' . ( '1' === $a['signin'] ? self::signin( [] ) : '' ) . '
'; } Vimine_Session::personal(); $client = new Vimine_Client(); $svg = $client->request( 'GET', '/v1/me/membership/card.svg', $token, null, 'image/svg+xml' ); if ( is_wp_error( $svg ) ) { $code = $svg->get_error_code(); // `not_active`: the membership exists but has no card yet (request pending, suspended, not admitted) — a // state to explain, not a failure. if ( 'not_found' === $code ) { $msg = __( 'No membership in this association.', 'vimine' ); } elseif ( 'not_active' === $code ) { $msg = __( 'Your membership is not active yet: the card appears once it is.', 'vimine' ); } else { $msg = __( 'The membership card is not available at the moment.', 'vimine' ); } return '

' . esc_html( $msg ) . '

'; } // The SVG is produced by Vimine for THIS member: rendered inline, restricted to the elements the card uses. return '
' . wp_kses( (string) $svg, self::svg_kses() ) . '
'; } /** * `[vimine_enroll slug="…" height="900" lang="en"]` — the hosted enrolment form, in the page's language (`lang`, * or the site's) and in the site's look: `scheme`, `accent`, `background` and `font`, else Settings → Vimine, else * the theme (Vimine_Appearance). Someone who already has a Vimine account is offered *Continue on Vimine*, which * opens the hosted page in a new tab where they join in one step: `allow-popups-to-escape-sandbox` lets that tab * open as an ordinary page, not as one more sandboxed frame. * * @param array|string $atts */ public static function enroll( $atts ): string { wp_enqueue_style( 'vimine' ); $a = shortcode_atts( [ 'slug' => '', 'height' => '900', 'lang' => '', 'scheme' => '', 'accent' => '', 'background' => '', 'font' => '' ], is_array( $atts ) ? $atts : [], 'vimine_enroll' ); if ( '' === $a['slug'] ) { return self::missing_slug( '[vimine_enroll]' ); } // The site's language as a tag (zh_CN → zh-CN): the hosted page serves the platform's language for it — Chinese by // script, Portuguese as Brazilian — or its default when Vimine does not speak it. $lang = preg_replace( '/[^A-Za-z-]/', '', str_replace( '_', '-', '' !== trim( (string) $a['lang'] ) ? trim( (string) $a['lang'] ) : determine_locale() ) ); $lang = '' !== $lang ? $lang : 'it'; $query = array_merge( [ 'locale' => $lang ], Vimine_Appearance::query( $a ) ); $src = Vimine_Settings::get()['app_url'] . '/enroll/' . rawurlencode( sanitize_title( $a['slug'] ) ) . '?' . http_build_query( $query, '', '&', PHP_QUERY_RFC3986 ); return ''; } /** * `[vimine_events slug="…" filter="upcoming" limit="10"]` — the association's events: the public ones for * a visitor, every published one (members-only included) for a signed-in member. Read-only: each event links * to its page on Vimine (/agenda//), where the registration happens on the member's session. * * @param array|string $atts */ public static function events( $atts ): string { wp_enqueue_style( 'vimine' ); $a = shortcode_atts( [ 'slug' => '', 'filter' => 'upcoming', 'limit' => '10' ], is_array( $atts ) ? $atts : [], 'vimine_events' ); if ( ! Vimine_Settings::configured() ) { return ''; } $slug = sanitize_title( $a['slug'] ); if ( '' === $slug ) { return self::missing_slug( '[vimine_events]' ); } $filter = in_array( $a['filter'], [ 'upcoming', 'past', 'cancelled', 'all' ], true ) ? $a['filter'] : 'upcoming'; $query = '?' . http_build_query( [ 'filter' => $filter, 'limit' => max( 1, min( 50, (int) $a['limit'] ) ) ] ); $client = new Vimine_Client(); $token = Vimine_Session::access_token(); if ( null !== $token ) { Vimine_Session::personal(); // a member's list holds the members-only events } $data = null !== $token ? $client->request( 'GET', '/v1/events' . $query, $token ) : $client->public_get( '/v1/aps/' . rawurlencode( $slug ) . '/events' . $query ); if ( is_wp_error( $data ) && null !== $token ) { $data = $client->public_get( '/v1/aps/' . rawurlencode( $slug ) . '/events' . $query ); // a token without events:read: the public ones } if ( is_wp_error( $data ) ) { return '

' . esc_html__( 'Events are not available at the moment.', 'vimine' ) . '

'; } $items = isset( $data['items'] ) && is_array( $data['items'] ) ? $data['items'] : []; if ( ! $items ) { return '

' . esc_html__( 'No upcoming events.', 'vimine' ) . '

'; } $app = Vimine_Settings::get()['app_url']; $html = '
'; foreach ( $items as $ev ) { if ( ! is_array( $ev ) || empty( $ev['slug'] ) ) { continue; } $url = $app . '/agenda/' . rawurlencode( $slug ) . '/' . rawurlencode( (string) $ev['slug'] ); $when = ! empty( $ev['starts_at'] ) ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( (string) $ev['starts_at'] ) ) : ''; $place = is_array( $ev['location'] ?? null ) ? ( $ev['location']['name'] ?? $ev['location']['address'] ?? '' ) : ''; $html .= '
'; if ( ! empty( $ev['cover_url'] ) ) { $html .= ''; } $html .= '

' . esc_html( (string) ( $ev['title'] ?? '' ) ) . '

'; $html .= '

' . esc_html( trim( $when . ( $place ? ' · ' . $place : '' ) ) ) . '

'; if ( ! empty( $ev['summary'] ) ) { $html .= '

' . esc_html( (string) $ev['summary'] ) . '

'; } foreach ( is_array( $ev['registration_types'] ?? null ) ? $ev['registration_types'] : [] as $tp ) { if ( ! is_array( $tp ) ) { continue; } $amount = (float) ( $tp['amount'] ?? 0 ); $price = $amount > 0 ? number_format_i18n( $amount, 2 ) . ' ' . (string) ( $tp['currency'] ?? 'EUR' ) : _x( 'free', 'the price of a registration type', 'vimine' ); $left = ''; if ( isset( $tp['remaining'] ) ) { $n = (int) $tp['remaining']; $left = sprintf( /* translators: %d: how many places are left */ _n( '%d place left', '%d places left', $n, 'vimine' ), $n ); } $html .= '
' . esc_html( (string) ( $tp['name'] ?? '' ) . ' · ' . $price ) . '' . esc_html( $left ) . '
'; } if ( ( $ev['status'] ?? '' ) === 'published' ) { $html .= '' . esc_html__( 'Register', 'vimine' ) . ''; } $html .= '
'; } return $html . '
'; } /** * What the card image is made of, and nothing else: the surface, the texts, the rules between the facts (`line`), * the code's modules, and the Vimine mark at the foot — a stroked `path` and two `circle`s inside a transformed * group, wrapped in a link to the public site when the deployment names one (`a`, whose `href` WordPress restricts * to its allowed protocols). No script, no style, no image, no foreign content. * * @return array> */ private static function svg_kses(): array { $attrs = [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'fill' => true, 'stroke' => true, 'opacity' => true, 'font-family' => true, 'font-size' => true, 'font-weight' => true, 'text-anchor' => true, 'clip-path' => true, 'id' => true, 'role' => true, 'aria-label' => true, 'viewbox' => true, 'xmlns' => true ]; $stroke = [ 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true ]; return [ 'svg' => $attrs, 'g' => $attrs + [ 'transform' => true ], 'defs' => $attrs, 'clippath' => $attrs, 'rect' => $attrs, 'text' => $attrs, 'line' => [ 'x1' => true, 'x2' => true, 'y1' => true, 'y2' => true, 'stroke' => true ] + $stroke, 'path' => [ 'd' => true, 'fill' => true, 'stroke' => true ] + $stroke, 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true ], 'a' => [ 'href' => true, 'target' => true, 'rel' => true ], 'title' => [], ]; } }