PluginProbe
CryptX / 4.2.1
CryptX v4.2.1
4.2.1 4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 All 93 releases
cryptx / classes / Admin / SiteHealth.php

SiteHealth.php in CryptX 4.2.1, at classes/Admin/SiteHealth.php

226 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace CryptX\Admin;
4
5 use CryptX\CryptX;
6 use CryptX\Exposure;
7
8 /**
9 * A Site Health test that answers the only question that matters.
10 *
11 * The settings screen already judges what the plugin produces -- hidden, merely
12 * encoded, or plainly readable -- but only for whoever opens the Appearance tab
13 * and looks. Site Health is where someone goes when they want to know whether
14 * their site is in order, and until now CryptX said nothing there. A plugin
15 * whose whole job is invisible needs to be able to report on itself.
16 *
17 * The test is deliberately not alarmist. Several settings weaken protection on
18 * purpose, and the defaults are among them: feeds are left alone because a feed
19 * reader runs no JavaScript. Those are reported as what they are -- a choice
20 * with a consequence -- and not as a fault.
21 *
22 * @package CryptX
23 * @since 4.2.0
24 */
25 final class SiteHealth
26 {
27 /**
28 * Hooks the test in.
29 *
30 * @return void
31 */
32 public function register(): void
33 {
34 add_filter('site_status_tests', [$this, 'addTest']);
35 }
36
37 /**
38 * Declares the test.
39 *
40 * @param array<string, mixed> $tests The tests Site Health knows about.
41 *
42 * @return array<string, mixed> The tests including ours.
43 */
44 public function addTest(array $tests): array
45 {
46 $tests['direct']['cryptx_protection'] = [
47 'label' => __('Email address protection', 'cryptx'),
48 'test' => [$this, 'run'],
49 ];
50
51 return $tests;
52 }
53
54 /**
55 * Runs the sample through the real chain and reports what came out.
56 *
57 * @return array<string, mixed> The result in the shape Site Health expects.
58 */
59 public function run(): array
60 {
61 $result = [
62 'label' => __('Email addresses are hidden from spam bots', 'cryptx'),
63 'status' => 'good',
64 'badge' => [
65 'label' => __('Security', 'cryptx'),
66 'color' => 'blue',
67 ],
68 'description' => '',
69 'actions' => sprintf(
70 '<p><a href="%s">%s</a></p>',
71 esc_url(admin_url('options-general.php?page=' . SettingsPage::MENU_SLUG)),
72 esc_html__('Review the CryptX settings', 'cryptx')
73 ),
74 'test' => 'cryptx_protection',
75 ];
76
77 $cryptx = CryptX::get_instance();
78 $options = $cryptx->loadCryptXOptionsWithDefaults();
79
80 // The real filter chain on a real sample, not a description of what it
81 // ought to do. This is the same path the front end takes.
82 //
83 // The carrier sentence is NOT translatable. A translation that loses
84 // the "%s" produces a sample without an address; PHP 8 discards the
85 // surplus argument without complaint, Exposure::of() then finds nothing
86 // and the test reports "good" for ever after. A security check that
87 // reassures because its own input went missing is worse than no check.
88 $sample = 'Write to ' . Exposure::SAMPLE_ADDRESS . ' if you have any questions.';
89
90 // Belt and braces: if the sample ever stops carrying an address, say so
91 // instead of reporting success.
92 if (Exposure::of($sample) !== Exposure::PLAIN) {
93 $result['status'] = 'recommended';
94 $result['badge']['color'] = 'blue';
95 $result['label'] = __('Email address protection could not be checked', 'cryptx');
96 $result['description'] = '<p>' . esc_html__(
97 'CryptX could not build its test address, so the check has nothing to judge. This is a fault in the plugin, not in your settings.',
98 'cryptx'
99 ) . '</p>';
100
101 return $result;
102 }
103
104 // The exemption list is deliberately switched off for the measurement.
105 // The sample lives at example.com, so a site that exempts
106 // "@example.com" -- a plausible thing to write -- would make its own
107 // sample readable and report a working installation as critical. The
108 // question here is whether the mechanism works, not whether every
109 // address on the site is covered; the exemptions are a choice, and
110 // choices are listed below rather than counted against the result.
111 $markup = $cryptx->renderPreviewMarkup(['exemptAddresses' => ''], $sample);
112
113 $exposure = Exposure::of($markup);
114 $notes = $this->settingNotes($options);
115
116 // Das Badge bleibt blau. In WordPress selbst steht es 28 von 28 Mal
117 // auf 'blue': es benennt die Kategorie ("Security"), nicht das
118 // Ergebnis. Das Ergebnis traegt das Symbol und die Einsortierung in
119 // "Kritische Probleme" bzw. "Empfohlene Verbesserungen". Ein orangenes
120 // Security-Badge daneben liest sich wie eine zweite Kategorie.
121 if ($exposure === Exposure::PLAIN) {
122 $result['status'] = 'critical';
123 $result['label'] = __('Email addresses are readable in your pages', 'cryptx');
124 $result['description'] = '<p>' . esc_html__(
125 'With the current settings an address survives in the delivered HTML exactly as it was written. Any spam bot that reads the page reads the address.',
126 'cryptx'
127 ) . '</p>';
128 } elseif ($exposure === Exposure::ENCODED) {
129 $result['status'] = 'recommended';
130 $result['label'] = __('Email addresses are only lightly disguised', 'cryptx');
131 $result['description'] = '<p>' . esc_html__(
132 'Addresses reach the page as HTML entities. That defeats a simple scanner, but any spam bot that decodes entities -- and most do -- reads them. Switching the method to JavaScript hides the address until someone clicks.',
133 'cryptx'
134 ) . '</p>';
135 } else {
136 $result['description'] = '<p>' . esc_html__(
137 'A test address was run through the same filters your pages use. Nothing resembling an address was left in the result.',
138 'cryptx'
139 ) . '</p>';
140 }
141
142 if ($notes !== []) {
143 // Listed, not counted against the result. Every one of these is a
144 // setting somebody chose, and the commonest of them -- feeds left
145 // alone -- is the default. Turning a default into a warning is how
146 // people learn to ignore warnings, so the status follows the
147 // measurement above and nothing else.
148 $result['description'] .= '<p>' . esc_html__(
149 'These settings deliberately leave some addresses unprotected:',
150 'cryptx'
151 ) . '</p><ul><li>' . implode('</li><li>', array_map('esc_html', $notes)) . '</li></ul>';
152 }
153
154 return $result;
155 }
156
157 /**
158 * The settings that knowingly leave addresses in the open.
159 *
160 * @param array<string, mixed> $options The stored options.
161 *
162 * @return array<int, string> One sentence per setting, or an empty array.
163 */
164 private function settingNotes(array $options): array
165 {
166 $notes = [];
167
168 if (!empty($options['disable_rss'])) {
169 $notes[] = __('RSS feeds are left unprotected, so addresses are readable in your feed. This is the default, because a feed reader runs no JavaScript and a protected link would be dead there.', 'cryptx');
170 }
171
172 if (empty($options['autolink'])) {
173 $notes[] = __('Plain addresses are not turned into links. They are still disguised, but they never become a working contact link.', 'cryptx');
174 }
175
176 $exempt = array_filter(array_map('trim', explode(',', (string) ($options['exemptAddresses'] ?? ''))));
177
178 if ($exempt !== []) {
179 $notes[] = sprintf(
180 /* translators: %d: number of exempt email addresses */
181 _n(
182 '%d address is on the list of addresses to leave alone, and stays readable wherever it appears.',
183 '%d addresses are on the list of addresses to leave alone, and stay readable wherever they appear.',
184 count($exempt),
185 'cryptx'
186 ),
187 count($exempt)
188 );
189 }
190
191 $excluded = array_filter(array_map('trim', explode(',', (string) ($options['excludedIDs'] ?? ''))));
192
193 if ($excluded !== []) {
194 $notes[] = sprintf(
195 /* translators: %d: number of excluded posts */
196 _n(
197 '%d post or page is excluded from CryptX. Addresses in it stay exactly as written.',
198 '%d posts or pages are excluded from CryptX. Addresses in them stay exactly as written.',
199 count($excluded),
200 'cryptx'
201 ),
202 count($excluded)
203 );
204 }
205
206 // Four whole sentences rather than one with a noun slotted in. A
207 // sentence assembled from parts survives translation into German and
208 // falls apart in any language that inflects the noun -- the i18n
209 // handbook says so, and there are only four of them.
210 $switchedOff = [
211 'the_content' => __('CryptX is switched off for posts and pages.', 'cryptx'),
212 'the_excerpt' => __('CryptX is switched off for excerpts.', 'cryptx'),
213 'comment_text' => __('CryptX is switched off for comments.', 'cryptx'),
214 'widget_text' => __('CryptX is switched off for widgets.', 'cryptx'),
215 ];
216
217 foreach ($switchedOff as $key => $sentence) {
218 if (empty($options[$key])) {
219 $notes[] = $sentence;
220 }
221 }
222
223 return $notes;
224 }
225 }
226