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 / Plugin.php

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

321 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main plugin bootstrap.
4 *
5 * @package ERecht24LegalText
6 */
7
8 namespace ERecht24LegalText;
9
10 use ERecht24LegalText\Admin\Admin_Page;
11 use ERecht24LegalText\Api\Client;
12 use ERecht24LegalText\Api\Push_Controller;
13 use ERecht24LegalText\Frontend\Blocks;
14 use ERecht24LegalText\Frontend\Google_Analytics;
15 use ERecht24LegalText\Frontend\Shortcodes;
16 use ERecht24LegalText\Integrations\Elementor;
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * Coordinates plugin services.
22 */
23 final class Plugin {
24
25
26 /**
27 * Singleton instance.
28 *
29 * @var self|null
30 */
31 private static $instance = null;
32
33 /**
34 * Settings service.
35 *
36 * @var Settings
37 */
38 private $settings;
39
40 /**
41 * API client.
42 *
43 * @var Client
44 */
45 private $api_client;
46
47 /**
48 * Shortcode renderer.
49 *
50 * @var Shortcodes
51 */
52 private $shortcodes;
53
54 /**
55 * Return the singleton.
56 */
57 public static function instance(): self {
58 if ( null === self::$instance ) {
59 self::$instance = new self();
60 }
61
62 return self::$instance;
63 }
64
65 /**
66 * Prevent constructing the singleton from outside {@see instance()}.
67 */
68 private function __construct() {}
69
70 /**
71 * Prevent cloning of the singleton.
72 */
73 private function __clone() {}
74
75 /**
76 * Prevent deserialization of the singleton.
77 *
78 * @throws \RuntimeException Always.
79 */
80 public function __wakeup(): void {
81 throw new \RuntimeException( 'Cannot deserialize singleton.' );
82 }
83
84 /**
85 * Activation callback.
86 *
87 * @param bool $network_wide Whether the plugin is network activated.
88 */
89 public static function activate( bool $network_wide = false ): void {
90 Settings::activate( $network_wide );
91 }
92
93 /**
94 * Init WordPress hooks.
95 */
96 public function init(): void {
97 $this->settings = new Settings();
98 $this->settings->migrate_from_v3();
99 $this->api_client = new Client( $this->settings );
100
101 add_action(
102 'init',
103 function (): void {
104 $this->maybe_reregister_push_client();
105 }
106 );
107
108 $this->shortcodes = new Shortcodes( $this->settings );
109
110 $this->shortcodes->register();
111
112 $blocks = new Blocks( $this->shortcodes, $this->settings );
113 $blocks->register_hooks();
114
115 $google_analytics = new Google_Analytics( $this->settings );
116 $google_analytics->register_hooks();
117
118 $push_controller = new Push_Controller( $this->settings, $this->api_client );
119 add_action( 'rest_api_init', array( $push_controller, 'register_routes' ) );
120
121 if ( is_admin() ) {
122 $admin_page = new Admin_Page( $this->settings, $this->api_client );
123 $admin_page->register_hooks();
124 }
125
126 $elementor = new Elementor();
127 $elementor->register_hooks();
128
129 add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
130 add_action( 'wp_initialize_site', array( $this, 'initialize_new_site' ) );
131 add_action( 'shutdown', array( $this->settings, 'flush_logs' ) );
132 }
133
134 /**
135 * Re-register the push client whenever an API key is stored without push
136 * credentials — regardless of how that state arose (fresh v3 migration, an
137 * already-affected install updating, or a previously failed registration
138 * attempt). Rate-limited via transient so a persistently failing eRecht24
139 * API doesn't retry on every single request.
140 *
141 * Hooked to `init` rather than called directly from `init()` (which runs on
142 * `plugins_loaded`): register_client() calls rest_url(), which requires the
143 * global $wp_rewrite to be set up — that happens later in WordPress's own
144 * bootstrap, so calling it during plugins_loaded fatals with
145 * "Call to a member function using_index_permalinks() on null".
146 *
147 * @param bool $force Skip the "client_secret is empty" check — used for an
148 * explicit admin action re-registering a client that already
149 * has *a* secret, e.g. one inherited from a v3.x migration
150 * that predates this codebase's own registration, or one a
151 * Remote Push Test found to be failing despite looking valid.
152 *
153 * @return bool True if a registration attempt was made and succeeded.
154 */
155 public function maybe_reregister_push_client( bool $force = false ): bool {
156 $api_key = $this->settings->get_api_key();
157
158 if ( '' === $api_key ) {
159 return false;
160 }
161
162 if ( ! $force && '' !== $this->settings->get_client_secret() ) {
163 return false;
164 }
165
166 if ( get_transient( 'erecht24_push_reregister_attempted' ) ) {
167 return false;
168 }
169
170 set_transient( 'erecht24_push_reregister_attempted', '1', HOUR_IN_SECONDS );
171
172 try {
173 $registration = $this->api_client->register_client( $api_key );
174 } catch ( \Throwable $exception ) {
175 $this->settings->add_log( 'Automatic push client re-registration crashed: ' . $exception->getMessage() );
176 return false;
177 }
178
179 if ( is_wp_error( $registration ) ) {
180 $this->settings->add_log( 'Automatic push client re-registration failed: ' . $registration->get_error_message() );
181 return false;
182 }
183
184 $secret = (string) ( $registration['secret'] ?? '' );
185 $client_id = absint( $registration['client_id'] ?? 0 );
186
187 $this->settings->save_api_connection( $api_key, $client_id, $secret );
188
189 if ( '' === $secret || 0 === $client_id ) {
190 $this->settings->add_log( 'Push client re-registration returned an incomplete response (missing secret or client_id).' );
191 return false;
192 }
193
194 $this->settings->add_log( 'Push client automatically re-registered after detecting a missing push secret.' );
195 $this->settings->set_push_test_failed( false );
196
197 $this->cleanup_duplicate_clients( $api_key, $client_id );
198
199 return true;
200 }
201
202 /**
203 * Delete any other push clients at eRecht24 that share this site's push_uri
204 * (e.g. one left over from the previous v3.x plugin, or one created by an
205 * earlier failed repair attempt). Having more than one client registered
206 * for the same push_uri is a real problem, not just clutter: eRecht24 may
207 * push to any of them, and an old client can have a stale `push_method`
208 * (e.g. GET from the v3.x plugin) that this route no longer accepts,
209 * producing a genuine `rest_no_route` failure that has nothing to do with
210 * the current, correctly registered client.
211 *
212 * @param string $api_key API key.
213 * @param int $current_client_id The client id to keep — never deleted.
214 *
215 * @return int Number of duplicate clients actually deleted.
216 */
217 public function cleanup_duplicate_clients( string $api_key, int $current_client_id ): int {
218 if ( 1 > $current_client_id ) {
219 // Without a valid id of our own there is no safe exclusion criterion —
220 // never delete anything rather than risk removing a legitimate client.
221 $this->settings->add_log( 'Duplicate push client cleanup skipped: no valid current client id to keep.' );
222 return 0;
223 }
224
225 try {
226 $clients = $this->api_client->list_clients( $api_key );
227 } catch ( \Throwable $exception ) {
228 $this->settings->add_log( 'Duplicate push client cleanup crashed while listing clients: ' . $exception->getMessage() );
229 return 0;
230 }
231
232 if ( is_wp_error( $clients ) || ! is_array( $clients ) ) {
233 return 0;
234 }
235
236 $removed = 0;
237
238 $push_uri = rest_url( 'erecht24/v1/push' );
239
240 foreach ( $clients as $client ) {
241 if ( ! is_array( $client ) ) {
242 continue;
243 }
244
245 $client_id = absint( $client['client_id'] ?? 0 );
246
247 if ( 0 === $client_id || $current_client_id === $client_id ) {
248 continue;
249 }
250
251 if ( ( $client['push_uri'] ?? '' ) !== $push_uri ) {
252 continue;
253 }
254
255 try {
256 $deleted = $this->api_client->delete_client( $api_key, $client_id );
257 } catch ( \Throwable $exception ) {
258 $this->settings->add_log( 'Duplicate push client cleanup crashed deleting client ' . $client_id . ': ' . $exception->getMessage() );
259 continue;
260 }
261
262 if ( is_wp_error( $deleted ) ) {
263 $this->settings->add_log( 'Duplicate push client cleanup failed for client ' . $client_id . ': ' . $deleted->get_error_message() );
264 continue;
265 }
266
267 $this->settings->add_log( 'Duplicate push client (id ' . $client_id . ', same push_uri) deleted at eRecht24.' );
268 ++$removed;
269 }
270
271 return $removed;
272 }
273
274 /**
275 * Initialize plugin settings for a newly created site in the network.
276 *
277 * @param \WP_Site $site The newly created site.
278 */
279 public function initialize_new_site( \WP_Site $site ): void {
280 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
281 require_once ABSPATH . 'wp-admin/includes/plugin.php';
282 }
283
284 if ( ! is_plugin_active_for_network( ERECHT24_LEGAL_TEXT_BASENAME ) ) {
285 return;
286 }
287
288 switch_to_blog( (int) $site->blog_id );
289 Settings::activate_current_site();
290 restore_current_blog();
291 }
292
293 /**
294 * Add suggested privacy policy text for the external eRecht24 service.
295 */
296 public function add_privacy_policy_content(): void {
297 if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
298 return;
299 }
300
301 $content = '<p class="privacy-policy-tutorial">' . esc_html__(
302 'eRecht24 Legal Texts verbindet diese Website nur nach Konfiguration durch eine berechtigte Administratorin oder einen berechtigten Administrator mit dem eRecht24-Dienst.',
303 'erecht24'
304 ) . '</p>';
305
306 $content .= '<p>' . wp_kses_post(
307 sprintf(
308 /* translators: 1: eRecht24 privacy URL. */
309 __( 'Wenn der API-Schlüssel gespeichert oder Rechtstexte synchronisiert werden, werden technische Verbindungsdaten sowie die im Plugin angezeigte Push-URL an eRecht24 übertragen. Details zur Verarbeitung durch eRecht24 stehen in der <a href="%1$s" target="_blank" rel="noopener noreferrer">Datenschutzerklärung von eRecht24</a>.', 'erecht24' ),
310 esc_url( 'https://www.e-recht24.de/datenschutzerklaerung.html' )
311 )
312 ) . '</p>';
313 $content .= '<p>' . esc_html__( 'Wenn Google Analytics in den Plugin-Einstellungen aktiviert wird, lädt die Website das Google-Tag und muss dafür eine passende Einwilligungs- und Datenschutzlösung bereitstellen.', 'erecht24' ) . '</p>';
314
315 wp_add_privacy_policy_content(
316 'eRecht24 Legal Texts',
317 wp_kses_post( wpautop( $content, false ) )
318 );
319 }
320 }
321