Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.42% covered (success)
92.42%
61 / 66
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Plugins_List_Table
92.42% covered (success)
92.42%
61 / 66
75.00% covered (warning)
75.00%
3 / 4
25.27
0.00% covered (danger)
0.00%
0 / 1
 plugin_specific_action_links
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
10
 row_meta
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 merge_arrays
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 edit_plugins_array
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * The main class in the plugin.
4 * Hooked to actions and filters defined in WP_Plugins_List_Table.
5 *
6 * Actions links runs first, but needs to pull in links from meta links, so fires that filter during its run, which
7 * is then fired again later during the normal meta links run.
8 *
9 * @link       https://github.com/brianhenryie/bh-wp-plugins-page
10 * @since      1.0.0
11 *
12 * @package    brianhenryie/bh-wp-plugins-page
13 */
14
15namespace BrianHenryIE\WP_Plugins_Page\Admin;
16
17use BrianHenryIE\WP_Plugins_Page\API\API;
18use BrianHenryIE\WP_Plugins_Page\API\Parsed_Link;
19
20/**
21 * Parses each link, then decides to move or discard, then removes formatting.
22 *
23 * @see \WP_List_Table
24 * @see \WP_Plugins_List_Table
25 */
26class Plugins_List_Table {
27
28    /**
29     * Links found in the first column that should be moved to the middle column.
30     * indexed by plugin basename.
31     *
32     * @var array<string, array<Parsed_Link>>
33     */
34    protected array $external_parsed_action_links = array();
35
36    /**
37     * Links from the middle column that should be moved to the first column.
38     * indexed by plugin basename.
39     *
40     * @var array<string, array<Parsed_Link>>
41     */
42    protected array $internal_parsed_meta_links = array();
43
44    /**
45     * Hooked to plugin-specific action links filters (by looping over 'active_plugins' option).
46     *
47     * @hooked plugin_action_links_{$basename}
48     *
49     * @param array<int|string, string>     $action_links The existing plugin links (usually "Deactivate").
50     * @param string                        $plugin_basename The plugin's directory/filename.php.
51     * @param null|array<int|string, mixed> $plugin_data An array of plugin data. See `get_plugin_data()`.
52     * @param string                        $context     The plugin context. 'all'|'active'|'inactive'|'recently_activated'
53     *                                                    |'upgrade'|'mustuse'|'dropins'|'search'.
54     *
55     * @return array<int|string, string> The links to display below the plugin name on plugins.php.
56     */
57    public function plugin_specific_action_links( array $action_links, string $plugin_basename, ?array $plugin_data, string $context ): array {
58
59        // This is probably the case where JetPack (or maybe another plugin, like this does) is running `apply_filters`, so this isn't the case we want to work on.
60        if ( empty( $plugin_data ) ) {
61            return $action_links;
62        }
63
64        $parsed_action_links = array();
65        foreach ( $action_links as $key => $html_string ) {
66            $parsed_action_links[ $key ] = new Parsed_Link( $key, $html_string );
67        }
68
69        $internal_parsed_action_links = array();
70        $external_parsed_action_links = array();
71
72        // Save external links to move them to the middle column.
73        foreach ( $parsed_action_links as $key => $parsed_link ) {
74
75            if ( $parsed_link->has_external_url() ) {
76                $external_parsed_action_links[ $key ] = $parsed_link;
77            } else {
78                $internal_parsed_action_links[ $key ] = $parsed_link;
79            }
80        }
81
82        $this->external_parsed_action_links[ $plugin_basename ] = $external_parsed_action_links;
83
84        /**
85         * Get internal links from second column.
86         *
87         * We're already hooked on this filter. We need to invoke it to pull in data from other plugins.
88         *
89         * @see self::row_meta()
90         */
91        apply_filters( 'plugin_row_meta', array(), $plugin_basename, $plugin_data, $context );
92        $internal_parsed_meta_links = $this->internal_parsed_meta_links[ $plugin_basename ] ?? array();
93
94        /**
95         * All links we want in this column.
96         *
97         * @var Parsed_Link[] $parsed_action_links
98         */
99        $parsed_action_links = $this->merge_arrays( array( $internal_parsed_action_links, $internal_parsed_meta_links ) );
100
101        // Reorder:
102        // Move settings to the beginning.
103        // Move Logs second to end.
104        // Move Deactivate at the end.
105        $ordered_links_arrays = array(
106            'settings'   => array(),
107            'links'      => array(),
108            'log'        => array(),
109            'deactivate' => array(),
110        );
111
112        // If there is no anchor, e.g. it is just text, we do not want it in the action links.
113        // Filter unwanted links.
114        // Remove upsells.
115        foreach ( $parsed_action_links as $key => $parsed_link ) {
116
117            if ( $parsed_link->is_empty()
118                || $parsed_link->is_contains_unwanted_terms() ) {
119                continue;
120            }
121
122            $type = $parsed_link->get_type() ?? 'links';
123
124            if ( is_int( $key ) ) {
125                $ordered_links_arrays[ $type ][] = $parsed_link;
126            } else {
127                $ordered_links_arrays[ $type ][ $key ] = $parsed_link;
128            }
129        }
130
131        /**
132         * Merge the sorted links into one array.
133         *
134         * @var Parsed_Link[] $ordered_links
135         */
136        $ordered_links = $this->merge_arrays( $ordered_links_arrays );
137
138        $cleaned_action_links = array();
139        foreach ( $ordered_links as $key => $parsed_link ) {
140            $cleaned_action_links[ $key ] = $parsed_link->get_cleaned_link();
141        }
142        return $cleaned_action_links;
143    }
144
145    /**
146     * Row meta is the middle column.
147     *
148     * Thankfully, plugin_row_meta runs after plugin_action_links, allowing us to move links from the more important
149     * column to the description column.
150     *
151     * @hooked plugin_row_meta
152     * Hooked at 9999 so all links have been added first.
153     *
154     * @see https://rudrastyh.com/wordpress/plugin_action_links-plugin_row_meta.html
155     *
156     * @param string[] $meta_links The meta information/links displayed by the plugin description.
157     * @param string   $plugin_file_name The plugin filename to match when filtering.
158     * @param string[] $plugin_data Associative array including PluginURI, slug, Author, Version.
159     * @param string   $status The plugin status, e.g. 'Inactive'.
160     *
161     * @return string[] The filtered $plugin_meta.
162     */
163    public function row_meta( array $meta_links, $plugin_file_name, ?array $plugin_data, ?string $status ): array {
164
165        $parsed_meta_links = array();
166        foreach ( $meta_links as $key => $html_string ) {
167            $parsed_meta_links[ $key ] = new Parsed_Link( $key, $html_string );
168        }
169
170        $internal_parsed_meta_links = array();
171        $external_parsed_meta_links = array();
172
173        // Save external links to move them to the middle column.
174        foreach ( $parsed_meta_links as $key => $parsed_link ) {
175
176            // Check all URLs in the text for external links.
177            if ( $parsed_link->has_internal_url() && 'view-details' !== $parsed_link->get_type() ) {
178                $internal_parsed_meta_links[ $key ] = $parsed_link;
179            } else {
180                $external_parsed_meta_links[ $key ] = $parsed_link;
181            }
182        }
183
184        // Save internal links for use in the first column.
185        $this->internal_parsed_meta_links[ $plugin_file_name ] = $internal_parsed_meta_links;
186
187        // Get external links from first column.
188        $external_parsed_action_links = $this->external_parsed_action_links[ $plugin_file_name ] ?? array();
189
190        /**
191         * All the links we want to display in this column.
192         *
193         * @var Parsed_Link[] $external_parsed_meta_links
194         */
195        $external_parsed_meta_links = $this->merge_arrays( array( $external_parsed_meta_links, $external_parsed_action_links ) );
196
197        // Filter unwanted links.
198        // Remove upsells.
199        // Remove external license links.
200        $cleaned_links = array();
201        foreach ( $external_parsed_meta_links as $key => $parsed_link ) {
202
203            if ( $parsed_link->is_empty()
204            || $parsed_link->is_contains_unwanted_terms() ) {
205                continue;
206            }
207
208            $parsed_link->replace_text_with_icons();
209            $cleaned_links[ $key ] = $parsed_link->get_cleaned_link();
210        }
211
212        return $cleaned_links;
213    }
214
215
216    /**
217     * Merge associative arrays, preserve string keys.
218     *
219     * @param array<mixed> $all_arrays Array of arrays.
220     *
221     * @return array<mixed>
222     */
223    protected function merge_arrays( array $all_arrays ): array {
224        $merged_array = array();
225
226        foreach ( $all_arrays as $sub_array ) {
227            foreach ( $sub_array as $key => $value ) {
228                if ( is_int( $key ) ) {
229                    $merged_array[] = $value;
230                } else {
231                    $merged_array[ $key ] = $value;
232                }
233            }
234        }
235        return $merged_array;
236    }
237
238    /**
239     * Merge our saved changes into the get_plugins() array when the page is rendering.
240     *
241     * @hooked all_plugins
242     * @see \WP_Plugins_List_Table::prepare_items()
243     *
244     * @param array<string,array<string,string>> $all_plugins The WordPress `get_plugins()` array.
245     *
246     * @return array<string,array<string,string>>
247     */
248    public function edit_plugins_array( array $all_plugins ): array {
249
250        $changes = get_option( API::PLUGINS_PAGE_CHANGES_OPTION_NAME, array() );
251
252        foreach ( $changes as $plugin_basename => $plugin_changes ) {
253            if ( isset( $all_plugins[ $plugin_basename ] ) ) {
254                $all_plugins[ $plugin_basename ] = array_merge( $all_plugins[ $plugin_basename ], $plugin_changes );
255            }
256        }
257
258        return $all_plugins;
259    }
260}