Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.83% covered (danger)
35.83%
43 / 120
15.38% covered (danger)
15.38%
2 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Addresses_List_Table
35.83% covered (danger)
35.83%
43 / 120
15.38% covered (danger)
15.38%
2 / 13
358.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 get_columns
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 get_bitcoin_address_object
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_bitcoin_wallet_object
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 column_title
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 column_status
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 column_order_id
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 column_transactions_count
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 column_received
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 column_wallet
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
3.03
 column_gateways
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
182
 column_derive_path_sequence
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 edit_row_actions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Display generated addresses, their status and related orders.
4 *
5 * TODO: Add filters for status, wallet address.
6 * TODO: Hijack Add New button to generate new addresses.
7 *
8 * @package    brianhenryie/bh-wp-bitcoin-gateway
9 */
10
11namespace BrianHenryIE\WP_Bitcoin_Gateway\Admin;
12
13use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Address;
14use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Wallet;
15use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Bitcoin_Address_Repository;
16use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Bitcoin_Wallet_Repository;
17use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Address_WP_Post_Interface;
18use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Exceptions\BH_WP_Bitcoin_Gateway_Exception;
19use BrianHenryIE\WP_Bitcoin_Gateway\API_Interface;
20use BrianHenryIE\WP_Bitcoin_Gateway\Brick\Money\Exception\UnknownCurrencyException;
21use BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\Bitcoin_Gateway;
22use BrianHenryIE\WP_Bitcoin_Gateway\WP_Includes\Post_BH_Bitcoin_Address;
23use Exception;
24use WP_Post;
25use WP_Post_Type;
26use WP_Posts_List_Table;
27use WP_Screen;
28
29/**
30 * Hooks into standard WP_List_Table actions and filters.
31 *
32 * @see wp-admin/edit.php?post_type=bh-bitcoin-address
33 * @see WP_Posts_List_Table
34 *
35 * @phpstan-type Address_List_Table_Dependencies_Array array{api:API_Interface,bitcoin_address_repository:Bitcoin_Address_Repository,bitcoin_wallet_repository:Bitcoin_Wallet_Repository}
36 */
37class Addresses_List_Table extends WP_Posts_List_Table {
38
39    /**
40     *
41     *
42     * @uses API_Interface::get_bitcoin_gateways()
43     */
44    protected API_Interface $api;
45
46    /**
47     * To get the full address details for display.
48     */
49    protected Bitcoin_Address_Repository $bitcoin_address_repository;
50
51    /**
52     * To get the integrations the address is related to.
53     */
54    protected Bitcoin_Wallet_Repository $bitcoin_wallet_repository;
55
56    /**
57     * Constructor
58     *
59     * Retrieves dependencies from the WP_Post_Type object that were registered
60     * via Post_BH_Bitcoin_Address::register_address_post_type().
61     *
62     * @see _get_list_table()
63     *
64     * @param array{screen?:WP_Screen} $args The data passed by WordPress.
65     *
66     * @throws BH_WP_Bitcoin_Gateway_Exception If the post type object does not exist or does not have the dependencies this class needs.
67     */
68    public function __construct( $args = array() ) {
69        parent::__construct( $args );
70
71        $post_type_name = $this->screen->post_type;
72
73        /**
74         * Since this object is instantiated because it was defined when registering the post type, it's
75         * extremely unlikely the post type will not exist.
76         *
77         * @see Post_BH_Bitcoin_Address::$dependencies
78         * @see Post_BH_Bitcoin_Address::register_address_post_type()
79         *
80         * @var WP_Post_Type&object{dependencies:Address_List_Table_Dependencies_Array} $post_type_object
81         */
82        $post_type_object = get_post_type_object( $post_type_name );
83
84        // @phpstan-ignore-next-line function.impossibleType This could be null – I don't know how to use & in the type above with null.
85        if ( is_null( $post_type_object ) ) {
86            throw new BH_WP_Bitcoin_Gateway_Exception( 'Addresses_List_Table constructed before post type registered' );
87        }
88        if ( ! isset( $post_type_object->dependencies ) ) {
89            throw new BH_WP_Bitcoin_Gateway_Exception( 'Addresses_List_Table constructed without required dependencies' );
90        }
91
92        $this->api                        = $post_type_object->dependencies['api'];
93        $this->bitcoin_address_repository = $post_type_object->dependencies['bitcoin_address_repository'];
94        $this->bitcoin_wallet_repository  = $post_type_object->dependencies['bitcoin_wallet_repository'];
95
96        add_filter( 'post_row_actions', array( $this, 'edit_row_actions' ), 10, 2 );
97    }
98
99    /**
100     * When rendering the wallet column, we will link to the gateways it is being used in.
101     *
102     * @var array<int, array<Bitcoin_Gateway>>
103     */
104    protected array $wallet_id_to_gateways_map = array();
105
106    /**
107     * Define the custom columns for the post type.
108     * Status|Order|Transactions|Amount-Received|Wallet|Derivation-path|Last-modified.
109     *
110     * @return array<string, string> Column name : HTML output.
111     */
112    public function get_columns() {
113        /** @var non-empty-array<string,string> $columns */
114        $columns = parent::get_columns();
115
116        /** @var non-empty-array<string,string> $new_columns */
117        $new_columns = array();
118        foreach ( $columns as $key => $column ) {
119
120            // Omit the "comments" column.
121            if ( 'comments' === $key ) {
122                continue;
123            }
124
125            // Add remaining columns after the Title column.
126            $new_columns[ $key ] = $column;
127            if ( 'title' === $key ) {
128
129                $new_columns['status']               = 'Status';
130                $new_columns['order_id']             = 'Order'; // TODO: Change to ~"assigned to".
131                $new_columns['transactions_count']   = 'Transactions';
132                $new_columns['received']             = 'Received';
133                $new_columns['wallet']               = 'Wallet'; // TODO: hide when there is only one.
134                $new_columns['derive_path_sequence'] = 'Path';
135                $new_columns['gateways']             = 'Gateways'; // TODO: hide when there is only one.
136            }
137            // The date column will be added last.
138        }
139
140        return $new_columns;
141    }
142
143    /**
144     * Given a WP_Post, get the corresponding Bitcoin_Address object (TODO: use a local array to cache.).
145     *
146     * @param WP_Post $post The post the address information is stored under.
147     *
148     * @throws BH_WP_Bitcoin_Gateway_Exception When the post/post id does not match a bh-bitcoin-address cpt.
149     */
150    protected function get_bitcoin_address_object( WP_Post $post ): Bitcoin_Address {
151        return $this->bitcoin_address_repository->get_by_wp_post( $post );
152    }
153
154    /**
155     * Get the Wallet object so we can link to the gateways it's used for.
156     *
157     * @used-by self::column_gateways()
158     *
159     * @param int $post_id The post id for the saved wallet.
160     * @throws UnknownCurrencyException TODO: we should catch this and return null, it's not important enough to crash.
161     */
162    protected function get_bitcoin_wallet_object( int $post_id ): Bitcoin_Wallet {
163        return $this->bitcoin_wallet_repository->get_by_wp_post_id( $post_id );
164    }
165
166    /**
167     * For now, let's link to an external site when the address is clicked.
168     * That way we can skip working on a single post view, and also provide an authoritative view of the address information.
169     *
170     * TODO: Replace this external link with a view that shows the transactions.
171     *
172     * @param WP_Post $post The post this row is being rendered for.
173     *
174     * @return string The return of this gets `echo`d by {@see WP_Posts_List_Table::_column_title()}.
175     */
176    public function column_title( $post ) {
177        ob_start();
178        parent::column_title( $post );
179        $render = (string) ob_get_clean();
180
181        $bitcoin_address = $this->get_bitcoin_address_object( $post );
182
183        $link = sprintf(
184            'https://www.blockchain.com/btc/address/%s',
185            $bitcoin_address->get_raw_address()
186        );
187
188        $render = (string) preg_replace( '/(.*<a.*)(href=")([^"]*)(".*>)/', '$1$2' . $link . '$4', $render, 1 );
189
190        $render = (string) preg_replace( '/<a\s/', '<a target="_blank" ', $render, 1 );
191
192        return wp_kses_post( $render );
193    }
194
195    /**
196     *
197     * @param WP_Post $item The post this row is being rendered for.
198     *
199     * @return void Echos HTML.
200     */
201    public function column_status( WP_Post $item ): void {
202
203        $bitcoin_address = $this->get_bitcoin_address_object( $item );
204
205        echo esc_html( $bitcoin_address->get_status()->value );
206    }
207
208    /**
209     *
210     * @param WP_Post $item The post this row is being rendered for.
211     *
212     * @return void Echos HTML.
213     */
214    public function column_order_id( WP_Post $item ): void {
215
216        $bitcoin_address = $this->get_bitcoin_address_object( $item );
217
218        $order_id = $bitcoin_address->get_order_id();
219        if ( ! is_null( $order_id ) ) {
220            $url      = admin_url( "post.php?post={$order_id}&action=edit" );
221            $order_id = (string) $order_id;
222            echo '<a href="' . esc_url( $url ) . '">' . esc_html( $order_id ) . '</a>';
223        }
224    }
225
226    /**
227     *
228     * @param WP_Post $item The post this row is being rendered for.
229     *
230     * @return void Echos HTML.
231     */
232    public function column_transactions_count( WP_Post $item ): void {
233
234        $bitcoin_address = $this->get_bitcoin_address_object( $item );
235
236        $transactions = $this->api->get_saved_transactions( $bitcoin_address );
237        if ( is_array( $transactions ) ) {
238            echo count( $transactions );
239        } else {
240            echo '';
241        }
242    }
243
244    /**
245     *
246     * @param WP_Post $item The post this row is being rendered for.
247     *
248     * @return void Echos HTML.
249     */
250    public function column_received( WP_Post $item ): void {
251
252        $bitcoin_address = $this->get_bitcoin_address_object( $item );
253
254        echo esc_html( $bitcoin_address->get_amount_received() ?? 'unknown' );
255    }
256
257    /**
258     * Print the HTML for the "wallet" column.
259     *
260     * Most sites will probably only ever use one wallet. Others might change wallet once or twice. Some will have
261     * multiple instances of the gateway running at the same time.
262     *
263     * @param WP_Post $item The post this row is being rendered for.
264     *
265     * @return void Echos HTML.
266     */
267    public function column_wallet( WP_Post $item ): void {
268
269        try {
270            $bitcoin_address = $this->get_bitcoin_address_object( $item );
271        } catch ( Exception ) {
272            return;
273        }
274
275        $wallet_post_id = $bitcoin_address->get_wallet_parent_post_id();
276        $wallet_post    = get_post( $wallet_post_id );
277        if ( ! $wallet_post ) {
278            // TODO: echo/log error.
279            return;
280        }
281
282        $wallet_address   = $wallet_post->post_title;
283        $wallet_admin_url = admin_url(
284            sprintf(
285                'post.php?post=%d&action=edit',
286                $bitcoin_address->get_wallet_parent_post_id()
287            )
288        );
289        $abbreviated      = substr( $wallet_address, 0, 7 ) . '...' . substr( $wallet_address, -3 );
290
291        printf(
292            '<span title="%s"><a href="%s">%s</a></span>',
293            esc_attr( $wallet_address ),
294            esc_url( $wallet_admin_url ),
295            esc_html( $abbreviated ),
296        );
297    }
298
299    /**
300     * Print the HTML for the "gateways" column.
301     *
302     * This will be ~"WooCommerce: bitcoin" and link to the gateway settings page.
303     *
304     * Most sites will probably only ever use one gateway.
305     *
306     * @see API_Interface::get_or_save_wallet_for_master_public_key()
307     *
308     * @param WP_Post $item The post this row is being rendered for.
309     *
310     * @return void Echos HTML.
311     */
312    public function column_gateways( WP_Post $item ): void {
313
314        try {
315            $bitcoin_address = $this->get_bitcoin_address_object( $item );
316        } catch ( Exception ) {
317            return;
318        }
319
320        try {
321            $bitcoin_wallet = $this->get_bitcoin_wallet_object( $bitcoin_address->get_wallet_parent_post_id() );
322        } catch ( Exception ) {
323            return;
324        }
325
326        foreach ( $bitcoin_wallet->get_associated_gateways_details() as $gateways_list_item ) {
327
328            if ( empty( $gateways_list_item['integration'] ) || empty( $gateways_list_item['gateway_id'] ) ) {
329                // TODO: log.
330                continue;
331            }
332
333            $integration = $gateways_list_item['integration'];
334            $gateway_id  = $gateways_list_item['gateway_id'];
335
336            /** @var array{href?:string|mixed,text?:string|mixed} $gateway_href_and_text */
337            $gateway_href_and_text = array(
338                'href' => '',
339                'text' => '',
340            );
341
342            // TODO: cache this.
343            /**
344             * @param array{href?:string|mixed,text?:string|mixed} $filtered_result
345             * @param non-empty-string $integration
346             * @param non-empty-string $gateway_id
347             * @param Bitcoin_Wallet $bitcoin_wallet
348             * @param ?Bitcoin_Address $bitcoin_address The address the current table row is displaying (when in appropriate context).
349             * @var array{href?:string|mixed,text?:string|mixed} $gateway_href_and_text
350             */
351            $gateway_href_and_text = apply_filters( 'bh_wp_bitcoin_gateway_gateway_link', $gateway_href_and_text, $integration, $gateway_id, $bitcoin_wallet, $bitcoin_address );
352
353            /** @var ?non-empty-string $gateway_href */
354            $gateway_href = isset( $gateway_href_and_text['href'] ) && is_string( $gateway_href_and_text['href'] ) && '' !== $gateway_href_and_text['href']
355                ? $gateway_href_and_text['href']
356                : null;
357
358            /** @var non-empty-string $gateway_text */
359            $gateway_text = isset( $gateway_href_and_text['text'] ) && is_string( $gateway_href_and_text['text'] ) && '' !== $gateway_href_and_text['text']
360                ? $gateway_href_and_text['text']
361                : $gateway_id;
362
363            /** @var non-empty-string $link */
364            $link = ! empty( $gateway_href )
365                ? sprintf(
366                    '<a href="%s">%s</a>',
367                    esc_url( $gateway_href, null, 'href' ),
368                    esc_html( $gateway_text )
369                )
370                : $gateway_text;
371
372            printf(
373                '<span class="%s">%s</span>',
374                esc_attr( sprintf( '%s::%s', $integration, $gateway_id ) ),
375                wp_kses_post( $link )
376            );
377        }
378    }
379
380    /**
381     * TODO: This should be sortable, and in theory should match the ID asc/desc sequence.
382     *
383     * @param WP_Post $item The post this row is being rendered for.
384     *
385     * @return void Echos HTML.
386     */
387    public function column_derive_path_sequence( WP_Post $item ) {
388
389        $bitcoin_address = $this->get_bitcoin_address_object( $item );
390
391        $nth  = $bitcoin_address->get_derivation_path_sequence_number();
392        $path = "0/$nth";
393        echo esc_html( $path );
394    }
395
396    /**
397     * Remove edit and view actions, add an update action.
398     *
399     * TODO: add a click handler to the update (query for new transactions) action.
400     *
401     * @hooked post_row_actions
402     * @see WP_Posts_List_Table::handle_row_actions()
403     *
404     * @param array<string,string> $actions Action id : HTML.
405     * @param WP_Post              $post     The post object.
406     *
407     * @return array<string,string>
408     */
409    public function edit_row_actions( array $actions, WP_Post $post ): array {
410
411        if ( Bitcoin_Address_WP_Post_Interface::POST_TYPE !== $post->post_type ) {
412            return $actions;
413        }
414
415        unset( $actions['edit'] );
416        unset( $actions['inline hide-if-no-js'] ); // "quick edit".
417        unset( $actions['view'] );
418
419        $actions['update_address'] = 'Update';
420
421        return $actions;
422    }
423}