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
← All changes | classes/Admin/SettingsSchema.php +93 -9 4.1.14.2.1 View file →
@@ -25,15 +25,31 @@
25 25
26 26 /**
27 27 * Options that exist in the stored array but are never offered for editing.
28 28 *
29 - * 'version' and 'encryption_password' are written by the plugin itself;
29 + * 'version' and the four secret-related keys are written by the plugin
30 + * itself. Three of those are key material -- the retired image secret opens
31 + * every token made before the last rotation -- and none of them may ever be
32 + * settable from a form. Listing them here changes nothing on its own,
33 + * because sanitize() already drops anything without a field definition;
34 + * they are named so that the list matches its own description, which is how
35 + * the shortcode equivalent came to be missing them for a release;
30 36 * 'echo' is a leftover that no code path reads any more;
31 37 * 'use_secure_encryption' is derived from 'encryption_mode' on save, see
32 38 * deriveImpliedValues() -- two switches for one decision only ever
33 39 * contradict each other.
34 40 */
35 - private const INTERNAL_KEYS = ['version', 'encryption_password', 'echo', 'use_secure_encryption'];
41 + private const INTERNAL_KEYS = [
42 + 'version',
43 + 'encryption_password',
44 + 'image_token_secret',
45 + 'image_token_secret_previous',
46 + 'image_token_secret_previous_until',
47 + 'secrets_rotated_at',
48 + 'review_prompt_due',
49 + 'echo',
50 + 'use_secure_encryption',
51 + ];
36 52
37 53 /**
38 54 * The tabs, in the order they appear.
39 55 *
@@ -132,9 +148,9 @@
132 148 'default' => true,
133 149 'tab' => self::TAB_PROTECTION,
134 150 'section' => __('Recognition', 'cryptx'),
135 151 'label' => __('Turn plain addresses into links', 'cryptx'),
136 - 'help' => __('With this on, an address written as plain text becomes a working contact link and is protected. With it off, CryptX only protects addresses that were already linked -- a plain one stays readable for spam bots.', 'cryptx'),
152 + 'help' => __('With this on, an address written as plain text becomes a working contact link and is protected. With it off, CryptX only protects addresses that were already linked -- a plain one stays readable for spam bots. Widgets are the exception, as long as they are switched on above: there a plain address is linked and protected either way. That is long-standing behaviour and errs towards protection, so it has been left as it is rather than changed under people who rely on it.', 'cryptx'),
137 153 ],
138 154 'java' => [
139 155 'type' => 'choice',
140 156 'default' => 1,
@@ -167,9 +183,9 @@
167 183 'default' => 0,
168 184 'tab' => self::TAB_APPEARANCE,
169 185 'section' => __('What visitors see', 'cryptx'),
170 186 'label' => __('Instead of the address, show', 'cryptx'),
171 - 'help' => __('The address in the link target is always protected. This only decides what is written on the link itself.', 'cryptx'),
187 + 'help' => __('The address in the link target is always protected. This only decides what is written on the link itself. Note that both picture options trade accessibility for protection: a visitor using a screen reader or a text browser cannot read the address at all, only follow the link. The picture drawn by CryptX is made on your own server, and since 4.2.0 it is fetched under a web address that gives nothing away -- before that the email address stood in it, and so in your access log.', 'cryptx'),
172 188 'choices' => [
173 189 ['value' => 0, 'label' => __('The address with @ and . replaced', 'cryptx')],
174 190 ['value' => 1, 'label' => __('A text of your choice', 'cryptx')],
175 191 ['value' => 2, 'label' => __('An image from a web address', 'cryptx')],
@@ -296,8 +312,16 @@
296 312 'section' => __('Individual posts', 'cryptx'),
297 313 'label' => __('Offer a switch in the post editor', 'cryptx'),
298 314 'help' => __('Adds a "Disable CryptX for this post/page" box to the editor, which writes into the list above. Without it the list can still be edited here.', 'cryptx'),
299 315 ],
316 + 'exemptAddresses' => [
317 + 'type' => 'addresslist',
318 + 'default' => '',
319 + 'tab' => self::TAB_EXCEPTIONS,
320 + 'section' => __('Individual addresses', 'cryptx'),
321 + 'label' => __('Addresses to leave alone', 'cryptx'),
322 + 'help' => __('Separated by commas. CryptX leaves these exactly as written -- no masking, no link, no encryption -- for an address a helpdesk has to read out of the page, or one in a code example. Write "@example.com" to cover a whole domain. Two limits on purpose: the shortcode and the block say "protect this one, here" and are never overruled by this list; and in comments only whole addresses count, never the domain form, so an exempt domain cannot be used to collect what visitors leave behind. (In comments WordPress makes a link out of a bare address by itself, before CryptX sees it.)', 'cryptx'),
323 + ],
300 324 'whiteList' => [
301 325 'type' => 'string',
302 326 'default' => 'jpeg,jpg,png,gif',
303 327 'tab' => self::TAB_EXCEPTIONS,
@@ -302,9 +326,9 @@
302 326 'default' => 'jpeg,jpg,png,gif',
303 327 'tab' => self::TAB_EXCEPTIONS,
304 328 'section' => __('False positives', 'cryptx'),
305 329 'label' => __('Endings that are not addresses', 'cryptx'),
306 - 'help' => __('Anything ending in one of these is left alone. This is what keeps file names such as logo@2x.png from being treated as an email address. It cannot be used to exempt a particular address -- use the list of posts above for that.', 'cryptx'),
330 + 'help' => __('Anything ending in one of these is left alone. This is what keeps file names such as logo@2x.png from being treated as an email address. To exempt a real address, use the field above instead.', 'cryptx'),
307 331 ],
308 332 'disable_rss' => [
309 333 'type' => 'boolean',
310 334 'default' => true,
@@ -320,9 +344,9 @@
320 344 'default' => 10000,
321 345 'tab' => self::TAB_ADVANCED,
322 346 'section' => __('Encryption', 'cryptx'),
323 347 'label' => __('Key strengthening', 'cryptx'),
324 - 'help' => __('How much work goes into deriving the key. The cost is paid once per page, not per address. Higher makes life harder for anyone trying to unpick the addresses in bulk; lower renders pages faster.', 'cryptx'),
348 + 'help' => __('How much work goes into deriving the key. The cost is paid once per page, not per address. Higher makes life harder for anyone trying to unpick the addresses in bulk; lower renders pages faster. Changing it is safe: since 4.2.0 every link records what it was made with, so the ones already published keep working. Before that, changing this quietly broke all of them.', 'cryptx'),
325 349 'depends' => ['encryption_mode' => 'secure'],
326 350 'choices' => [
327 351 ['value' => 100000, 'label' => __('Thorough (100,000)', 'cryptx')],
328 352 ['value' => 10000, 'label' => __('Balanced (10,000)', 'cryptx')],
@@ -374,13 +398,17 @@
374 398
375 399 /**
376 400 * The schema as the settings screen consumes it, with runtime values filled in.
377 401 *
402 + * @param array<int, string> $without Field keys to leave out -- the network
403 + * defaults screen uses this for the two settings that mean something
404 + * different on every site.
405 + *
378 406 * @return array<string, mixed>
379 407 */
380 - public static function forClient(): array
408 + public static function forClient(array $without = []): array
381 409 {
382 - $fields = self::fields();
410 + $fields = array_diff_key(self::fields(), array_flip($without));
383 411 $out = [];
384 412 foreach ($fields as $key => $definition) {
385 413 $definition['key'] = $key;
386 414 $out[] = $definition;
@@ -385,10 +413,22 @@
385 413 $definition['key'] = $key;
386 414 $out[] = $definition;
387 415 }
388 416
417 + // A tab whose every field was left out is not shown at all: an empty
418 + // tab reads as a broken screen rather than as a deliberate omission.
419 + // No tab loses everything today -- Exceptions keeps four fields even
420 + // without the two per-site ones -- so this is for the next omission,
421 + // not for the current one.
422 + $used = array_unique(array_column($out, 'tab'));
423 +
424 + $tabs = array_values(array_filter(
425 + self::tabs(),
426 + static fn(array $tab): bool => in_array($tab['id'], $used, true)
427 + ));
428 +
389 429 return [
390 - 'tabs' => self::tabs(),
430 + 'tabs' => $tabs,
391 431 'fields' => $out,
392 432 ];
393 433 }
394 434
@@ -503,8 +543,52 @@
503 543 return absint($value);
504 544
505 545 case 'htmlid':
506 546 return sanitize_html_class((string) $value);
547 +
548 + case 'addresslist':
549 + // Two forms, and both have to survive: a whole address, and
550 + // "@example.com" for a domain. sanitize_email() would eat the
551 + // second one -- it needs a local part -- so the two are
552 + // cleaned apart.
553 + $entries = [];
554 +
555 + foreach (explode(',', (string) $value) as $entry) {
556 + $entry = strtolower(trim($entry));
557 +
558 + if ($entry === '') {
559 + continue;
560 + }
561 +
562 + if (str_starts_with($entry, '@')) {
563 + $domain = substr($entry, 1);
564 +
565 + // A domain, judged by asking whether it makes a valid
566 + // address rather than by a pattern of our own.
567 + if (is_email('user@' . $domain)) {
568 + $entries[] = '@' . $domain;
569 + }
570 +
571 + continue;
572 + }
573 +
574 + // Rejected, not repaired. sanitize_email() strips whatever
575 + // it dislikes and hands back the rest, so a typo becomes a
576 + // different, perfectly valid address: "info@exam ple.com"
577 + // turns into "info@example.com", and a domain typed with an
578 + // umlaut loses the letter rather than the entry.
579 + // Silently exempting an address nobody
580 + // typed is the one thing this field must not do, and an
581 + // entry that vanishes is noticed -- one that changed is
582 + // not. The domain form above is already this strict.
583 + $address = sanitize_email($entry);
584 +
585 + if ($address !== '' && $address === $entry) {
586 + $entries[] = $address;
587 + }
588 + }
589 +
590 + return implode(',', array_unique($entries));
507 591
508 592 case 'idlist':
509 593 $ids = array_filter(array_map('absint', explode(',', (string) $value)));
510 594 sort($ids);