Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
SSO_Hosting_Login
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_config
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 enqueue_styles
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 render
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 allowed_svg_tags
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace NewfoldLabs\WP\Module\SSO;
4
5/**
6 * Renders an optional "Login with <Host>" button on the wp-login.php screen.
7 *
8 * Brand-agnostic. The button appears only when a consumer (typically a brand
9 * plugin) populates the `newfold/sso/hosting_login` filter. The destination
10 * is the host customer portal, where the user can use the SSO magic-link
11 * flow (handled elsewhere in this module) to return to their WordPress site.
12 *
13 * Filter shape:
14 *   array(
15 *     'enabled'  => bool,    // default false
16 *     'url'      => string,  // required when enabled
17 *     'label'    => string,  // required when enabled
18 *     'icon_svg'     => string,  // optional inline <svg>; use fill="currentColor" so it tints with the button text color
19 *     'new_tab'      => bool,    // default false
20 *     'accent_color' => string,  // optional CSS color used for button background, border, and hover/focus states
21 *   )
22 *
23 * Markup lives in `includes/views/hosting-login.php`; styles in
24 * `assets/css/hosting-login.css`.
25 */
26class SSO_Hosting_Login {
27
28    const FILTER       = 'newfold/sso/hosting_login';
29    const STYLE_HANDLE = 'nfd-sso-hosting-login';
30
31    /**
32     * Register the WordPress hooks that enqueue the stylesheet and render the
33     * button on wp-login.php.
34     */
35    public function __construct() {
36        add_action( 'login_enqueue_scripts', array( $this, 'enqueue_styles' ) );
37        // Late priority so our markup renders after any other login_form callbacks
38        // (e.g. external SSO providers). Combined with the high `order` value in
39        // our CSS, this anchors us at the bottom of the form regardless of what
40        // else is hooking in — without us needing to know about those plugins.
41        add_action( 'login_form', array( $this, 'render' ), PHP_INT_MAX );
42    }
43
44    /**
45     * Resolve the runtime config. Returns null if disabled or incomplete.
46     *
47     * @return array|null
48     */
49    protected function get_config() {
50        $defaults = array(
51            'enabled'      => false,
52            'url'          => '',
53            'label'        => '',
54            'icon_svg'     => '',
55            'new_tab'      => false,
56            'accent_color' => '',
57        );
58
59        $config = (array) apply_filters( self::FILTER, $defaults );
60        $config = array_merge( $defaults, $config );
61
62        if ( empty( $config['enabled'] ) || empty( $config['url'] ) || empty( $config['label'] ) ) {
63            return null;
64        }
65
66        return $config;
67    }
68
69    /**
70     * Enqueue the button stylesheet on wp-login.php. Skipped when the filter
71     * is unset or the resolved config is incomplete.
72     */
73    public function enqueue_styles() {
74        if ( null === $this->get_config() ) {
75            return;
76        }
77
78        wp_enqueue_style(
79            self::STYLE_HANDLE,
80            NFD_SSO_URL . '/assets/css/hosting-login.css',
81            array( 'login' ),
82            defined( 'NFD_SSO_VERSION' ) ? NFD_SSO_VERSION : false
83        );
84    }
85
86    /**
87     * Render the button markup. No-op when the filter is unset or the resolved
88     * config is missing required fields.
89     */
90    public function render() {
91        $config = $this->get_config();
92        if ( null === $config ) {
93            return;
94        }
95
96        $icon_html = ! empty( $config['icon_svg'] )
97            ? sprintf(
98                '<span class="nfd-sso-hosting-login__icon" aria-hidden="true">%s</span>',
99                wp_kses( $config['icon_svg'], self::allowed_svg_tags() )
100            )
101            : '';
102
103        require NFD_SSO_DIR . '/includes/views/hosting-login.php';
104    }
105
106    /**
107     * Allowed SVG tags/attributes for the icon. Broad enough to accommodate
108     * brand marks that use stroke, transforms, or text — defensive sanitation
109     * against scripts and unknown tags, not a strict shape contract.
110     *
111     * @return array
112     */
113    protected static function allowed_svg_tags() {
114        return array(
115            'svg'    => array(
116                'class'        => true,
117                'fill'         => true,
118                'height'       => true,
119                'stroke'       => true,
120                'stroke-width' => true,
121                'viewbox'      => true,
122                'width'        => true,
123                'xmlns'        => true,
124            ),
125            'g'      => array(
126                'fill'              => true,
127                'stroke'            => true,
128                'stroke-miterlimit' => true,
129                'stroke-width'      => true,
130                'transform'         => true,
131            ),
132            'path'   => array(
133                'd'               => true,
134                'fill'            => true,
135                'opacity'         => true,
136                'stroke'          => true,
137                'stroke-linecap'  => true,
138                'stroke-linejoin' => true,
139                'stroke-width'    => true,
140                'transform'       => true,
141            ),
142            'rect'   => array(
143                'fill'      => true,
144                'height'    => true,
145                'rx'        => true,
146                'transform' => true,
147                'width'     => true,
148                'x'         => true,
149                'y'         => true,
150            ),
151            'circle' => array(
152                'cx'   => true,
153                'cy'   => true,
154                'fill' => true,
155                'r'    => true,
156            ),
157            'text'   => array(
158                'fill'        => true,
159                'font-family' => true,
160                'font-size'   => true,
161                'font-weight' => true,
162                'transform'   => true,
163                'x'           => true,
164                'y'           => true,
165            ),
166        );
167    }
168}