gate = $gate; $this->invoices = $invoices; $this->customers = $customers; $this->presenter = $presenter; $this->settings = $settings; $this->limiter = $limiter; } /** * {@inheritdoc} */ public function register(): void { add_action( 'admin_post_' . self::ACTION, array( $this, 'print_for_admin' ) ); add_action( 'admin_post_' . self::CUSTOMER_ACTION, array( $this, 'print_for_customer' ) ); add_action( 'template_redirect', array( $this, 'maybe_print_public' ) ); } /** * URL an admin opens to print an invoice. * * @since 0.1.0 * * @param int $invoice_id Invoice id. * @return string */ public static function admin_url( int $invoice_id ): string { return NonceUrl::make( admin_url( 'admin-post.php' ), array( 'action' => self::ACTION, 'id' => $invoice_id, ), self::ACTION . '_' . $invoice_id ); } /** * URL a customer opens to print one of their own invoices. * * @since 0.1.0 * * @param int $invoice_id Invoice id. * @return string */ public static function customer_url( int $invoice_id ): string { return NonceUrl::make( admin_url( 'admin-post.php' ), array( 'action' => self::CUSTOMER_ACTION, 'id' => $invoice_id, ), self::CUSTOMER_ACTION . '_' . $invoice_id ); } /** * Shows an invoice to the customer it belongs to. * * @since 0.1.0 * * @return void */ public function print_for_customer(): void { $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified on the next line. check_admin_referer( self::CUSTOMER_ACTION . '_' . $id ); $invoice = $this->invoices->find( $id ); // Not yours, a draft and missing all look the same. if ( null === $invoice || 'draft' === $invoice['status'] || ! $this->gate->customer_owns( 'invoice', $id ) ) { wp_die( esc_html__( 'Invoice not found.', 'hostkhata' ), '', array( 'response' => 404 ) ); } $this->render( $invoice ); } /** * Shows an invoice to an admin. * * @since 0.1.0 * * @return void */ public function print_for_admin(): void { $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified on the next line. check_admin_referer( self::ACTION . '_' . $id ); if ( ! current_user_can( Capabilities::MANAGE_INVOICES ) ) { wp_die( esc_html__( 'You are not allowed to view invoices.', 'hostkhata' ), '', array( 'response' => 403 ) ); } $invoice = $this->invoices->find( $id ); if ( null === $invoice ) { wp_die( esc_html__( 'Invoice not found.', 'hostkhata' ), '', array( 'response' => 404 ) ); } $this->render( $invoice ); } /** * Shows an invoice to anyone with its secret link, when links are enabled. * * @since 0.1.0 * * @return void */ public function maybe_print_public(): void { if ( ! isset( $_GET['hostkhata_invoice'] ) || ! $this->settings->get( 'invoice_public_links' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The secret token is the credential. return; } $token = preg_replace( '/[^a-f0-9]/', '', sanitize_text_field( wp_unslash( $_GET['hostkhata_invoice'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Guessing tokens is pointless at 128 bits, but this also stops scraping. if ( ! $this->limiter->hit( 'invoice_view', 60, HOUR_IN_SECONDS ) ) { wp_die( esc_html__( 'Too many requests. Please try again later.', 'hostkhata' ), '', array( 'response' => 429 ) ); } $invoice = strlen( $token ) === 32 ? $this->invoices->find_by_token( $token ) : null; if ( null === $invoice || 'draft' === $invoice['status'] ) { wp_die( esc_html__( 'Invoice not found.', 'hostkhata' ), '', array( 'response' => 404 ) ); } $this->render( $invoice ); } /** * Outputs the page and stops. * * @param array $invoice Invoice with lines. * @return void */ private function render( array $invoice ): void { $customer = $this->customers->find( $invoice['customer_id'] ); nocache_headers(); header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); header( 'X-Robots-Tag: noindex, nofollow' ); $brand = (string) $this->settings->get( 'portal_primary_color' ); wp_register_style( 'hostkhata-invoice', HOSTKHATA_URL . 'assets/public/invoice.css', array(), HOSTKHATA_VERSION ); wp_register_script( 'hostkhata-invoice', HOSTKHATA_URL . 'assets/public/invoice.js', array(), HOSTKHATA_VERSION, true ); if ( preg_match( '/^#[0-9a-f]{6}$/i', $brand ) ) { wp_add_inline_style( 'hostkhata-invoice', ':root { --brand: ' . $brand . '; }' ); } $html = View::render( 'invoice/invoice', array( 'invoice' => $this->presenter->detail( $invoice ), 'customer' => $customer, 'business' => array( 'name' => (string) $this->settings->get( 'business_name' ), 'email' => (string) $this->settings->get( 'business_email' ), 'phone' => (string) $this->settings->get( 'business_phone' ), 'address' => (string) $this->settings->get( 'business_address' ), 'logo' => (string) $this->settings->get( 'business_logo_url' ), 'website' => (string) $this->settings->get( 'business_website' ), 'tax_id' => (string) $this->settings->get( 'business_tax_id' ), ), 'watermark' => (bool) $this->settings->get( 'invoice_watermark' ), 'brand' => (string) $this->settings->get( 'portal_primary_color' ), 'payment_instructions' => (string) $this->settings->get( 'payment_instructions' ), 'footer' => (string) $this->settings->get( 'invoice_footer' ), ) ); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The template escapes everything it prints. exit; } }