'render_form', 'fields/title' => 'render_title', 'fields/content' => 'render_content', 'fields/taxonomy' => 'render_taxonomy', 'fields/tags' => 'render_tags', 'fields/image' => 'render_image', 'fields/submit' => 'render_submit', 'fields/credit-notice' => 'render_credit_notice', 'dashboard' => 'render_dashboard', 'dashboard-parts/my-plans' => 'render_my_plans', 'dashboard-parts/my-posts' => 'render_my_posts', 'dashboard-parts/new-post' => 'render_new_post', 'plans' => 'render_plans', 'plan/name' => 'render_plan_name', 'plan/price' => 'render_plan_price', 'plan/description' => 'render_plan_description', 'plan/features' => 'render_plan_features', 'plan/buy' => 'render_plan_buy', ); /** * Blocks that decide for themselves whether and how often their inner * blocks are rendered. */ const RENDERS_INNER_BLOCKS = array( 'dashboard', 'plans' ); /** * The post being edited, null when there is none, false until looked up. * * @var \WP_Post|null|false */ private static $editing = false; public static function init() { add_action( 'init', array( __CLASS__, 'register' ) ); add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'editor_data' ) ); } public static function register() { wp_register_style( 'postfee', POSTFEE_URL . 'assets/css/postfee.css', array(), POSTFEE_VERSION ); wp_register_script( 'postfee-my-posts', POSTFEE_URL . 'assets/js/my-posts.js', array(), POSTFEE_VERSION, array( 'strategy' => 'defer' ) ); foreach ( self::BLOCKS as $dir => $method ) { if ( file_exists( POSTFEE_DIR . 'build/' . $dir . '/block.json' ) ) { register_block_type( POSTFEE_DIR . 'build/' . $dir, array( 'render_callback' => array( __CLASS__, $method ), 'skip_inner_blocks' => in_array( $dir, self::RENDERS_INNER_BLOCKS, true ), ) ); } } } /** * Tells the editor which content can be submitted and which fields apply to it. */ public static function editor_data() { $post_types = array(); foreach ( postfee_post_types() as $post_type ) { $taxonomies = array(); foreach ( get_object_taxonomies( $post_type, 'objects' ) as $taxonomy ) { if ( $taxonomy->public && $taxonomy->hierarchical ) { $taxonomies[] = array( 'value' => $taxonomy->name, 'label' => $taxonomy->labels->singular_name, ); } } $post_types[] = array( 'value' => $post_type, 'label' => get_post_type_object( $post_type )->labels->name, 'taxonomies' => $taxonomies, 'tags' => is_object_in_taxonomy( $post_type, 'post_tag' ), 'image' => post_type_supports( $post_type, 'thumbnail' ), ); } wp_add_inline_script( 'wp-blocks', 'window.postfeeBlocks = ' . wp_json_encode( array( 'postTypes' => $post_types ) ) . ';', 'before' ); } /** * Markup of a block holding its default inner blocks. */ private static function markup( $name, $inner ) { $content = ''; foreach ( $inner as $inner_name ) { $content .= get_comment_delimited_block_content( 'postfee/' . $inner_name, array(), '' ) . "\n"; } return get_comment_delimited_block_content( 'postfee/' . $name, array(), "\n" . $content ); } /** * Markup of a form with every field, as inserted on the submit page. */ public static function default_form() { return self::markup( 'form', array( 'credit-notice', 'field-title', 'field-content', 'field-taxonomy', 'field-tags', 'field-image', 'submit' ) ); } /** * Markup of a dashboard with every section, as inserted on the "My Posts" page. */ public static function default_dashboard() { return self::markup( 'dashboard', array( 'my-plans', 'my-posts', 'new-post-button' ) ); } private static function editing() { if ( false === self::$editing ) { self::$editing = empty( $_GET['postfee_edit'] ) ? null : Checkout::editable_post( absint( $_GET['postfee_edit'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. } return self::$editing; } /** * What to show in place of the form, or '' when the fields are shown. */ private static function view( $post_type ) { if ( ! is_user_logged_in() ) { return 'login'; } if ( ! empty( $_GET['postfee_pay'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. return 'pay'; } if ( ! empty( $_GET['postfee_edit'] ) && ! self::editing() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. return 'no_edit'; } if ( ! in_array( self::editing() ? self::editing()->post_type : $post_type, postfee_post_types(), true ) ) { return 'no_type'; } return ''; } /** * The post type a field is being shown for, or '' when fields are not shown. */ private static function field_post_type( $block ) { $post_type = isset( $block->context['postfee/postType'] ) ? sanitize_key( $block->context['postfee/postType'] ) : 'post'; if ( '' !== self::view( $post_type ) ) { return ''; } return self::editing() ? self::editing()->post_type : $post_type; } /** * After a failed submit, what the user typed rather than the saved post. * Only put back into the form it came from, and sanitized by each field. */ private static function posted( $key ) { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $posted = isset( $_POST['postfee_action'] ) && 'submit_post' === $_POST['postfee_action']; return $posted && isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : null; // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } /** * Wraps a control in the block wrapper with its label and help text. * $control must already be escaped. */ private static function field( $attributes, $id, $default_label, $control ) { $label = isset( $attributes['label'] ) && '' !== $attributes['label'] ? $attributes['label'] : $default_label; $help = empty( $attributes['help'] ) ? '' : '

' . esc_html( $attributes['help'] ) . '

'; return sprintf( '
%s%s
', get_block_wrapper_attributes( array( 'class' => 'postfee-field' ) ), esc_attr( $id ), wp_kses_post( $label ), $control, $help ); } public static function render_form( $attributes, $content ) { $post_type = sanitize_key( $attributes['postType'] ); $view = self::view( $post_type ); $post = self::editing(); ob_start(); if ( '' === $view ) { printf( '
', get_block_wrapper_attributes( array( 'class' => 'postfee postfee-form' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by core. Frontend::notices(); wp_nonce_field( 'postfee_submit_post', '_postfee_nonce' ); echo ''; printf( '', esc_attr( $post ? $post->post_type : $post_type ) ); printf( '', absint( $post ? $post->ID : 0 ) ); echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Rendered blocks. echo '
'; return ob_get_clean(); } printf( '
', get_block_wrapper_attributes( array( 'class' => 'postfee' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by core. Frontend::notices(); if ( 'login' === $view ) { Frontend::login_prompt(); } elseif ( 'pay' === $view ) { Frontend::pay_step( Checkout::payable_post( isset( $_GET['postfee_pay'] ) ? absint( $_GET['postfee_pay'] ) : 0 ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. } elseif ( 'no_edit' === $view ) { echo '

' . esc_html__( 'You cannot edit this post.', 'postfee' ) . '

'; } else { echo '

' . esc_html__( 'This type of content cannot be submitted.', 'postfee' ) . '

'; } echo '
'; return ob_get_clean(); } public static function render_title( $attributes, $content, $block ) { if ( ! self::field_post_type( $block ) ) { return ''; } $post = self::editing(); $title = self::posted( 'postfee_title' ); $title = null !== $title ? sanitize_text_field( $title ) : ( $post ? $post->post_title : '' ); $id = wp_unique_id( 'postfee_title_' ); return self::field( $attributes, $id, __( 'Title', 'postfee' ), sprintf( '', esc_attr( $id ), esc_attr( $title ), esc_attr( $attributes['placeholder'] ) ) ); } public static function render_content( $attributes, $content, $block ) { if ( ! self::field_post_type( $block ) ) { return ''; } $post = self::editing(); $value = self::posted( 'postfee_content' ); $value = null !== $value ? wp_kses_post( $value ) : ( $post ? $post->post_content : '' ); $id = wp_unique_id( 'postfee_content_' ); $rows = max( 3, min( 30, (int) $attributes['rows'] ) ); if ( $attributes['richText'] ) { ob_start(); wp_editor( $value, $id, array( 'textarea_name' => 'postfee_content', 'media_buttons' => false, 'teeny' => true, 'textarea_rows' => $rows, 'quicktags' => false, ) ); $control = ob_get_clean(); } else { $control = sprintf( '', esc_attr( $id ), $rows, esc_textarea( $value ) ); } return self::field( $attributes, $id, __( 'Content', 'postfee' ), $control ); } public static function render_taxonomy( $attributes, $content, $block ) { $post_type = self::field_post_type( $block ); if ( ! $post_type ) { return ''; } $taxonomy = sanitize_key( $attributes['taxonomy'] ); $taxonomies = get_object_taxonomies( $post_type, 'objects' ); if ( ! isset( $taxonomies[ $taxonomy ] ) || ! $taxonomies[ $taxonomy ]->public || ! $taxonomies[ $taxonomy ]->hierarchical ) { return ''; } $post = self::editing(); $term_ids = $post ? wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) ) : array(); $id = wp_unique_id( 'postfee_term_' ); return self::field( $attributes, $id, $taxonomies[ $taxonomy ]->labels->singular_name, wp_dropdown_categories( array( 'taxonomy' => $taxonomy, 'name' => 'postfee_terms[' . $taxonomy . ']', 'id' => $id, 'hide_empty' => false, 'hierarchical' => true, 'selected' => $term_ids && ! is_wp_error( $term_ids ) ? (int) $term_ids[0] : 0, 'show_option_none' => '' !== $attributes['placeholder'] ? $attributes['placeholder'] : __( 'Select…', 'postfee' ), 'option_none_value' => 0, 'echo' => 0, ) ) ); } public static function render_tags( $attributes, $content, $block ) { $post_type = self::field_post_type( $block ); if ( ! $post_type || ! is_object_in_taxonomy( $post_type, 'post_tag' ) ) { return ''; } $post = self::editing(); $tags = $post ? implode( ', ', wp_get_object_terms( $post->ID, 'post_tag', array( 'fields' => 'names' ) ) ) : ''; $id = wp_unique_id( 'postfee_tags_' ); return self::field( $attributes, $id, __( 'Tags', 'postfee' ), sprintf( '', esc_attr( $id ), esc_attr( $tags ), esc_attr( '' !== $attributes['placeholder'] ? $attributes['placeholder'] : __( 'Separate tags with commas', 'postfee' ) ) ) ); } public static function render_image( $attributes, $content, $block ) { $post_type = self::field_post_type( $block ); if ( ! $post_type || ! post_type_supports( $post_type, 'thumbnail' ) ) { return ''; } $post = self::editing(); $id = wp_unique_id( 'postfee_image_' ); $control = $post && has_post_thumbnail( $post ) ? get_the_post_thumbnail( $post, 'thumbnail', array( 'class' => 'postfee-current-image' ) ) : ''; return self::field( $attributes, $id, __( 'Featured image', 'postfee' ), $control . sprintf( '', esc_attr( $id ) ) ); } public static function render_submit( $attributes, $content, $block ) { if ( ! self::field_post_type( $block ) ) { return ''; } if ( self::editing() ) { $text = '' !== $attributes['editText'] ? esc_html( $attributes['editText'] ) : esc_html__( 'Save changes', 'postfee' ); } else { $text = '' !== $attributes['text'] ? wp_kses_post( $attributes['text'] ) : esc_html__( 'Continue', 'postfee' ); } return sprintf( '', get_block_wrapper_attributes( array( 'class' => 'postfee-button' ) ), $text ); } public static function render_credit_notice( $attributes, $content, $block ) { $post_type = self::field_post_type( $block ); if ( ! $post_type || self::editing() ) { return ''; } $entitlement = Entitlements::find_usable( get_current_user_id(), $post_type ); if ( ! $entitlement ) { return ''; } $remaining = Entitlements::remaining( $entitlement ); return sprintf( '

%s

', get_block_wrapper_attributes( array( 'class' => 'postfee-notice postfee-notice-info' ) ), esc_html( null === $remaining ? __( 'Your plan covers this post.', 'postfee' ) /* translators: %d: number of posts */ : sprintf( _n( 'You have %d prepaid post left. This one will use it.', 'You have %d prepaid posts left. This one will use one of them.', $remaining, 'postfee' ), $remaining ) ) ); } public static function render_dashboard( $attributes, $content, $block ) { ob_start(); printf( '
', get_block_wrapper_attributes( array( 'class' => 'postfee' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by core. Frontend::notices(); if ( ! is_user_logged_in() ) { Frontend::login_prompt(); } elseif ( ! empty( $_GET['postfee_upgrade'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. Frontend::upgrade_step( Checkout::upgradable_post( absint( $_GET['postfee_upgrade'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only chooses what is displayed. } else { foreach ( $block->inner_blocks as $inner_block ) { echo $inner_block->render(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Rendered blocks. } } echo '
'; return ob_get_clean(); } /** * Wraps one of the dashboard tables, which print nothing when empty. * $args are passed on to the table after its heading. */ private static function table( $attributes, $method, $default_heading, ...$args ) { if ( ! is_user_logged_in() ) { return ''; } ob_start(); Frontend::$method( '' !== $attributes['heading'] ? $attributes['heading'] : $default_heading, ...$args ); $table = ob_get_clean(); return $table ? sprintf( '
%s
', get_block_wrapper_attributes(), $table ) : ''; } public static function render_my_plans( $attributes ) { return self::table( $attributes, 'entitlements_table', __( 'My plans', 'postfee' ) ); } public static function render_my_posts( $attributes ) { return self::table( $attributes, 'posts_table', __( 'My posts', 'postfee' ), max( 1, min( 100, (int) $attributes['perPage'] ) ) ); } public static function render_new_post( $attributes ) { return sprintf( '%s', esc_url( postfee_page_url( 'submit' ) ), get_block_wrapper_attributes( array( 'class' => 'postfee-button' ) ), '' !== $attributes['text'] ? wp_kses_post( $attributes['text'] ) : esc_html__( 'Submit a new post', 'postfee' ) ); } public static function render_plans( $attributes, $content, $block ) { // Single posts and upgrades are always bought for a specific post. $types = array_values( array_intersect( (array) $attributes['types'], array( 'package', 'subscription' ) ) ); $plans = $types ? Plans::all( $types ) : array(); ob_start(); printf( '
', get_block_wrapper_attributes( array( 'class' => 'postfee' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by core. Frontend::notices(); if ( ! $plans ) { echo '

' . esc_html__( 'There are no plans available yet.', 'postfee' ) . '

'; } else { echo '
'; foreach ( $plans as $plan ) { printf( '
', esc_attr( $plan['type'] ) ); // The inner blocks are the layout of one plan: render them once per plan. foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) { echo ( new \WP_Block( $inner_block, array( 'postfee/planId' => $plan['id'] ) ) )->render(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Rendered blocks. } echo '
'; } echo '
'; } echo '
'; return ob_get_clean(); } /** * The plan a plan block is being rendered for. */ private static function plan( $block ) { return empty( $block->context['postfee/planId'] ) ? null : Plans::get( absint( $block->context['postfee/planId'] ) ); } public static function render_plan_name( $attributes, $content, $block ) { $plan = self::plan( $block ); if ( ! $plan ) { return ''; } $tag = 'h' . max( 2, min( 6, (int) $attributes['level'] ) ); return sprintf( '<%1$s %2$s>%3$s', $tag, get_block_wrapper_attributes( array( 'class' => 'postfee-plan-name' ) ), esc_html( $plan['name'] ) ); } public static function render_plan_price( $attributes, $content, $block ) { $plan = self::plan( $block ); return $plan ? sprintf( '
%s
', get_block_wrapper_attributes( array( 'class' => 'postfee-plan-price' ) ), esc_html( Plans::price_label( $plan ) ) ) : ''; } public static function render_plan_description( $attributes, $content, $block ) { $plan = self::plan( $block ); return $plan && $plan['description'] ? sprintf( '

%s

', get_block_wrapper_attributes( array( 'class' => 'postfee-plan-description' ) ), esc_html( $plan['description'] ) ) : ''; } public static function render_plan_features( $attributes, $content, $block ) { $plan = self::plan( $block ); if ( ! $plan ) { return ''; } $items = ''; foreach ( Plans::summary( $plan ) as $line ) { $items .= '
  • ' . esc_html( $line ) . '
  • '; } return sprintf( '', get_block_wrapper_attributes( array( 'class' => 'postfee-plan-includes' ) ), $items ); } public static function render_plan_buy( $attributes, $content, $block ) { $plan = self::plan( $block ); if ( ! $plan ) { return ''; } ob_start(); Frontend::plan_buy( $plan, 0, wp_kses_post( $attributes['text'] ), esc_html( $attributes['freeText'] ) ); return sprintf( '
    %s
    ', get_block_wrapper_attributes(), ob_get_clean() ); } }