customers = $customers; $this->invoices = $invoices; $this->payments = $payments; $this->settings = $settings; } /** * {@inheritdoc} */ public function register(): void { add_action( 'admin_post_' . self::ACTION, array( $this, 'download' ) ); } /** * Capability each export needs. * * @since 0.1.0 * * @return array */ public static function kinds(): array { return array( 'customers' => Capabilities::MANAGE_CUSTOMERS, 'invoices' => Capabilities::MANAGE_INVOICES, 'payments' => Capabilities::MANAGE_PAYMENTS, ); } /** * Link the app uses for a download. * * @since 0.1.0 * * @param string $kind `customers`, `invoices` or `payments`. * @return string */ public static function url( string $kind ): string { return NonceUrl::make( admin_url( 'admin-post.php' ), array( 'action' => self::ACTION, 'kind' => $kind, ), self::ACTION . '_' . $kind ); } /** * Sends the file. * * @since 0.1.0 * * @return void */ public function download(): void { $kind = isset( $_GET['kind'] ) ? sanitize_key( wp_unslash( $_GET['kind'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified on the next line. check_admin_referer( self::ACTION . '_' . $kind ); $kinds = self::kinds(); if ( ! isset( $kinds[ $kind ] ) || ! current_user_can( $kinds[ $kind ] ) ) { wp_die( esc_html__( 'You are not allowed to download this.', 'hostkhata' ), '', array( 'response' => 403 ) ); } nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="hostkhata-' . $kind . '-' . gmdate( 'Y-m-d' ) . '.csv"' ); header( 'X-Content-Type-Options: nosniff' ); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV, not HTML; cells are made safe by CsvWriter. echo "\xEF\xBB\xBF"; // Byte order mark, so Excel reads Bangla and other UTF-8 text correctly. foreach ( $this->lines( $kind ) as $line ) { echo $line; } // phpcs:enable exit; } /** * Header and rows of an export, one CSV line at a time. * * @param string $kind Kind. * @return \Generator */ public function lines( string $kind ): \Generator { $repository = 'customers' === $kind ? $this->customers : ( 'invoices' === $kind ? $this->invoices : $this->payments ); yield CsvWriter::line( $this->header( $kind ) ); $page = 1; $sent = 0; do { $batch = $repository->query( array( 'orderby' => 'id', 'order' => 'asc', 'page' => $page, 'per_page' => self::PER_PAGE, ) ); $size = 0; foreach ( $batch['items'] as $item ) { yield CsvWriter::line( $this->row( $kind, $item ) ); ++$sent; ++$size; } ++$page; } while ( self::PER_PAGE === $size && $sent < self::MAX_ROWS ); } /** * Column titles. * * @param string $kind Kind. * @return string[] */ private function header( string $kind ): array { switch ( $kind ) { case 'customers': return array( 'id', 'email', 'first_name', 'last_name', 'company', 'phone', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'status', 'tags', 'credit_balance', 'created' ); case 'invoices': return array( 'invoice_number', 'status', 'customer', 'customer_email', 'issue_date', 'due_date', 'subtotal', 'discount', 'tax', 'total', 'credit_applied', 'paid', 'balance', 'currency' ); default: return array( 'id', 'date', 'type', 'status', 'invoice_number', 'customer', 'amount', 'method', 'transaction_id', 'sender_number', 'currency' ); } } /** * One row. * * @param string $kind Kind. * @param array $item Record. * @return array */ private function row( string $kind, array $item ): array { $money = function ( $minor ): string { return $this->decimal( (int) $minor ); }; switch ( $kind ) { case 'customers': return array( $item['id'], $item['user_email'], $item['first_name'], $item['last_name'], $item['company'], $item['phone'], $item['address_1'], $item['address_2'], $item['city'], $item['state'], $item['postcode'], $item['country'], $item['status'], implode( ';', (array) $item['tags'] ), $money( $item['credit_balance'] ), $item['created_at_gmt'], ); case 'invoices': return array( $item['invoice_number'], $item['status'], $item['customer_name'], $item['customer_email'], $item['issue_date'], $item['due_date'], $money( $item['subtotal'] ), $money( $item['discount_total'] ), $money( $item['tax_total'] ), $money( $item['total'] ), $money( $item['credit_applied'] ), $money( $item['amount_paid'] ), $money( $item['balance'] ), $item['currency'] ?? $this->settings->get( 'currency_code' ), ); default: return array( $item['id'], $item['created_at_gmt'], $item['type'], $item['status'], $item['invoice_number'], $item['customer_name'], $money( $item['amount'] ), $item['gateway'], $item['transaction_id'], $item['sender_number'], $item['currency'] ?? $this->settings->get( 'currency_code' ), ); } } /** * Minor units as a plain decimal such as `1250.50`. * * @param int $minor Amount in minor units. * @return string */ private function decimal( int $minor ): string { $decimals = max( 0, (int) $this->settings->get( 'currency_decimals' ) ); return number_format( $minor / ( 10 ** $decimals ), $decimals, '.', '' ); } }