PluginProbe
eRecht24 Legal Texts / trunk
eRecht24 Legal Texts vtrunk
4.1.0 4.0.5 4.0.4 4.0.3 trunk 4.0.0 4.0.1 4.0.2
erecht24 / src / Settings.php

Settings.php in eRecht24 Legal Texts trunk, at src/Settings.php

806 lines 24.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Option handling and migration.
4 *
5 * @package ERecht24LegalText
6 */
7
8 namespace ERecht24LegalText;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Central access to plugin settings.
14 */
15 final class Settings {
16
17
18 public const OPTION_NAME = 'erecht24_legal_text_settings';
19
20 /**
21 * Default Google Analytics sub-settings.
22 * Mirrors the 'google_analytics' key in defaults() to avoid
23 * building the full defaults array on every GA getter call.
24 *
25 * @var array<string,mixed>
26 */
27 private const GA_DEFAULTS = array(
28 'enabled' => false,
29 'measurement' => '',
30 'usercentrics' => false,
31 'opt_out' => false,
32 );
33
34 /**
35 * Option cache.
36 *
37 * @var array<string,mixed>|null
38 */
39 private ?array $cache = null;
40
41 /**
42 * Queued log entries.
43 *
44 * @var array<int,array<string,string>>
45 */
46 private array $pending_logs = array();
47
48 /**
49 * Supported legal document type keys.
50 *
51 * Not for display — use document_labels() for translated labels.
52 */
53 public const DOCUMENT_TYPES = array( 'imprint', 'privacy_policy', 'privacy_policy_social_media' );
54
55 /**
56 * Activate plugin settings and migrate legacy values.
57 *
58 * @param bool $network_wide Whether the plugin is network activated.
59 */
60 public static function activate( bool $network_wide = false ): void {
61 if ( is_multisite() && $network_wide ) {
62 $site_ids = get_sites(
63 array(
64 'fields' => 'ids',
65 'number' => 0,
66 )
67 );
68
69 foreach ( $site_ids as $site_id ) {
70 switch_to_blog( (int) $site_id );
71 self::activate_current_site();
72 restore_current_blog();
73 }
74
75 return;
76 }
77
78 self::activate_current_site();
79 }
80
81 /**
82 * Return translated document labels.
83 *
84 * @return array<string,string>
85 */
86 public static function document_labels(): array {
87 return array(
88 'imprint' => __( 'Impressum', 'erecht24' ),
89 'privacy_policy' => __( 'Datenschutzerklärung', 'erecht24' ),
90 'privacy_policy_social_media' => __( 'Datenschutzerklärung für Social Media', 'erecht24' ),
91 );
92 }
93
94 /**
95 * Return short introductory descriptions for each document type.
96 *
97 * @return array<string,string>
98 */
99 public static function document_labels_description(): array {
100 return array(
101 'imprint' => __( 'Hinterlegen Sie hier Ihr Impressum – synchronisiert aus dem eRecht24 Projekt Manager oder als selbst gepflegter lokaler Text.', 'erecht24' ),
102 'privacy_policy' => __( 'Verwalten Sie hier Ihre Datenschutzerklärung – automatisch aktuell über den eRecht24 Projekt Manager oder manuell als lokaler Text gepflegt.', 'erecht24' ),
103 'privacy_policy_social_media' => __( 'Diese Datenschutzerklärung ist speziell für Ihre Social-Media-Auftritte gedacht und kann unabhängig von der allgemeinen Datenschutzerklärung ausgegeben werden.', 'erecht24' ),
104 );
105 }
106
107 /**
108 * Normalize API, shortcode and legacy document type names.
109 *
110 * @param string $type Raw document type.
111 */
112 public static function normalize_document_type( string $type ): string {
113 $type = sanitize_key( str_replace( '-', '_', $type ) );
114
115 $map = array(
116 'privacy' => 'privacy_policy',
117 'datenschutz' => 'privacy_policy',
118 'privacypolicy' => 'privacy_policy',
119 'privacy_policy' => 'privacy_policy',
120 'privacy_policy_social' => 'privacy_policy_social_media',
121 'privacypolicysocialmedia' => 'privacy_policy_social_media',
122 'privacy_policy_social_media' => 'privacy_policy_social_media',
123 'impressum' => 'imprint',
124 'imprint' => 'imprint',
125 );
126
127 return $map[ $type ] ?? '';
128 }
129
130 /**
131 * Convert normalized document type to eRecht24 API path fragment.
132 *
133 * @param string $type Normalized document type.
134 */
135 public static function document_type_to_api_path( string $type ): string {
136 $map = array(
137 'imprint' => 'imprint',
138 'privacy_policy' => 'privacyPolicy',
139 'privacy_policy_social_media' => 'privacyPolicySocialMedia',
140 );
141
142 return $map[ $type ] ?? '';
143 }
144
145 /**
146 * Default option value.
147 *
148 * @return array<string,mixed>
149 */
150 public static function defaults(): array {
151 $documents = array();
152
153 foreach ( self::DOCUMENT_TYPES as $type ) {
154 $documents[ $type ] = array(
155 'source' => 'remote',
156 'remote' => array(
157 'de' => '',
158 'en' => '',
159 'modified' => '',
160 ),
161 'local' => array(
162 'de' => '',
163 'en' => '',
164 'modified' => '',
165 ),
166 );
167 }
168
169 return array(
170 'api_key' => '',
171 'api_key_status' => 'missing',
172 'client_id' => 0,
173 'client_secret' => '',
174 'push_test_failed' => false,
175 'documents' => $documents,
176 'google_analytics' => array(
177 'enabled' => false,
178 'measurement' => '',
179 'usercentrics' => false,
180 'opt_out' => false,
181 ),
182 'logs' => array(),
183 'version' => ERECHT24_LEGAL_TEXT_VERSION,
184 );
185 }
186
187 /**
188 * Load all settings.
189 *
190 * @return array<string,mixed>
191 */
192 public function get_all(): array {
193 if ( null !== $this->cache ) {
194 return $this->cache;
195 }
196
197 $raw = get_option( self::OPTION_NAME, array() );
198
199 if ( ! is_array( $raw ) ) {
200 $raw = array();
201 }
202
203 $settings = array_replace_recursive( self::defaults(), $raw );
204
205 if ( '' !== (string) $settings['api_key'] && 'missing' === (string) $settings['api_key_status'] ) {
206 $settings['api_key_status'] = 'unchecked';
207 }
208
209 $this->cache = $settings;
210
211 return $this->cache;
212 }
213
214 /**
215 * Persist settings.
216 *
217 * @param array<string,mixed> $settings New settings.
218 */
219 public function update_all( array $settings ): void {
220 $settings = array_intersect_key( $settings, self::defaults() );
221 $settings['version'] = ERECHT24_LEGAL_TEXT_VERSION;
222 $merged = array_replace_recursive( self::defaults(), $settings );
223 $this->cache = $merged;
224
225 update_option( self::OPTION_NAME, $merged, false );
226 }
227
228 /**
229 * Return stored API key.
230 */
231 public function get_api_key(): string {
232 $settings = $this->get_all();
233 return self::decrypt_value( (string) $settings['api_key'] );
234 }
235
236 /**
237 * Return API key status.
238 */
239 public function get_api_key_status(): string {
240 $settings = $this->get_all();
241 $status = (string) ( $settings['api_key_status'] ?? 'missing' );
242
243 return in_array( $status, array( 'missing', 'unchecked', 'valid', 'invalid' ), true ) ? $status : 'missing';
244 }
245
246 /**
247 * Documents may only render when an API key exists and is not marked invalid.
248 */
249 public function can_render_documents(): bool {
250 return '' !== $this->get_api_key() && in_array( $this->get_api_key_status(), array( 'valid', 'unchecked' ), true );
251 }
252
253 /**
254 * Return stored API client id.
255 */
256 public function get_client_id(): int {
257 $settings = $this->get_all();
258 return absint( $settings['client_id'] );
259 }
260
261 /**
262 * Return stored push secret.
263 */
264 public function get_client_secret(): string {
265 $settings = $this->get_all();
266 return self::decrypt_value( (string) $settings['client_secret'] );
267 }
268
269 /**
270 * Whether the last Remote Push Test failed.
271 */
272 public function get_push_test_failed(): bool {
273 $settings = $this->get_all();
274 return ! empty( $settings['push_test_failed'] );
275 }
276
277 /**
278 * Record the outcome of a Remote Push Test.
279 *
280 * @param bool $failed Whether the test failed.
281 */
282 public function set_push_test_failed( bool $failed ): void {
283 $settings = $this->get_all();
284 $settings['push_test_failed'] = $failed;
285 $this->update_all( $settings );
286 }
287
288 /**
289 * Store API connection data.
290 *
291 * @param string $api_key API key.
292 * @param int $client_id API client id.
293 * @param string $client_secret Push secret.
294 */
295 public function save_api_connection( string $api_key, int $client_id = 0, string $client_secret = '' ): void {
296 $settings = $this->get_all();
297 $clean_key = self::sanitize_api_key( $api_key );
298 $settings['api_key'] = self::encrypt_value( $clean_key );
299 $settings['api_key_status'] = '' === $clean_key ? 'missing' : 'valid';
300 $settings['client_id'] = absint( $client_id );
301 $settings['client_secret'] = self::encrypt_value( preg_replace( '/[^a-zA-Z0-9\-_=+\/]/', '', $client_secret ) );
302
303 $this->update_all( $settings );
304 }
305
306 /**
307 * Remove local API connection data.
308 */
309 public function clear_api_connection(): void {
310 $settings = $this->get_all();
311 $settings['api_key'] = '';
312 $settings['api_key_status'] = 'missing';
313 $settings['client_id'] = 0;
314 $settings['client_secret'] = '';
315
316 $this->update_all( $settings );
317 update_option( 'erecht24_v3_migrated', '1', false );
318 }
319
320 /**
321 * Mark the current API key as invalid.
322 */
323 public function mark_api_key_invalid(): void {
324 $settings = $this->get_all();
325
326 if ( '' !== (string) $settings['api_key'] ) {
327 $settings['api_key_status'] = 'invalid';
328 $this->update_all( $settings );
329 }
330 }
331
332 /**
333 * Store Google Analytics settings from admin request.
334 *
335 * @param array<string,mixed> $input Raw request data.
336 */
337 public function save_google_analytics_from_request( array $input ): void {
338 $settings = $this->get_all();
339
340 $measurement = isset( $input['measurement'] ) && is_scalar( $input['measurement'] ) ? sanitize_text_field( wp_unslash( (string) $input['measurement'] ) ) : '';
341 $measurement = preg_match( '/^(G|UA)-[A-Z0-9-]+$/i', $measurement ) ? strtoupper( $measurement ) : '';
342
343 $settings['google_analytics'] = array(
344 'enabled' => ! empty( $input['enabled'] ) && '' !== $measurement,
345 'measurement' => $measurement,
346 'usercentrics' => ! empty( $input['usercentrics'] ),
347 'opt_out' => ! empty( $input['opt_out'] ),
348 );
349
350 $this->update_all( $settings );
351 }
352
353 /**
354 * Return Google Analytics settings.
355 *
356 * @return array<string,mixed>
357 */
358 public function get_google_analytics(): array {
359 $settings = $this->get_all();
360 return is_array( $settings['google_analytics'] )
361 ? array_replace( self::GA_DEFAULTS, $settings['google_analytics'] )
362 : self::GA_DEFAULTS;
363 }
364
365 /**
366 * Store an internal diagnostic log line.
367 *
368 * Messages are intentionally kept in English/technical wording, not
369 * translated: this log is exported verbatim via the Status tab so it can
370 * be pasted into a support ticket, and a language-independent log is
371 * easier to read across support staff and eRecht24 developers regardless
372 * of the site's locale.
373 *
374 * @param string $message Log message.
375 */
376 public function add_log( string $message ): void {
377 $this->pending_logs[] = array(
378 'time' => current_time( 'mysql' ),
379 'message' => sanitize_text_field( $message ),
380 );
381 }
382
383 /**
384 * Write all buffered log entries to the database in a single update.
385 *
386 * Call this at the end of any operation that may produce log entries
387 * (e.g. an API request). A shutdown hook in Plugin::init() guarantees
388 * that logs are always flushed even if the caller forgets.
389 */
390 public function flush_logs(): void {
391 if ( empty( $this->pending_logs ) ) {
392 return;
393 }
394
395 $settings = $this->get_all();
396 $logs = is_array( $settings['logs'] ?? null ) ? $settings['logs'] : array();
397 $settings['logs'] = array_slice( array_merge( $logs, $this->pending_logs ), -15 );
398 $this->pending_logs = array();
399 $this->update_all( $settings );
400 }
401
402 /**
403 * Return internal diagnostic logs.
404 *
405 * @return array<int,array<string,string>>
406 */
407 public function get_logs(): array {
408 $settings = $this->get_all();
409 return is_array( $settings['logs'] ?? null ) ? $settings['logs'] : array();
410 }
411
412 /**
413 * Store admin-edited document settings.
414 *
415 * @param array<string,mixed> $documents Raw document data.
416 */
417 public function save_documents_from_request( array $documents ): void {
418 $this->stage_documents_from_request( $documents );
419 $this->update_all( $this->cache ?? self::defaults() );
420 }
421
422 /**
423 * Apply admin-edited document data to the in-memory cache without writing to the database.
424 *
425 * Use before an operation that will call update_all() itself (e.g. save_remote_document)
426 * so that both changes land in a single database write.
427 *
428 * @param array<string,mixed> $documents Raw document data.
429 */
430 public function stage_documents_from_request( array $documents ): void {
431 $settings = $this->get_all();
432
433 foreach ( $documents as $raw_type => $document ) {
434 $type = self::normalize_document_type( (string) $raw_type );
435
436 if ( ! $type || ! isset( $settings['documents'][ $type ] ) || ! is_array( $document ) ) {
437 continue;
438 }
439
440 $source = isset( $document['source'] ) ? sanitize_key( wp_unslash( $document['source'] ) ) : 'remote';
441
442 if ( in_array( $source, array( 'remote', 'local' ), true ) ) {
443 $settings['documents'][ $type ]['source'] = $source;
444 }
445
446 $content_changed = false;
447
448 foreach ( array( 'de', 'en' ) as $language ) {
449 if ( isset( $document['local'][ $language ] ) ) {
450 $new_html = self::sanitize_html( $document['local'][ $language ] );
451
452 if ( ( $settings['documents'][ $type ]['local'][ $language ] ?? '' ) !== $new_html ) {
453 $content_changed = true;
454 }
455
456 $settings['documents'][ $type ]['local'][ $language ] = $new_html;
457 }
458 }
459
460 if ( $content_changed ) {
461 $settings['documents'][ $type ]['local']['modified'] = current_time( 'mysql' );
462 }
463 }
464
465 $this->cache = $settings;
466 }
467
468 /**
469 * Store one remote document response.
470 *
471 * @param string $type Normalized document type.
472 * @param array<string,mixed> $api_data API response data.
473 */
474 public function save_remote_document( string $type, array $api_data ): void {
475 $type = self::normalize_document_type( $type );
476
477 if ( ! $type ) {
478 return;
479 }
480
481 $settings = $this->get_all();
482 $modified = isset( $api_data['modified'] ) ? sanitize_text_field( (string) $api_data['modified'] ) : current_time( 'mysql' );
483
484 $settings['documents'][ $type ]['remote']['de'] = isset( $api_data['html_de'] ) ? self::sanitize_html( $api_data['html_de'] ) : '';
485 $settings['documents'][ $type ]['remote']['en'] = isset( $api_data['html_en'] ) ? self::sanitize_html( $api_data['html_en'] ) : '';
486 $settings['documents'][ $type ]['remote']['modified'] = $modified;
487
488 $this->update_all( $settings );
489 }
490
491 /**
492 * Copy the last synchronized remote document values into the local fields.
493 *
494 * @param string $type Document type.
495 *
496 * @return array<int,string> Languages written to the local document.
497 */
498 public function copy_remote_document_to_local( string $type ): array {
499 $type = self::normalize_document_type( $type );
500
501 if ( ! $type ) {
502 return array();
503 }
504
505 $settings = $this->get_all();
506
507 if ( ! isset( $settings['documents'][ $type ] ) ) {
508 return array();
509 }
510
511 $has_remote_text = false;
512 $remote_values = array();
513
514 foreach ( array( 'de', 'en' ) as $language ) {
515 $remote_html = (string) ( $settings['documents'][ $type ]['remote'][ $language ] ?? '' );
516 $remote_values[ $language ] = $remote_html;
517
518 if ( '' !== $remote_html ) {
519 $has_remote_text = true;
520 }
521 }
522
523 if ( ! $has_remote_text ) {
524 return array();
525 }
526
527 foreach ( $remote_values as $language => $remote_html ) {
528 $settings['documents'][ $type ]['local'][ $language ] = self::sanitize_html( $remote_html );
529 }
530
531 $settings['documents'][ $type ]['local']['modified'] = current_time( 'mysql' );
532
533 $this->update_all( $settings );
534
535 return array_keys( $remote_values );
536 }
537
538 /**
539 * Return document settings.
540 *
541 * @param string $type Document type.
542 *
543 * @return array<string,mixed>
544 */
545 public function get_document( string $type ): array {
546 $type = self::normalize_document_type( $type );
547 $settings = $this->get_all();
548
549 if ( ! $type || ! isset( $settings['documents'][ $type ] ) ) {
550 return array();
551 }
552
553 return $settings['documents'][ $type ];
554 }
555
556 /**
557 * Return the HTML displayed for a document.
558 *
559 * @param string $type Document type.
560 * @param string $language Language key.
561 */
562 public function get_document_html( string $type, string $language = 'de' ): string {
563 $document = $this->get_document( $type );
564 $language = 'en' === strtolower( $language ) ? 'en' : 'de';
565
566 if ( empty( $document ) ) {
567 return '';
568 }
569
570 $source = 'local' === ( $document['source'] ?? '' ) ? 'local' : 'remote';
571
572 return (string) ( $document[ $source ][ $language ] ?? '' );
573 }
574
575 /**
576 * Return local document HTML independent from API key state.
577 *
578 * @param string $type Document type.
579 * @param string $language Language key.
580 */
581 public function get_local_document_html( string $type, string $language = 'de' ): string {
582 $document = $this->get_document( $type );
583 $language = 'en' === strtolower( $language ) ? 'en' : 'de';
584
585 if ( empty( $document ) ) {
586 return '';
587 }
588
589 return (string) ( $document['local'][ $language ] ?? '' );
590 }
591
592 /**
593 * Mask API credentials for display.
594 *
595 * @param string $value Secret value.
596 */
597 public static function mask_secret( string $value ): string {
598 if ( '' === $value ) {
599 return '';
600 }
601
602 $tail = substr( $value, -4 );
603
604 return str_repeat( '*', max( 8, strlen( $value ) - 4 ) ) . $tail;
605 }
606
607 /**
608 * Encrypt a sensitive value for storage using AES-256-CBC with AUTH_KEY/AUTH_SALT as key material.
609 * Falls back to plaintext when openssl or the WordPress secret constants are unavailable.
610 *
611 * @param string $value Plaintext value to encrypt.
612 */
613 public static function encrypt_value( string $value ): string {
614 if ( '' === $value
615 || ! function_exists( 'openssl_encrypt' )
616 || ! defined( 'AUTH_KEY' )
617 || ! defined( 'AUTH_SALT' )
618 ) {
619 return $value;
620 }
621
622 $key = substr( hash( 'sha256', \AUTH_KEY . \AUTH_SALT ), 0, 32 );
623 $iv = random_bytes( 16 );
624 $enc = openssl_encrypt( $value, 'AES-256-CBC', $key, \OPENSSL_RAW_DATA, $iv );
625
626 if ( false === $enc ) {
627 return $value;
628 }
629
630 return 'enc::' . base64_encode( $iv . $enc ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- binary IV+ciphertext encoding, not obfuscation
631 }
632
633 /**
634 * Decrypt a value previously encrypted with encrypt_value().
635 * Returns plaintext unchanged if no encryption prefix is present (backward compatibility).
636 *
637 * @param string $value Stored value (encrypted or legacy plaintext).
638 */
639 public static function decrypt_value( string $value ): string {
640 if ( '' === $value || 0 !== strpos( $value, 'enc::' ) ) {
641 return $value;
642 }
643
644 if ( ! function_exists( 'openssl_decrypt' )
645 || ! defined( 'AUTH_KEY' )
646 || ! defined( 'AUTH_SALT' )
647 ) {
648 return '';
649 }
650
651 $decoded = base64_decode( substr( $value, 5 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- decoding binary IV+ciphertext, not obfuscation
652
653 if ( false === $decoded || strlen( $decoded ) < 17 ) {
654 return '';
655 }
656
657 $key = substr( hash( 'sha256', \AUTH_KEY . \AUTH_SALT ), 0, 32 );
658 $iv = substr( $decoded, 0, 16 );
659 $enc = substr( $decoded, 16 );
660 $dec = openssl_decrypt( $enc, 'AES-256-CBC', $key, \OPENSSL_RAW_DATA, $iv );
661
662 return false !== $dec ? $dec : '';
663 }
664
665 /**
666 * Sanitize API key.
667 *
668 * @param mixed $value Raw value.
669 */
670 public static function sanitize_api_key( $value ): string {
671 if ( is_array( $value ) || is_object( $value ) ) {
672 return '';
673 }
674
675 return sanitize_text_field( wp_unslash( (string) $value ) );
676 }
677
678 /**
679 * Sanitize legal text HTML.
680 *
681 * @param mixed $html Raw HTML.
682 */
683 public static function sanitize_html( $html ): string {
684 if ( is_array( $html ) || is_object( $html ) ) {
685 return '';
686 }
687
688 return wp_kses_post( wp_unslash( (string) $html ) );
689 }
690
691 /**
692 * Migrate options from v3 when no API key is present in the new format.
693 *
694 * Runs at plugins_loaded so auto-updates (which skip the activation hook)
695 * are also covered. Bails immediately once a key exists (cache lookup only).
696 * The flag erecht24_v3_migrated is set when no old key is found, so the
697 * extra get_option call for erecht24_api_key_settings is skipped on all
698 * subsequent requests once it is clear that no v3 data exists to import.
699 */
700 public function migrate_from_v3(): void {
701 if ( '' !== $this->get_api_key() ) {
702 return;
703 }
704
705 if ( get_option( 'erecht24_v3_migrated' ) ) {
706 return;
707 }
708
709 $old = get_option( 'erecht24_api_key_settings', null );
710
711 if ( ! is_array( $old ) || empty( $old['api_key'] ) ) {
712 update_option( 'erecht24_v3_migrated', '1', false );
713 return;
714 }
715
716 $migrated = self::migrate_legacy_options();
717 $this->cache = $migrated;
718 update_option( self::OPTION_NAME, $migrated, false );
719 update_option( 'erecht24_v3_migrated', '1', false );
720 }
721
722 /**
723 * Initialize settings for the current site.
724 */
725 public static function activate_current_site(): void {
726 $existing = get_option( self::OPTION_NAME, null );
727
728 if ( is_array( $existing ) && ! empty( $existing['api_key'] ) ) {
729 // Existing key — preserve config and ensure schema is complete.
730 update_option( self::OPTION_NAME, array_replace_recursive( self::defaults(), $existing ), false );
731 return;
732 }
733
734 // No key yet — try to import from v3 or initialise with defaults.
735 update_option( self::OPTION_NAME, self::migrate_legacy_options(), false );
736 }
737
738 /**
739 * Copy values from the previous eRecht24 plugin without deleting them.
740 *
741 * @return array<string,mixed>
742 */
743 private static function migrate_legacy_options(): array {
744 $settings = self::defaults();
745
746 $api_key_option = get_option( 'erecht24_api_key_settings', array() );
747 if ( is_array( $api_key_option ) && ! empty( $api_key_option['api_key'] ) ) {
748 $clean_key = self::sanitize_api_key( $api_key_option['api_key'] );
749 $settings['api_key'] = self::encrypt_value( $clean_key );
750 $settings['api_key_status'] = 'unchecked';
751 }
752
753 $google_analytics = get_option( 'erecht24_google_analytics_settings', array() );
754 if ( is_array( $google_analytics ) ) {
755 $settings['google_analytics'] = array(
756 'enabled' => ! empty( $google_analytics['google_analytics_enabled'] ),
757 'measurement' => ! empty( $google_analytics['google_analytics_key'] ) ? sanitize_text_field( (string) $google_analytics['google_analytics_key'] ) : '',
758 'usercentrics' => ! empty( $google_analytics['google_analytics_usercentrics'] ),
759 'opt_out' => ! empty( $google_analytics['google_analytics_opt_out'] ),
760 );
761 }
762
763 // Deliberately not migrating client_id/client_secret from the previous plugin: a push
764 // client registered by a different codebase (potentially with a different push_uri or
765 // push mechanism) should never be trusted as-is. Leaving these at their empty defaults
766 // means Plugin::maybe_reregister_push_client() will register a fresh, 4.x-native client
767 // automatically on the next request, with no action required from the site owner.
768
769 $legacy_map = array(
770 'imprint' => 'erecht24_imprint_settings',
771 'privacy_policy' => 'erecht24_privacy_policy_settings',
772 'privacy_policy_social_media' => 'erecht24_privacy_policy_social_media_settings',
773 );
774
775 foreach ( $legacy_map as $type => $option_name ) {
776 $legacy = get_option( $option_name, array() );
777
778 if ( ! is_array( $legacy ) ) {
779 continue;
780 }
781
782 $settings['documents'][ $type ]['source'] = ! empty( $legacy['document_source'] ) ? 'local' : 'remote';
783 $settings['documents'][ $type ]['remote']['de'] = isset( $legacy['document_de_remote'] ) ? self::sanitize_html( $legacy['document_de_remote'] ) : '';
784 $settings['documents'][ $type ]['remote']['en'] = isset( $legacy['document_en_remote'] ) ? self::sanitize_html( $legacy['document_en_remote'] ) : '';
785 $settings['documents'][ $type ]['remote']['modified'] = isset( $legacy['document_de_last_update_remote'] ) ? sanitize_text_field( (string) $legacy['document_de_last_update_remote'] ) : '';
786 $settings['documents'][ $type ]['local']['de'] = isset( $legacy['document_de_local'] ) ? self::sanitize_html( $legacy['document_de_local'] ) : '';
787 $settings['documents'][ $type ]['local']['en'] = isset( $legacy['document_en_local'] ) ? self::sanitize_html( $legacy['document_en_local'] ) : '';
788 $settings['documents'][ $type ]['local']['modified'] = isset( $legacy['document_de_last_update_local'] ) ? sanitize_text_field( (string) $legacy['document_de_last_update_local'] ) : '';
789 }
790
791 $old_imprint = get_option( 'imprint_text', '' );
792 if ( $old_imprint && empty( $settings['documents']['imprint']['local']['de'] ) ) {
793 $settings['documents']['imprint']['source'] = 'local';
794 $settings['documents']['imprint']['local']['de'] = self::sanitize_html( $old_imprint );
795 }
796
797 $old_privacy = get_option( 'privacy_text', '' );
798 if ( $old_privacy && empty( $settings['documents']['privacy_policy']['local']['de'] ) ) {
799 $settings['documents']['privacy_policy']['source'] = 'local';
800 $settings['documents']['privacy_policy']['local']['de'] = self::sanitize_html( $old_privacy );
801 }
802
803 return $settings;
804 }
805 }
806