Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.86% covered (danger)
42.86%
36 / 84
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Addresses_List_Table
42.86% covered (danger)
42.86%
36 / 84
18.18% covered (danger)
18.18%
2 / 11
121.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_columns
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 get_cached_bitcoin_address_object
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 column_title
100.00% covered (success)
100.00%
8 / 8
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
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
6.05
 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\Addresses\Bitcoin_Address;
14use BrianHenryIE\WP_Bitcoin_Gateway\API\Addresses\Bitcoin_Address_Factory;
15use BrianHenryIE\WP_Bitcoin_Gateway\API\Addresses\Bitcoin_Wallet;
16use BrianHenryIE\WP_Bitcoin_Gateway\API_Interface;
17use BrianHenryIE\WP_Bitcoin_Gateway\WooCommerce\Bitcoin_Gateway;
18use Exception;
19use WP_Post;
20
21/**
22 * Hooks into standard WP_List_Table actions and filters.
23 *
24 * @see wp-admin/edit.php?post_type=bh-bitcoin-address
25 * @see WP_Posts_List_Table
26 */
27class Addresses_List_Table extends \WP_Posts_List_Table {
28
29    /**
30     *
31     *
32     * @uses API_Interface::get_bitcoin_gateways()
33     */
34    protected API_Interface $api;
35
36    /**
37     * Constructor
38     *
39     * @see _get_list_table()
40     *
41     * @param array{screen?:\WP_Screen} $args The data passed by WordPress.
42     */
43    public function __construct( $args = array() ) {
44        parent::__construct( $args );
45
46        $post_type_name = $this->screen->post_type;
47
48        /**
49         * Since this object is instantiated because it was defined when registering the post type, it's
50         * extremely unlikely the post type will not exist.
51         *
52         * @var \WP_Post_Type $post_type_object
53         */
54        $post_type_object = get_post_type_object( $post_type_name );
55        $this->api        = $post_type_object->plugin_objects['api'];
56
57        add_filter( 'post_row_actions', array( $this, 'edit_row_actions' ), 10, 2 );
58    }
59
60    /**
61     * Cache to avoid repeatedly instantiating each Bitcoin_Address.
62     *
63     * @var array<int, Bitcoin_Address>
64     */
65    protected array $addresses_cache = array();
66
67    /**
68     * Cache to avoid repeatedly instantiating each Bitcoin_Wallet.
69     *
70     * @var array<int, Bitcoin_Wallet>
71     */
72    protected array $wallet_cache = array();
73
74    /**
75     * When rendering the wallet column, we will link to the gateways it is being used in.
76     *
77     * @var array<int, array<Bitcoin_Gateway>>
78     */
79    protected array $wallet_id_to_gateways_map = array();
80
81    /**
82     * Define the custom columns for the post type.
83     * Status|Order|Transactions|Received|Wallet|Derivation path.
84     *
85     * @return array<string, string> Column name : HTML output.
86     */
87    public function get_columns() {
88        $columns = parent::get_columns();
89
90        $new_columns = array();
91        foreach ( $columns as $key => $column ) {
92
93            // Omit the "comments" column.
94            if ( 'comments' === $key ) {
95                continue;
96            }
97
98            // Add remaining columns after the Title column.
99            $new_columns[ $key ] = $column;
100            if ( 'title' === $key ) {
101
102                $new_columns['status']               = 'Status';
103                $new_columns['order_id']             = 'Order';
104                $new_columns['transactions_count']   = 'Transactions';
105                $new_columns['received']             = 'Received';
106                $new_columns['wallet']               = 'Wallet';
107                $new_columns['derive_path_sequence'] = 'Path';
108            }
109            // The date column will be added last.
110        }
111
112        return $new_columns;
113    }
114
115    /**
116     * Given a WP_Post, get the corresponding Bitcoin_Address object, using a local array to cache for this request/object.
117     *
118     * @param WP_Post $post The post the address information is stored under.
119     *
120     * @return Bitcoin_Address
121     * @throws Exception When the post/post id does not match a bh-bitcoin-address cpt.
122     */
123    protected function get_cached_bitcoin_address_object( WP_Post $post ): Bitcoin_Address {
124        if ( ! isset( $this->addresses_cache[ $post->ID ] ) ) {
125            $this->addresses_cache[ $post->ID ] = new Bitcoin_Address( $post->ID );
126        }
127        return $this->addresses_cache[ $post->ID ];
128    }
129
130    /**
131     * For now, let's link to an external site when the address is clicked.
132     * That way we can skip working on a single post view, and also provide an authoritative view of the address information.
133     *
134     * @param WP_Post $post The post this row is being rendered for.
135     *
136     * @return void Echos HTML.
137     */
138    public function column_title( $post ) {
139        ob_start();
140        parent::column_title( $post );
141        $render = (string) ob_get_clean();
142
143        $bitcoin_address = $this->get_cached_bitcoin_address_object( $post );
144
145        $link = esc_url( "https://www.blockchain.com/btc/address/{$bitcoin_address->get_raw_address()}" );
146
147        $render = (string) preg_replace( '/(.*<a.*)(href=")([^"]*)(".*>)/', '$1$2' . $link . '$4', $render, 1 );
148
149        $render = (string) preg_replace( '/<a\s/', '<a target="_blank" ', $render, 1 );
150
151        echo $render;
152    }
153
154    /**
155     *
156     * @param WP_Post $item The post this row is being rendered for.
157     *
158     * @return void Echos HTML.
159     */
160    public function column_status( WP_Post $item ) {
161
162        $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
163
164        echo esc_html( $bitcoin_address->get_status() );
165    }
166
167    /**
168     *
169     * @param WP_Post $item The post this row is being rendered for.
170     *
171     * @return void Echos HTML.
172     */
173    public function column_order_id( WP_Post $item ) {
174
175        $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
176
177        $order_id = $bitcoin_address->get_order_id();
178        if ( ! is_null( $order_id ) ) {
179            $url      = admin_url( "post.php?post={$order_id}&action=edit" );
180            $order_id = (string) $order_id;
181            echo '<a href="' . esc_url( $url ) . '">' . esc_html( $order_id ) . '</a>';
182        }
183    }
184
185    /**
186     *
187     * @param WP_Post $item The post this row is being rendered for.
188     *
189     * @return void Echos HTML.
190     */
191    public function column_transactions_count( WP_Post $item ) {
192
193        $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
194
195        $transactions = $bitcoin_address->get_blockchain_transactions();
196        if ( is_array( $transactions ) ) {
197            echo count( $transactions );
198        } else {
199            echo '';
200        }
201    }
202
203    /**
204     *
205     * @param WP_Post $item The post this row is being rendered for.
206     *
207     * @return void Echos HTML.
208     */
209    public function column_received( WP_Post $item ) {
210
211        $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
212
213        echo esc_html( $bitcoin_address->get_balance() ?? 'unknown' );
214    }
215
216    /**
217     * Print the HTML for the "wallet" column.
218     *
219     * Most sites will probably only ever use one wallet. Others might change wallet once or twice. Some will have
220     * multiple instances of the gateway running at the same time.
221     *
222     * TODO: Currently, this links to the WC_Payment_Gateway page. Maybe it should link externally like the
223     * title column?
224     *
225     * @param WP_Post $item The post this row is being rendered for.
226     *
227     * @return void Echos HTML.
228     */
229    public function column_wallet( WP_Post $item ) {
230
231        try {
232            $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
233        } catch ( Exception $exception ) {
234            return;
235        }
236
237        $wallet_post_id = $bitcoin_address->get_wallet_parent_post_id();
238        $wallet_post    = get_post( $wallet_post_id );
239        if ( ! $wallet_post ) {
240            // TODO: echo/log error.
241            return;
242        }
243        $wallet_address = $wallet_post->post_excerpt;
244        $abbreviated    = substr( $wallet_address, 0, 7 ) . '...' . substr( $wallet_address, -3 );
245
246        // Is this wallet being used by a gateway?
247        if ( ! isset( $this->wallet_id_to_gateways_map[ $wallet_post_id ] ) ) {
248            $this->wallet_id_to_gateways_map[ $wallet_post_id ] = array_filter(
249                $this->api->get_bitcoin_gateways(),
250                function ( Bitcoin_Gateway $gateway ) use ( $wallet_address ): bool {
251                    return $gateway->get_xpub() === $wallet_address;
252                }
253            );
254        }
255        $gateways = $this->wallet_id_to_gateways_map[ $wallet_post_id ];
256
257        $href_html = '';
258        if ( 1 === count( $gateways ) ) {
259            $gateway   = array_pop( $gateways );
260            $href_html = '<a href="' . esc_url( admin_url( "admin.php?page=wc-settings&tab=checkout&section={$gateway->id}" ) ) . '">';
261        }
262
263        echo '<span title="' . esc_attr( $wallet_address ) . '">';
264        echo wp_kses_post( $href_html );
265        echo esc_html( $abbreviated );
266        if ( ! empty( $href_html ) ) {
267            echo '</a>'; }
268        echo '</span>';
269    }
270
271    /**
272     * TODO: This should be sortable, and in theory should match the ID asc/desc sequence.
273     *
274     * @param WP_Post $item The post this row is being rendered for.
275     *
276     * @return void Echos HTML.
277     */
278    public function column_derive_path_sequence( WP_Post $item ) {
279
280        $bitcoin_address = $this->get_cached_bitcoin_address_object( $item );
281
282        $nth  = $bitcoin_address->get_derivation_path_sequence_number();
283        $path = "0/$nth";
284        echo esc_html( $path );
285    }
286
287    /**
288     * Remove edit and view actions, add an update action.
289     *
290     * TODO: add a click handler to the update (query for new transactions) action.
291     *
292     * @hooked post_row_actions
293     * @see \WP_Posts_List_Table::handle_row_actions()
294     *
295     * @param array<string,string> $actions Action id : HTML.
296     * @param WP_Post              $post     The post object.
297     *
298     * @return array<string,string>
299     */
300    public function edit_row_actions( array $actions, WP_Post $post ): array {
301
302        if ( Bitcoin_Address::POST_TYPE !== $post->post_type ) {
303            return $actions;
304        }
305
306        unset( $actions['edit'] );
307        unset( $actions['inline hide-if-no-js'] ); // "quick edit".
308        unset( $actions['view'] );
309
310        $actions['update_address'] = 'Update';
311
312        return $actions;
313    }
314}