Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SSO_CLI
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 13
1980
0.00% covered (danger)
0.00%
0 / 1
 __invoke
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 build_request_params
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
156
 create_salt_nonce_and_hash
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 table
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 bold_heading
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 success
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 info
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 warning
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 error
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 colorize_log
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 new_line
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log_to_json
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 confirm
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2namespace NewfoldLabs\WP\Module\SSO;
3
4/**
5 * Class SSO_CLI
6 */
7class SSO_CLI extends \WP_CLI_Command {
8
9    /**
10     * @var string - Stored transient key used for SSO.
11     */
12    public static $transient_slug = 'sso_token';
13
14    /**
15     * @var string - Nonce validation key.
16     */
17    public static $nonce_slug = 'newfold-sso';
18
19    /**
20     * @var string - Nonce action key.
21     */
22    public static $nonce_action = 'sso-check';
23
24    /**
25     * @var int Time for nonce token to be valid.
26     */
27    public $expiry_min = 3;
28
29    /**
30     * @var string - Cryptographic salt.
31     */
32    protected $salt;
33
34    /**
35     * @var string - Validation nonce.
36     */
37    protected $nonce;
38
39    /**
40     * @var string - Cryptographic hash.
41     */
42    protected $hash;
43
44    /**
45     * Single Sign On via WP-CLI.
46     *
47     * @param  null  $args Unused.
48     * @param  array $assoc_args Additional args to define which user or role to login as.
49     */
50    public function __invoke( $args, $assoc_args ) {
51
52        $this->create_salt_nonce_and_hash();
53
54        $params = $this->build_request_params(
55            $assoc_args,
56            array(
57                'action' => static::$nonce_action,
58                'salt'   => $this->salt,
59                'nonce'  => $this->nonce,
60            )
61        );
62
63        set_transient(
64            static::$transient_slug,
65            $this->hash,
66            MINUTE_IN_SECONDS * $this->expiry_min
67        );
68
69        $link = add_query_arg( $params, admin_url( 'admin-ajax.php' ) );
70
71        if ( isset( $assoc_args['url-only'] ) ) {
72            \WP_CLI::log( $link );
73        } else {
74            /* Translators: %d number */
75            $this->success( sprintf( __( 'Single-use login link valid for %d minutes', 'wp-module-sso' ), $this->expiry_min ) );
76            $this->colorize_log( $link, 'underline' );
77        }
78    }
79
80    /**
81     * Build request parameters for SSO URL.
82     *
83     * @param array $assoc_args
84     * @param array $params
85     *
86     * @return array
87     */
88    protected function build_request_params( $assoc_args, $params ) {
89        if ( ! empty( $assoc_args ) ) {
90            if ( isset( $assoc_args['role'] ) ) {
91                $user = get_users(
92                    array(
93                        'role'   => 'administrator',
94                        'number' => 1,
95                    )
96                );
97                if ( is_array( $user ) && is_a( $user[0], 'WP_User' ) ) {
98                    $params['user'] = $user[0]->ID;
99                }
100            }
101
102            if ( isset( $assoc_args['email'] ) ) {
103                $user = get_user_by( 'email', $assoc_args['email'] );
104                if ( is_a( $user, 'WP_User' ) ) {
105                    $params['user'] = $user->ID;
106                }
107            }
108
109            if ( isset( $assoc_args['username'] ) ) {
110                $user = get_user_by( 'login', $assoc_args['username'] );
111                if ( is_a( $user, 'WP_User' ) ) {
112                    $params['user'] = $user->ID;
113                }
114            }
115
116            if ( isset( $assoc_args['id'] ) ) {
117                $user = get_user_by( 'ID', $assoc_args['id'] );
118                if ( is_a( $user, 'WP_User' ) ) {
119                    $params['user'] = $user->ID;
120                }
121            }
122
123            if ( isset( $assoc_args['min'] ) ) {
124                $this->expiry_min = (int) $assoc_args['min'];
125            }
126        }
127
128        return $params;
129    }
130
131    /**
132     * Setup cryptographic strings for SSO link.
133     */
134    protected function create_salt_nonce_and_hash() {
135        $this->salt  = wp_generate_password( 32, false );
136        $this->nonce = wp_create_nonce( static::$nonce_slug );
137        $this->hash  = substr(
138            base64_encode( hash( 'sha256', $this->nonce . $this->salt, false ) ),
139            0,
140            64
141        );
142    }
143
144
145    /**
146     * Helper to format data into tables.
147     *
148     * By default, the method creates simple $key => $value tables.
149     * Set $type to 'adv' and the table inherits keys from $data. DATA MUST BE UNIFORM & MATCH FIRST ROW.
150     *
151     * 1. Provide $data as an array or object
152     * 2. Provide $keys as two strings -- by default 'DETAIL' and 'VALUE' are used.
153     * 3. Prints ASCII Table
154     *
155     * @param array  $data
156     * @param array  $keys
157     * @param string $type
158     */
159    protected function table( $data, $keys = array( 'DETAIL', 'VALUE' ), $type = 'simple' ) {
160        if ( empty( $data ) ) {
161            return;
162        }
163
164        if ( 'adv' === $type ) {
165            $items = $data;
166            $keys  = array_keys( array_shift( $data ) );
167        } else {
168            $items = array();
169            foreach ( $data as $detail => $value ) {
170                $items[] = array(
171                    $keys[0] => $detail,
172                    $keys[1] => $value,
173                );
174            }
175        }
176
177        Utils\format_items( 'table', $items, $keys );
178    }
179
180    /**
181     * Creates Heading with Blue background and Grey text.
182     *
183     * @param string $message
184     * @param string $emoji
185     */
186    protected function bold_heading( $message, $emoji = '' ) {
187        $this->colorize_log( $message, '4', 'W', $emoji );
188    }
189
190    /**
191     * Formatted Success message.
192     *
193     * @param string $message
194     */
195    protected function success( $message, $silent = false ) {
196        $pre_ = $silent ? '' : __( 'Success: ', 'wp-module-sso' );
197        $this->colorize_log( $pre_ . $message, '2', 'k', '✅' );
198    }
199
200    /**
201     * Formatted Info message.
202     *
203     * @param string $message
204     */
205    protected function info( $message ) {
206        $this->colorize_log( $message, '4', 'W', 'ℹ️' );
207    }
208
209    /**
210     * Formatted Warning message.
211     *
212     * @param string $message
213     */
214    protected function warning( $message ) {
215        $this->colorize_log( $message, '3', 'k', '⚠️' );
216    }
217
218    /**
219     * Formatted Error message. Halts by default.
220     *
221     * @param string $message
222     * @param bool   $silent
223     * @param bool   $halt
224     * @param int    $code
225     *
226     * @throws \WP_CLI\ExitException
227     */
228    protected function error( $message, $silent = false, $halt = true, $code = 400 ) {
229        $pre_ = $silent ? '' : __( 'Error: ', 'wp-module-sso' );
230        $this->colorize_log( $pre_ . $message, '1', 'W', '🛑️' );
231        if ( $halt ) {
232            WP_CLI::halt( $code );
233        }
234    }
235
236    /**
237     * Formatting helper for colorized messages.
238     *
239     * @param string $message
240     * @param string $background
241     * @param string $text_color
242     * @param string $emoji_prefix
243     */
244    protected function colorize_log( $message = '', $background = '', $text_color = '%_', $emoji_prefix = '' ) {
245        if ( ! empty( $background ) ) {
246            $background = '%' . $background;
247        }
248
249        if ( ! empty( $text_color ) && false === strpos( $text_color, '%' ) ) {
250            $text_color = '%' . $text_color;
251        }
252
253        if ( ! empty( $emoji_prefix ) ) {
254            $message = $emoji_prefix . '  ' . $message;
255        }
256
257        \WP_CLI::log( \WP_CLI::colorize( $background . $text_color . $message . '%n' ) );
258    }
259
260    /**
261     * Empty linebreak
262     */
263    protected function new_line() {
264        \WP_CLI::log( __return_empty_string() );
265    }
266
267    /**
268     * Helper function for returning clean JSON response.
269     *
270     * @param array|string $data - Provide well-formed array or existing JSON string.
271     */
272    protected function log_to_json( $data ) {
273        if ( is_array( $data ) ) {
274            \WP_CLI::log( json_encode( $data ) );
275        } elseif ( is_array( json_decode( $data, true ) ) ) {
276            \WP_CLI::log( $data );
277        } else {
278            $this->error( __( 'Provided $data wasn\'t valid array or JSON string.', 'wp-module-sso' ) );
279        }
280    }
281
282    /**
283     * Formatted Confirm Dialog. A 'n' response breaks the thread.
284     *
285     * @param string $question
286     * @param string $type
287     *
288     * @throws \WP_CLI\ExitException
289     */
290    protected function confirm( $question, $type = 'normal' ) {
291        switch ( $type ) {
292            case 'omg':
293                \WP_CLI::confirm( $this->warning( '☢ 🙊 🙈 🙊 ☢️  ' . $question ) );
294                break;
295            case 'red':
296                \WP_CLI::confirm( $this->error( $question, true ) );
297                break;
298            case 'yellow':
299                \WP_CLI::confirm( $this->warning( $question ) );
300                break;
301            case 'green':
302                \WP_CLI::confirm( $this->success( $question ) );
303                break;
304            case 'underline':
305                \WP_CLI::confirm( $this->colorize_log( $question, '', 'U' ) );
306                break;
307            case 'normal':
308            default:
309                \WP_CLI::confirm( $question );
310                break;
311        }
312    }
313}