PluginProbe
avalex – Automatisch sichere Rechtstexte / 2.0.4
avalex – Automatisch sichere Rechtstexte v2.0.4
trunk 1.5.6 1.5.7 1.5.8 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 3.1.4
avalex / avalex.php

avalex.php in avalex – Automatisch sichere Rechtstexte 2.0.4, at avalex.php

628 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: avalex
4 Description: Das Plugin ermöglicht es, Ihre Website mit der avalex API zu verbinden. Einen API Key erhalten Sie auf www.avalex.de.
5 Author: avalex GmbH
6 Author URI: https://avalex.de/
7 Version: 2.0.4
8 Text Domain: Avalex
9 Domain Path: /languages
10 */
11
12 class Avalex {
13 protected $apiUrl = 'https://avalex.de';
14 protected $fallbackApiUrl = 'https://proxy.avalex.de';
15 protected $types = [
16 'dse' => '/avx-datenschutzerklaerung',
17 'imprint' => '/avx-impressum',
18 'agb' => '/avx-bedingungen',
19 'widerruf' => '/avx-widerruf',
20 ];
21
22 public function __construct() {
23 // Call the functions that handle the stuff we need to do on plugin activation.
24 register_activation_hook(__FILE__, array($this, 'activatePlugin'));
25
26 // And call the functions, when the user deactivates the plugin.
27 register_deactivation_hook(__FILE__, array($this, 'deactivatePlugin'));
28
29 // And finally call the functions, when the user deletes the plugin.
30 register_uninstall_hook(__FILE__, 'Avalex::uninstallPlugin');
31
32 // We also want to load the language files (To make WordPress think it is translated).
33 $this->loadLanguageFiles();
34
35 // Set our variables.
36 global $wpdb;
37 $this->pluginPath = dirname(__FILE__);
38 $this->tableName = $wpdb->prefix . 'avalex';
39 $this->apiKey = get_option('avalex_api_key', false);
40 $this->isKeyValid = get_option('avalex_valid_api_key', false);
41 $this->isDomainValid = false;
42 $this->response = false;
43 $this->recursiveCalls = 0;
44 $this->notice = false;
45 $this->cachePath = WP_CONTENT_DIR . '/cache';
46 $this->avalexCronAction();
47 $this->maybeUpdateDatabase();
48
49 // Register a new cron schedule.
50 add_filter('cron_schedules', array($this, 'addQuarterlyCronSchedule'));
51
52 // Call our methods that we need as early as possible.
53 $this->init();
54
55 // Add our admin menu page.
56 add_action('admin_menu', array($this, 'addAdminMenu'));
57
58 // Add our DSE shortcode.
59 add_shortcode('avalex', array($this, 'renderAvalexShortcode'));
60 add_shortcode('avalex_de_datenschutz', array($this, 'renderAvalexShortcode'));
61 add_shortcode('avalex_de_impressum', array($this, 'renderAvalexShortcode'));
62 add_shortcode('avalex_de_agb', array($this, 'renderAvalexShortcode'));
63 add_shortcode('avalex_de_widerrufsbelehrung', array($this, 'renderAvalexShortcode'));
64
65 // Add update functionality.
66 add_action('pre_set_site_transient_update_plugins', array($this, 'pluginUpdateNotification'));
67
68 // We need an action to make it possible to force a DSE update from outside for important updates that can't wait.
69 add_action('init', array($this, 'forceUpdate'));
70
71 // Add settings link to plugin page.
72 add_action('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'addSettingsLink'));
73 }
74
75 public function activatePlugin() {
76 $this->createTable();
77 $this->maybeDeleteOldCronJob();
78 $this->registerCronJob();
79 }
80
81 public function checkPhpVersion() {
82 if (version_compare(PHP_VERSION, '5.6', '>')) {
83 return;
84 }
85
86 wp_die('PHP Version zu alt. Bitte nutzen Sie mindestens PHP 5.6.<br>Sie nutzen aktuell Version: ' . PHP_VERSION);
87 }
88
89 public function createTable() {
90 global $wpdb;
91 $charsetCollate = $wpdb->get_charset_collate();
92
93 // First we want to drop any old tables of avalex, if there are some.
94 $this->deleteAvalexTable();
95
96 // Now create our new table.
97 $sql = "CREATE TABLE $this->tableName (
98 id mediumint(9) NOT NULL AUTO_INCREMENT,
99 time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
100 type text NOT NULL,
101 data longtext NOT NULL,
102 PRIMARY KEY (id)
103 ) $charsetCollate;";
104
105 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
106 dbDelta($sql);
107 }
108
109 public function maybeUpdateDatabase() {
110 global $wpdb;
111 $charsetCollate = $wpdb->get_charset_collate();
112
113 // Now create our new table.
114 $sql = "CREATE TABLE $this->tableName (
115 id mediumint(9) NOT NULL AUTO_INCREMENT,
116 time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
117 data longtext NOT NULL,
118 type text NOT NULL,
119 PRIMARY KEY (id)
120 ) $charsetCollate;";
121
122 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
123 dbDelta($sql);
124 }
125
126 public function maybeDeleteOldCronJob() {
127 if (wp_next_scheduled('avalex_cron_event')) {
128 wp_clear_scheduled_hook('avalex_cron_event');
129 }
130
131 if(wp_next_scheduled('avalex_update_dse_cron_event')) {
132 wp_clear_scheduled_hook('avalex_update_dse_cron_event');
133 }
134 }
135
136 public function registerCronJob() {
137 if (!wp_next_scheduled('avalex_update_dse_cron_event')) {
138 wp_schedule_event(time(), 'avalex_interval', 'avalex_update_dse_cron_event');
139 }
140 }
141
142 public function deactivatePlugin() {
143 $this->maybeDeleteOldCronJob();
144 }
145
146 public function uninstallPlugin() {
147 global $wpdb;
148 $tableName = $wpdb->prefix . 'avalex';
149 $wpdb->query("DROP TABLE IF EXISTS $tableName");
150
151 self::deleteOptions();
152 }
153
154 public function deleteAvalexTable() {
155 global $wpdb;
156 $wpdb->query("DROP TABLE IF EXISTS $this->tableName");
157 }
158
159 public function deleteOptions() {
160 delete_option('avalex_api_key');
161 delete_option('avalex_valid_api_key');
162 }
163
164 public function avalexCronAction() {
165 add_action('avalex_update_dse_cron_event', array($this, 'fetchAvalexTexts'));
166 }
167
168 public function addQuarterlyCronSchedule($schedules) {
169 $schedules['avalex_interval'] = array(
170 'interval' => 21600,
171 'display' => __('4 times a day'));
172 return $schedules;
173 }
174
175 public function loadLanguageFiles() {
176 load_plugin_textdomain( 'Avalex', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' );
177 }
178
179 public function init() {
180 // If the user hits the submit button, we want to store the new apikey.
181 if(!isset($_POST['save_avalex'])) {
182 return;
183 }
184
185 if (!$this->saveApiKey()) {
186 return;
187 }
188
189 if (!$this->validateApiKey()) {
190 return;
191 }
192
193 $this->fetchAvalexTexts();
194 }
195
196 public function addAdminMenu() {
197 add_options_page('avalex', 'avalex', 'manage_options', 'avalex', array($this, 'avalexAdminPage'));
198 }
199
200 public function avalexAdminPage() {
201 include $this->pluginPath . '/templates/admin_page.php';
202 }
203
204 public function addNotice() {
205 if (!$this->notice) {
206 return false;
207 }
208
209 echo '<div class="notice notice-' . $this->noticeType . '"><p>' . $this->noticeMessage . '</p></div>';
210 return true;
211 }
212
213 private function saveApiKey() {
214 // Check if api key was entered.
215 $apiKey = sanitize_text_field($_POST['avalex_api_key']);
216 update_option('avalex_api_key', $apiKey);
217
218 if (!$apiKey) {
219 $this->notice = true;
220 $this->noticeType = 'error';
221 $this->noticeMessage = __('Please enter an API key.', 'Avalex');
222 return false;
223 }
224
225 if ($apiKey) {
226 $this->apiKey = $apiKey;
227 return true;
228 }
229 }
230
231 private function showApiKey() {
232 if ($this->apiKey) {
233 echo $this->apiKey;
234 return;
235 }
236
237 return false;
238 }
239
240 private function validateApiKey() {
241
242 // Build the api url.
243 $apiUrlDomain = $this->apiUrl . '/avx-datenschutzerklaerung';
244 if ($this->recursiveCalls == 1) {
245 // $apiUrlDomain = $this->fallbackApiUrl . 'avx-datenschutzerklaerung';
246 }
247
248 $serverDomain = $this->trimDomain(home_url());
249
250 $apiUrlDomain = add_query_arg('apikey', $this->apiKey, $apiUrlDomain);
251 $apiUrlDomain = add_query_arg('domain', $serverDomain, $apiUrlDomain);
252
253 // Call the api.
254 $response = wp_remote_get($apiUrlDomain);
255 $responseCode = wp_remote_retrieve_response_code($response);
256
257 // If we have a 401, the key is not valid.
258 if ($responseCode == 401) {
259 $this->notice = true;
260 $this->noticeType = 'error';
261 $this->noticeMessage = 'Der API-Key ist ungültig.';
262 $this->isKeyValid = false;
263 update_option('avalex_valid_api_key', false);
264 return false;
265 }
266
267 // 200 means api key is valid, so we can grab the domain and proceed.
268 if ($responseCode == 200) {
269 update_option('avalex_valid_api_key', true);
270 $this->isKeyValid = 1;
271 $this->response = json_decode(wp_remote_retrieve_body($response), true);
272 $this->recursiveCalls = 0;
273 return true;
274 }
275
276 if (is_wp_error($response)) {
277 // When this is the first time the error happens, we want to try the fallback url.
278 if ($this->recursiveCalls == 0) {
279 $this->recursiveCalls = 1;
280 return $this->validateApiKey();
281 }
282
283 $errorMessage = $response->get_error_message();
284 $this->notice = true;
285 $this->noticeType = 'error';
286 $this->noticeMessage = 'Beim Datenabgleich mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
287 return false;
288 }
289
290 if ($responseCode == 400) {
291 // Website not configured.
292 $this->notice = true;
293 $this->noticeType = 'error';
294 $this->noticeMessage = 'Webseite noch nicht fertig konfiguriert oder die hinterlegte Domain stimmt nicht mit ihrem Server überein.';
295 $this->isKeyValid = false;
296 update_option('avalex_valid_api_key', false);
297 return false;
298 }
299
300
301 update_option('avalex_valid_api_key', false);
302 return false;
303 }
304
305 public function trimDomain($domain) {
306 // Remove protocoll and www.
307 $domain = str_replace('http://', '', $domain);
308 $domain = str_replace('https://', '', $domain);
309 $domain = str_replace('www.', '', $domain);
310 $domain = rtrim($domain, '/');
311 return $domain;
312 }
313
314 public function fetchAvalexTexts() {
315
316 // We loop trough every type and save everything in its own row.
317 foreach($this->types as $type => $endpoint) {
318 $this->fetchAvalexDse($endpoint, $type);
319 }
320
321 }
322
323 public function fetchAvalexDse($endpoint, $type) {
324 $apiUrl = $this->apiUrl . $endpoint;
325
326
327 if ($this->recursiveCalls == 1) {
328 $apiUrl = $this->fallbackApiUrl . $endpoint;
329 }
330
331 $serverDomain = $this->trimDomain(home_url());
332
333 $apiUrl = add_query_arg('apikey', $this->apiKey, $apiUrl);
334 $apiUrl = add_query_arg('domain', $serverDomain, $apiUrl);
335 $response = wp_remote_get($apiUrl);
336 $responseCode = wp_remote_retrieve_response_code($response);
337
338 if ($responseCode == 401) {
339 // API Key not authorized, shouldn't happen at this point, but you never know.
340 return;
341 }
342
343 if ($responseCode == 200) {
344 // We got something back, let's see if the body has some data.
345 $data = wp_remote_retrieve_body($response);
346
347 // If the data is empty, we do nothing to avoid overwriting the DSE with empty content.
348 if (empty($data)) {
349 return false;
350 }
351
352 // Alright, we should be safe to actually save the dse in the database. But to be sure, we sanitize the data.
353 $sanitizedData = sanitize_post_field('post_content', trim($data), false, 'display');
354 $trimmedData = preg_replace("/\r|\n/", '', $sanitizedData);
355 $this->dseHtml = $trimmedData;
356 $this->writeDseIntoDatabase($type);
357 $this->recursiveCalls = 0;
358
359 // Set the data for the successfull update notice.
360 $this->notice = true;
361 $this->noticeType = 'success';
362 $this->noticeMessage = 'Der API Key und die avalex Rechtstexte wurden aktualisiert.';
363
364 // Now we delete the whole cache of WordPress to make sure the update actually shows.
365 $this->emptyCache();
366
367 return true;
368 }
369
370 if (is_wp_error($response)) {
371 if ($this->recursiveCalls == 0) {
372 $this->recursiveCalls = 1;
373 return $this->fetchAvalexDse($endpoint);
374 }
375
376 $errorMessage = $response->get_error_message();
377 $this->notice = true;
378 $this->noticeType = 'error';
379 $this->noticeMessage = 'Beim Datenabgleich mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
380 return false;
381 }
382 }
383
384 public function writeDseIntoDatabase($type) {
385 if (!$this->dseHtml) {
386 return;
387 }
388
389 // Write our new data into the database. And delete the old row beforehand.
390 global $wpdb;
391
392 $wpdb->query("DELETE FROM $this->tableName WHERE type = '$type'");
393
394
395 $wpdb->insert(
396 $this->tableName,
397 array(
398 'time' => current_time('mysql'),
399 'data' => $this->dseHtml,
400 'type' => $type,
401 ),
402 array(
403 '%s',
404 '%s',
405 '%s',
406 )
407 );
408 }
409
410 public function getDseFromDatabase($type) {
411 // Write our new data into the database.
412 global $wpdb;
413 $dseRow = $wpdb->get_results("SELECT * FROM $this->tableName WHERE type = '$type'");
414
415 if (!$dseRow) {
416 return;
417 }
418
419 return $dseRow[0]->data;
420 }
421
422 public function renderAvalexShortcode($atts, $content, $shortcode) {
423 if (!$this->isKeyValid) {
424
425 // Try to revalidate.
426 if($this->validateApiKey()) {
427 $this->renderAvalexShortcode(false, false, $shortcode);
428 }
429
430 return 'Avalex ist noch nicht fertig eingerichtet.';
431 }
432
433 switch ($shortcode) {
434 case 'avalex_de_impressum':
435 $type = 'imprint';
436 break;
437
438 case 'avalex_de_agb':
439 $type = 'agb';
440 break;
441
442 case 'avalex_de_widerrufsbelehrung':
443 $type = 'widerruf';
444 break;
445
446 case 'avalex':
447 case 'avalex_de_datenschutz':
448 $type = 'dse';
449 break;
450
451 default:
452 return;
453 }
454
455 // First we check if by whatever reason the dse is empty, if it is, we try to fetch the new data.
456 if (!$this->getDseFromDatabase($type) || empty($this->getDseFromDatabase($type))) {
457 if (!$this->validateApiKey()) {
458 if($this->addNotice()) {
459 return;
460 }
461 return 'API Key ungültig.';
462 }
463
464 if (!$this->fetchAvalexTexts() && $this->recursiveCalls < 2) {
465 $this->recursiveCalls++;
466 $this->renderAvalexShortcode(false, false, $shortcode);
467 return false;
468 }
469 }
470
471 // We have a DSE and we will use it!
472 return $this->getDseFromDatabase($type);
473 }
474
475 public function getDseTime() {
476 global $wpdb;
477 $result = $wpdb->get_row("SELECT time FROM $this->tableName");
478
479 if (!$result) {
480 echo 'Noch keine DSE vorhanden.';
481 }
482
483 echo $result->time;
484 }
485
486 public function pluginUpdateNotification($transient) {
487 if (empty($transient->checked)) {
488 return $transient;
489 }
490
491 $url = $this->apiUrl . '/files/wordpress/package.json';
492
493 if ($this->recursiveCalls == 1) {
494 $url = $this->fallbackApiUrl . '/files/wordpress/package.json';
495 }
496
497 $response = wp_remote_get($url);
498
499 if (is_wp_error($response)) {
500 if ($this->recursiveCalls == 0) {
501 $this->recursiveCalls = 1;
502 return $this->pluginUpdateNotification($transient);
503 }
504
505 $errorMessage = $response->get_error_message();
506 $this->notice = true;
507 $this->noticeType = 'error';
508 $this->noticeMessage = 'Beim Datenabgleich mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
509 return false;
510 }
511
512 $body = json_decode(wp_remote_retrieve_body($response));
513 $version = $body->version;
514
515 $pluginData = get_plugin_data(__FILE__, false, false);
516
517 if (version_compare($version, $pluginData['Version'], '<=')) {
518 return $transient;
519 }
520
521 if ($this->recursiveCalls == 0) {
522 $updateInfo = array(
523 'plugin' => plugin_basename(__FILE__),
524 'slug' => plugin_basename(__FILE__),
525 'new_version' => $version,
526 'url' => 'https://avalex.de',
527 'package' => 'https://avalex.de/files/wordpress/avalex_wordpress.zip',
528 );
529 }
530
531 if ($this->recursiveCalls == 1) {
532 $updateInfo = array(
533 'plugin' => plugin_basename(__FILE__),
534 'slug' => plugin_basename(__FILE__),
535 'new_version' => $version,
536 'url' => 'https://avalex.de',
537 'package' => 'https://proxy.avalex.de/files/wordpress/avalex_wordpress.zip',
538 );
539 }
540
541 $this->recursiveCalls = 0;
542
543 $transient->response[plugin_basename(__FILE__)] = (object) $updateInfo;
544 return $transient;
545 }
546
547 public function forceUpdate() {
548 if (!isset($_GET['force_dse_update']) || $_GET['force_dse_update'] != true) {
549 return;
550 }
551
552 $this->fetchAvalexDse();
553 $this->emptyCache();
554 }
555
556 public function emptyCache() {
557 // We want to get all folders in the cache path, not files. Atleast not in the root of the cache folder.
558 $objects = glob($this->cachePath . '/*');
559 foreach ($objects as $object) {
560 if (!is_dir($object)) {
561 continue;
562 }
563
564
565 $foldersToExclude = [
566 'borlabs-cookie',
567 'autoptimize',
568 'tmp',
569 ];
570 // We also want to exclude some cache paths.
571
572 $skip = false;
573 foreach($foldersToExclude as $folderName) {
574 if(strpos($object, $folderName) !== false) {
575 $skip = true;
576 break;
577 }
578 }
579
580 if($skip) {
581 continue;
582 }
583
584 $this->emptyFolder($object);
585 }
586
587 }
588
589 public function emptyFolder($dir) {
590 $objects = glob($dir . '/*');
591
592 foreach ($objects as $object) {
593 if (!is_dir($object)) {
594 $this->deleteFile($object);
595 continue;
596 }
597
598 // Now we know, this is a folder, so restart the process.
599 $this->emptyFolder($object);
600 }
601 }
602
603 public function deleteFile($file) {
604
605 // We only want to delete html or gzip files, no css or js.
606 $extension = pathinfo($file)['extension'];
607 $allowedExtensionsToDelete = [
608 'html', 'html_gzip', 'gz',
609 ];
610
611 if(!in_array($extension, $allowedExtensionsToDelete)) {
612 return;
613 }
614
615 unlink($file);
616 }
617
618 public function addSettingsLink($links) {
619 $links = array_merge(array(
620 '<a href="' . esc_url(admin_url('/options-general.php?page=avalex')) . '">Einstellungen</a>',
621 ), $links);
622 return $links;
623 }
624 }
625
626 // Call our class.
627 new Avalex();
628