Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.60% covered (warning)
88.60%
101 / 114
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateRemoteConfigRequest
88.60% covered (warning)
88.60%
101 / 114
75.00% covered (warning)
75.00%
3 / 4
11.18
0.00% covered (danger)
0.00%
0 / 1
 rules
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
1 / 1
1
 withValidator
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 validateLinkedinSelectors
18.75% covered (danger)
18.75%
3 / 16
0.00% covered (danger)
0.00%
0 / 1
25.31
 validateRegexPattern
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace App\Http\Requests\v2\RemoteConfig;
4
5use App\Http\Models\RemoteConfig;
6use App\Http\Requests\v2\Parameter\Concerns\AuthorizesVengresoAdmin;
7use Illuminate\Foundation\Http\FormRequest;
8
9/**
10 * Request for full update (PUT) of the remote config.
11 *
12 * Validates all 8 config sections. Each section is optional (sometimes|nullable)
13 * but when present must conform to its specific structure.
14 *
15 * @property array|null $linkedin_selectors LinkedIn DOM selectors (18 required top-level keys)
16 * @property array|null $outlook_domains Outlook domain detection (hostnames + regex_pattern)
17 * @property array|null $timing Timing constants in milliseconds
18 * @property array|null $feature_flags Feature flags for gradual rollout
19 * @property array|null $features Extension feature toggles (booleans)
20 * @property array|null $playlists Playlist config objects (title, link, description)
21 * @property array|null $blacklist_config Input blacklist rules
22 * @property array|null $ui_visibility UI element visibility toggles
23 * @property array|null $field_injection Field injection config per domain (domain, fields with selector/shadow DOM/offsets)
24 * @property array|null $force_plain_text Force plain-text paste config per domain (domain + optional field selectors with boundary conditions)
25 * @property array|null $display_strategy_grammar Display strategy config for Grammar feature (domain, fields with selector/inside_of/strategy)
26 * @property array|null $display_strategy_magic_wand Display strategy config for Magic Wand feature (domain, fields with selector/inside_of/strategy)
27 * @property array|null $display_strategy_paragraph Display strategy config for Paragraph feature (domain, fields with selector/inside_of/strategy)
28 * @property string|null $change_description Optional description of the change
29 */
30class UpdateRemoteConfigRequest extends FormRequest
31{
32    use AuthorizesVengresoAdmin;
33
34    /**
35     * Get the validation rules that apply to the request.
36     *
37     * @return array<string, mixed>
38     */
39    public function rules(): array
40    {
41        return [
42            // LinkedIn Selectors
43            'linkedin_selectors' => ['sometimes', 'nullable', 'array'],
44            'linkedin_selectors.*' => ['sometimes'],
45
46            // Outlook Domains
47            'outlook_domains' => ['sometimes', 'nullable', 'array'],
48            'outlook_domains.hostnames' => ['required_with:outlook_domains', 'array', 'min:1'],
49            'outlook_domains.hostnames.*' => ['string'],
50            'outlook_domains.regex_pattern' => ['required_with:outlook_domains', 'string'],
51
52            // Timing
53            'timing' => ['sometimes', 'nullable', 'array'],
54            'timing.default_clear_buffer_timeout' => ['sometimes', 'integer', 'min:100', 'max:10000'],
55            'timing.cleanup_interval' => ['sometimes', 'integer', 'min:5000', 'max:300000'],
56            'timing.cache_ttl' => ['sometimes', 'integer', 'min:1000', 'max:3600000'],
57
58            // Feature Flags
59            'feature_flags' => ['sometimes', 'nullable', 'array'],
60            'feature_flags.disable_old_linkedin_plugin' => ['sometimes', 'boolean'],
61
62            // Features
63            'features' => ['sometimes', 'nullable', 'array'],
64            'features.*' => ['boolean'],
65
66            // Playlists
67            'playlists' => ['sometimes', 'nullable', 'array'],
68            'playlists.*' => ['array'],
69            'playlists.*.title' => ['required', 'string'],
70            'playlists.*.link' => ['sometimes', 'string'],
71            'playlists.*.description' => ['sometimes', 'string'],
72
73            // Blacklist Config
74            'blacklist_config' => ['sometimes', 'nullable', 'array'],
75            'blacklist_config.ignored_input_types' => ['sometimes', 'array'],
76            'blacklist_config.ignored_input_types.*' => ['string'],
77            'blacklist_config.classes' => ['sometimes', 'array'],
78            'blacklist_config.classes.*' => ['string'],
79            'blacklist_config.ids' => ['sometimes', 'array'],
80            'blacklist_config.ids.*' => ['string'],
81            'blacklist_config.attributes' => ['sometimes', 'array'],
82            'blacklist_config.attributes.*' => ['string'],
83            'blacklist_config.domain_rules' => ['sometimes', 'array'],
84
85            // UI Visibility
86            'ui_visibility' => ['sometimes', 'nullable', 'array'],
87            'ui_visibility.show_rewrite' => ['sometimes', 'boolean'],
88            'ui_visibility.show_write_upgrade_button' => ['sometimes', 'boolean'],
89            'ui_visibility.show_upgrade_button' => ['sometimes', 'boolean'],
90            'ui_visibility.show_write_message' => ['sometimes', 'string'],
91
92            // Field Injection
93            'field_injection' => ['sometimes', 'nullable', 'array'],
94            'field_injection.*' => ['array'],
95            'field_injection.*.domain' => ['required', 'string'],
96            'field_injection.*.fields' => ['required', 'array', 'min:1'],
97            'field_injection.*.fields.*' => ['array'],
98            'field_injection.*.fields.*.selector' => ['required', 'string'],
99            'field_injection.*.fields.*.is_inside_shadow_dom' => ['sometimes', 'boolean'],
100            'field_injection.*.fields.*.inside_of' => ['sometimes', 'string'],
101            'field_injection.*.fields.*.not_inside_of' => ['sometimes', 'string'],
102            'field_injection.*.fields.*.has_parent_selector' => ['sometimes', 'string'],
103            'field_injection.*.fields.*.icon_offset_bottom' => ['sometimes', 'integer'],
104            'field_injection.*.fields.*.icon_offset_right' => ['sometimes', 'integer'],
105            'field_injection.*.fields.*.icon_height' => ['sometimes', 'integer'],
106            'field_injection.*.fields.*.icon_width' => ['sometimes', 'integer'],
107
108            // Force Plain Text
109            'force_plain_text' => ['sometimes', 'nullable', 'array'],
110            'force_plain_text.*' => ['array'],
111            'force_plain_text.*.domain' => ['required', 'string'],
112            'force_plain_text.*.fields' => ['sometimes', 'array'],
113            'force_plain_text.*.fields.*' => ['array'],
114            'force_plain_text.*.fields.*.selector' => ['required', 'string'],
115            'force_plain_text.*.fields.*.inside_of' => ['sometimes', 'string'],
116            'force_plain_text.*.fields.*.not_inside_of' => ['sometimes', 'string'],
117
118            // Display Strategy Grammar
119            'display_strategy_grammar' => ['sometimes', 'nullable', 'array'],
120            'display_strategy_grammar.*' => ['array'],
121            'display_strategy_grammar.*.domain' => ['required', 'string'],
122            'display_strategy_grammar.*.fields' => ['required', 'array', 'min:1'],
123            'display_strategy_grammar.*.fields.*' => ['array'],
124            'display_strategy_grammar.*.fields.*.selector' => ['required', 'string'],
125            'display_strategy_grammar.*.fields.*.inside_of' => ['sometimes', 'string'],
126            'display_strategy_grammar.*.fields.*.not_inside_of' => ['sometimes', 'string'],
127            'display_strategy_grammar.*.fields.*.strategy' => ['required', 'string', 'in:mirror,mainPage,dialog'],
128
129            // Display Strategy Magic Wand
130            'display_strategy_magic_wand' => ['sometimes', 'nullable', 'array'],
131            'display_strategy_magic_wand.*' => ['array'],
132            'display_strategy_magic_wand.*.domain' => ['required', 'string'],
133            'display_strategy_magic_wand.*.fields' => ['required', 'array', 'min:1'],
134            'display_strategy_magic_wand.*.fields.*' => ['array'],
135            'display_strategy_magic_wand.*.fields.*.selector' => ['required', 'string'],
136            'display_strategy_magic_wand.*.fields.*.inside_of' => ['sometimes', 'string'],
137            'display_strategy_magic_wand.*.fields.*.not_inside_of' => ['sometimes', 'string'],
138            'display_strategy_magic_wand.*.fields.*.strategy' => ['required', 'string', 'in:mirror,mainPage,dialog'],
139
140            // Display Strategy Paragraph
141            'display_strategy_paragraph' => ['sometimes', 'nullable', 'array'],
142            'display_strategy_paragraph.*' => ['array'],
143            'display_strategy_paragraph.*.domain' => ['required', 'string'],
144            'display_strategy_paragraph.*.fields' => ['required', 'array', 'min:1'],
145            'display_strategy_paragraph.*.fields.*' => ['array'],
146            'display_strategy_paragraph.*.fields.*.selector' => ['required', 'string'],
147            'display_strategy_paragraph.*.fields.*.inside_of' => ['sometimes', 'string'],
148            'display_strategy_paragraph.*.fields.*.not_inside_of' => ['sometimes', 'string'],
149            'display_strategy_paragraph.*.fields.*.strategy' => ['required', 'string', 'in:mirror,mainPage,dialog'],
150
151            // Meta
152            'change_description' => ['sometimes', 'nullable', 'string', 'max:500'],
153        ];
154    }
155
156    /**
157     * Configure the validator instance.
158     *
159     * @param  \Illuminate\Validation\Validator  $validator
160     */
161    public function withValidator($validator): void
162    {
163        $validator->after(function ($validator) {
164            $this->validateLinkedinSelectors($validator);
165            $this->validateRegexPattern($validator);
166        });
167    }
168
169    /**
170     * Validate linkedin_selectors has required top-level keys and detection sub-keys.
171     *
172     * @param  \Illuminate\Validation\Validator  $validator
173     */
174    private function validateLinkedinSelectors($validator): void
175    {
176        $selectors = $this->input('linkedin_selectors');
177
178        if (! is_array($selectors)) {
179            return;
180        }
181
182        $missingKeys = array_diff(RemoteConfig::LINKEDIN_SELECTOR_KEYS, array_keys($selectors));
183        if (! empty($missingKeys)) {
184            $validator->errors()->add(
185                'linkedin_selectors',
186                'Missing required keys: '.implode(', ', $missingKeys)
187            );
188        }
189
190        if (isset($selectors['detection']) && is_array($selectors['detection'])) {
191            $missingDetection = array_diff(RemoteConfig::LINKEDIN_DETECTION_KEYS, array_keys($selectors['detection']));
192            if (! empty($missingDetection)) {
193                $validator->errors()->add(
194                    'linkedin_selectors.detection',
195                    'Missing required detection keys: '.implode(', ', $missingDetection)
196                );
197            }
198        }
199    }
200
201    /**
202     * Validate regex_pattern is a valid PCRE pattern.
203     *
204     * @param  \Illuminate\Validation\Validator  $validator
205     */
206    private function validateRegexPattern($validator): void
207    {
208        $pattern = $this->input('outlook_domains.regex_pattern');
209
210        if (! is_string($pattern)) {
211            return;
212        }
213
214        if (@preg_match('/'.$pattern.'/', '') === false) {
215            $validator->errors()->add(
216                'outlook_domains.regex_pattern',
217                'The regex pattern is not a valid PCRE pattern.'
218            );
219        }
220    }
221}