Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.16% covered (warning)
72.16%
70 / 97
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Brute_Force_Protection_Shared_Functions
72.16% covered (warning)
72.16%
70 / 97
33.33% covered (danger)
33.33%
2 / 6
77.25
0.00% covered (danger)
0.00%
0 / 1
 format_whitelist
n/a
0 / 0
n/a
0 / 0
1
 format_allow_list
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
8.83
 get_local_whitelist
n/a
0 / 0
n/a
0 / 0
1
 get_local_allow_list
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
3.11
 get_global_whitelist
n/a
0 / 0
n/a
0 / 0
1
 get_global_allow_list
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 create_ip_object
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 ip_object_to_string
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 save_whitelist
n/a
0 / 0
n/a
0 / 0
1
 save_allow_list
68.18% covered (warning)
68.18%
30 / 44
0.00% covered (danger)
0.00%
0 / 1
32.89
1<?php
2/**
3 * Class for functions shared by the Brute force protection feature and its related json-endpoints
4 *
5 * @package automattic/jetpack-waf
6 */
7
8namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
9
10use Automattic\Jetpack\IP\Utils as IP_Utils;
11use Automattic\Jetpack\Waf\Waf_Rules_Manager;
12use WP_Error;
13
14/**
15 * Shared Functions class.
16 */
17class Brute_Force_Protection_Shared_Functions {
18    /**
19     * Returns an array of IP objects that will never be blocked by the Brute force protection feature
20     *
21     * @deprecated 0.11.0 Use format_allow_list()
22     */
23    public static function format_whitelist() {
24        _deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::format_allow_list' );
25        return self::format_allow_list();
26    }
27
28    /**
29     * Returns an array of IP objects that will never be blocked by the Brute force protection feature
30     *
31     * The array is segmented into a local allow list which applies only to the current site
32     * and a global allow list which, for multisite installs, applies to the entire networko
33     *
34     * @return array
35     */
36    public static function format_allow_list() {
37        $local_allow_list = self::get_local_allow_list();
38        $formatted        = array(
39            'local' => array(),
40        );
41        foreach ( $local_allow_list as $item ) {
42            $formatted['local'][] = self::ip_object_to_string( $item, ' - ' );
43        }
44        if ( is_multisite() && current_user_can( 'manage_network' ) ) {
45            $formatted['global'] = array();
46            $global_allow_list   = self::get_global_allow_list();
47            if ( false === $global_allow_list ) {
48                // If the global allow list has never been set, check for a legacy option set prior to 3.6.
49                $global_allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
50            }
51            foreach ( $global_allow_list as $item ) {
52                $formatted['global'][] = self::ip_object_to_string( $item, ' - ' );
53            }
54        }
55        return $formatted;
56    }
57
58    /**
59     * Gets the local Brute force protection allow list.
60     *
61     * @deprecated 0.11.0 Use get_local_allow_list()
62     */
63    public static function get_local_whitelist() {
64        _deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::get_local_allow_list' );
65        return self::get_local_allow_list();
66    }
67
68    /**
69     * Gets the local Brute force protection allow list.
70     *
71     * The 'local' part of the allow list only really applies to multisite installs,
72     * which can have a network wide allow list, as well as a local list that applies
73     * only to the current site. On single site installs, there will only be a local
74     * allow list.
75     *
76     * @return array A list of IP Address objects or an empty array
77     */
78    public static function get_local_allow_list() {
79        $allow_list = get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME );
80        if ( false === $allow_list ) {
81            // The local allow list has never been set.
82            if ( is_multisite() ) {
83                // On a multisite, we can check for a legacy site_option that existed prior to v 3.6, or default to an empty array.
84                $allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
85            } else {
86                // On a single site, we can just use an empty array.
87                $allow_list = array();
88            }
89        } else {
90            $allow_list = IP_Utils::get_ip_addresses_from_string( $allow_list );
91            $allow_list = array_map(
92                function ( $ip_address ) {
93                    return self::create_ip_object( $ip_address );
94                },
95                $allow_list
96            );
97        }
98        return $allow_list;
99    }
100
101    /**
102     * Get the global, network-wide allow list.
103     *
104     * @deprecated 0.11.0 Use get_global_allow_list()
105     */
106    public static function get_global_whitelist() {
107        _deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::get_global_allow_list' );
108        return self::get_global_allow_list();
109    }
110
111    /**
112     * Get the global, network-wide allow list.
113     *
114     * It will revert to the legacy site_option if jetpack_protect_global_whitelist has never been set.
115     *
116     * @return array
117     */
118    public static function get_global_allow_list() {
119        $allow_list = get_site_option( 'jetpack_protect_global_whitelist' );
120        if ( false === $allow_list ) {
121            // The global allow list has never been set. Check for legacy site_option, or default to an empty array.
122            $allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
123        }
124        return $allow_list;
125    }
126
127    /**
128     * Convert a string into an IP Address object.
129     *
130     * @param string $ip_address The IP Address to convert.
131     * @return object An IP Address object.
132     */
133    private static function create_ip_object( $ip_address ) {
134        // Hyphenated range notation.
135        if ( strpos( $ip_address, '-' ) ) {
136            $ip_range_parts = explode( '-', $ip_address );
137            return (object) array(
138                'range'      => true,
139                'range_low'  => trim( $ip_range_parts[0] ),
140                'range_high' => trim( $ip_range_parts[1] ),
141            );
142        }
143
144        // CIDR notation.
145        if ( strpos( $ip_address, '/' ) !== false ) {
146            return (object) array(
147                'range'      => true,
148                'range_low'  => $ip_address,
149                'range_high' => null,
150            );
151        }
152
153        // Single IP Address.
154        return (object) array(
155            'range'      => false,
156            'ip_address' => $ip_address,
157        );
158    }
159
160    /**
161     * Render an IP Address object as a string.
162     *
163     * @param object $item      An IP Address object.
164     * @param string $separator Placed between the bounds of a hyphenated range.
165     * @return string
166     */
167    private static function ip_object_to_string( $item, $separator ) {
168        if ( ! $item->range ) {
169            return $item->ip_address;
170        }
171        if ( ! isset( $item->range_high ) ) {
172            return $item->range_low;
173        }
174        return $item->range_low . $separator . $item->range_high;
175    }
176
177    /**
178     * Save IP allow list.
179     *
180     * @deprecated 0.11.0 Use save_allow_list()
181     *
182     * @param mixed $allow_list IP allow list.
183     * @param bool  $global (default: false) Global.
184     */
185    public static function save_whitelist( $allow_list, $global = false ) {
186        _deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::save_allow_list' );
187        return self::save_allow_list( $allow_list, $global );
188    }
189
190    /**
191     * Save IP allow list.
192     *
193     * @access public
194     * @param mixed $allow_list IP allow list.
195     * @param bool  $global (default: false) Global.
196     * @return bool
197     */
198    public static function save_allow_list( $allow_list, $global = false ) {
199        $allow_list_error = false;
200        $new_items        = array();
201        if ( ! is_array( $allow_list ) ) {
202            return new WP_Error( 'invalid_parameters', __( 'Expecting an array', 'jetpack-waf' ) );
203        }
204        if ( $global && ! is_multisite() ) {
205            return new WP_Error( 'invalid_parameters', __( 'Cannot use global flag on non-multisites', 'jetpack-waf' ) );
206        }
207        if ( $global && ! current_user_can( 'manage_network' ) ) {
208            return new WP_Error( 'permission_denied', __( 'Only super admins can edit the global allow list', 'jetpack-waf' ) );
209        }
210        // Validate each item.
211        foreach ( $allow_list as $item ) {
212            $item = trim( $item );
213            if ( empty( $item ) ) {
214                continue;
215            }
216            $new_item = self::create_ip_object( $item );
217            if ( $new_item->range && null === $new_item->range_high ) {
218                if ( ! IP_Utils::validate_cidr( $new_item->range_low ) ) {
219                    $allow_list_error = true;
220                    break;
221                }
222            } elseif ( $new_item->range ) {
223                if ( ! filter_var( $new_item->range_low, FILTER_VALIDATE_IP ) || ! filter_var( $new_item->range_high, FILTER_VALIDATE_IP ) ) {
224                    $allow_list_error = true;
225                    break;
226                }
227                if ( ! IP_Utils::convert_ip_address( $new_item->range_low ) || ! IP_Utils::convert_ip_address( $new_item->range_high ) ) {
228                    $allow_list_error = true;
229                    break;
230                }
231            } else {
232                if ( ! filter_var( $new_item->ip_address, FILTER_VALIDATE_IP ) ) {
233                    $allow_list_error = true;
234                    break;
235                }
236                if ( ! IP_Utils::convert_ip_address( $new_item->ip_address ) ) {
237                    $allow_list_error = true;
238                    break;
239                }
240            }
241            $new_items[] = $new_item;
242        } // End item loop.
243        if ( ! empty( $allow_list_error ) ) {
244            return new WP_Error( 'invalid_ip', __( 'One of your IP addresses was not valid.', 'jetpack-waf' ) );
245        }
246        if ( $global ) {
247            update_site_option( 'jetpack_protect_global_whitelist', $new_items );
248            // Once a user has saved their global allow list, we can permanently remove the legacy option.
249            delete_site_option( 'jetpack_protect_whitelist' );
250        } else {
251            $new_items = array_map(
252                function ( $item ) {
253                    return self::ip_object_to_string( $item, '-' );
254                },
255                $new_items
256            );
257            update_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, implode( ' ', $new_items ) );
258        }
259        return true;
260    }
261}