Simple Google reCAPTCHA - Version 4.0

Version Description

  • Warning: I don't recommend Google reCAPTCHA v2. You should enable v3 in plugin settings (requires different keys)!
  • New: Hooks - sgr_render_list + sgr_verify_list. You are able to customize where Google reCAPTCHA will be rendered and verified - e.g. via functions.php.
Download this release

Release Info

Developer Minor
Plugin Icon 128x128 Simple Google reCAPTCHA
Version 4.0
Comparing to
See all releases

Code changes from version 3.9 to 4.0

Files changed (5) hide show
  1. entity.php +21 -41
  2. readme.txt +43 -6
  3. sgr.js +3 -3
  4. simple-google-recaptcha.php +180 -186
  5. uninstall.php +15 -14
entity.php CHANGED
@@ -1,17 +1,20 @@
1
  <?php
2
 
3
- use SimpleGoogleRecaptchaEntity as Entity;
4
-
5
 
6
  if (!defined('ABSPATH')) {
7
- die();
8
  }
9
 
10
  /**
11
- * Class SimpleGoogleRecaptchaEntity
 
12
  */
13
- class SimpleGoogleRecaptchaEntity
14
  {
 
 
 
15
  /** @var int */
16
  const INT = 1;
17
 
@@ -19,25 +22,25 @@ class SimpleGoogleRecaptchaEntity
19
  const STRING = 2;
20
 
21
  /** @var string */
22
- const PAGE_QUERY = '?page=sgr_options';
23
 
24
  /** @var string */
25
- const VERSION = 'sgr_version';
26
 
27
  /** @var string */
28
- const LOGIN_DISABLE = 'sgr_login_disable';
29
 
30
  /** @var string */
31
- const BADGE_HIDE = 'sgr_badge_hide';
32
 
33
  /** @var string */
34
- const SITE_KEY = 'sgr_site_key';
35
 
36
  /** @var string */
37
- const SECRET_KEY = 'sgr_secret_key';
38
 
39
  /** @var string */
40
- const HASH = 'sgr_hash';
41
 
42
  /** @var string */
43
  private $name;
@@ -46,56 +49,34 @@ class SimpleGoogleRecaptchaEntity
46
  private $type;
47
 
48
  /** @var string|int */
49
- private $value;
50
 
51
  /**
52
  * @param string $name
53
  * @param int $type
54
- * @param string|int $value
55
  */
56
- public function __construct($name, $type, $value = '')
57
  {
58
  $this->name = $name;
59
  $this->type = $type;
60
- $this->value = $value;
61
  }
62
 
63
  /**
64
  * @return string
65
  */
66
- public function getName()
67
  {
68
  return $this->name;
69
  }
70
 
71
- /**
72
- * @param string $name
73
- * @return Entity
74
- */
75
- public function setName($name)
76
- {
77
- $this->name = $name;
78
- return $this;
79
- }
80
-
81
  /**
82
  * @return int
83
  */
84
- public function getType()
85
  {
86
  return $this->type;
87
  }
88
 
89
- /**
90
- * @param int $type
91
- * @return Entity
92
- */
93
- public function setType($type)
94
- {
95
- $this->type = $type;
96
- return $this;
97
- }
98
-
99
  /**
100
  * @return int|string
101
  */
@@ -105,12 +86,11 @@ class SimpleGoogleRecaptchaEntity
105
  }
106
 
107
  /**
108
- * @param int|string $value
109
- * @return Entity
110
  */
111
  public function setValue($value)
112
  {
113
  $this->value = $value;
114
- return $this;
115
  }
116
  }
1
  <?php
2
 
3
+ namespace NovaMi\WordPress\SimpleGoogleRecaptcha;
 
4
 
5
  if (!defined('ABSPATH')) {
6
+ die('Direct access not allowed');
7
  }
8
 
9
  /**
10
+ * Class Entity
11
+ * @package NovaMi\WordPress\SimpleGoogleRecaptcha
12
  */
13
+ class Entity
14
  {
15
+ /** @var string */
16
+ const PREFIX = 'sgr_';
17
+
18
  /** @var int */
19
  const INT = 1;
20
 
22
  const STRING = 2;
23
 
24
  /** @var string */
25
+ const PAGE_QUERY = '?page=' . self::PREFIX . 'options';
26
 
27
  /** @var string */
28
+ const VERSION = self::PREFIX . 'version';
29
 
30
  /** @var string */
31
+ const LOGIN_DISABLE = self::PREFIX . 'login_disable';
32
 
33
  /** @var string */
34
+ const BADGE_HIDE = self::PREFIX . 'badge_hide';
35
 
36
  /** @var string */
37
+ const SITE_KEY = self::PREFIX . 'site_key';
38
 
39
  /** @var string */
40
+ const SECRET_KEY = self::PREFIX . 'secret_key';
41
 
42
  /** @var string */
43
+ const HASH = self::PREFIX . 'hash';
44
 
45
  /** @var string */
46
  private $name;
49
  private $type;
50
 
51
  /** @var string|int */
52
+ private $value = '';
53
 
54
  /**
55
  * @param string $name
56
  * @param int $type
 
57
  */
58
+ public function __construct(string $name, int $type = self::INT)
59
  {
60
  $this->name = $name;
61
  $this->type = $type;
 
62
  }
63
 
64
  /**
65
  * @return string
66
  */
67
+ public function getName(): string
68
  {
69
  return $this->name;
70
  }
71
 
 
 
 
 
 
 
 
 
 
 
72
  /**
73
  * @return int
74
  */
75
+ public function getType(): int
76
  {
77
  return $this->type;
78
  }
79
 
 
 
 
 
 
 
 
 
 
 
80
  /**
81
  * @return int|string
82
  */
86
  }
87
 
88
  /**
89
+ * @param $value
90
+ * @return void
91
  */
92
  public function setValue($value)
93
  {
94
  $this->value = $value;
 
95
  }
96
  }
readme.txt CHANGED
@@ -3,8 +3,8 @@ Contributors: Minor
3
  Tags: recaptcha, spam, protect, google, invisible
4
  Requires at least: 4.6
5
  Tested up to: 6.0
6
- Stable tag: 3.9
7
- Requires PHP: 7.1
8
  License: GPLv3
9
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
10
  Donate link: https://www.paypal.me/NovaMi
@@ -40,17 +40,50 @@ If you write me (on support forum etc.), be patient, please. I work on this plug
40
 
41
  == Frequently Asked Questions ==
42
  = Why to install this plugin? =
43
- * No ads & user tracking
44
- * Only 1 simple script file
45
- * New (hidden) reCAPTCHA v3
46
  * Possibility to replace v3 reCAPTCHA badge by text
47
  * reCAPTCHA language based on WordPress settings
48
  * Works in countries where Google domain is blocked
49
- * Emergency reCAPTCHA deactivate link
 
 
 
 
 
 
50
 
51
  = How to disable this plugin? =
52
  Use standard WordPress Plugins page. In emergency case, rename plugin folder under /wp-content/plugins/ over FTP access or use emergency reCAPTCHA deactivate link.
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  == Screenshots ==
55
  1. New comment
56
  2. New password
@@ -61,6 +94,10 @@ Use standard WordPress Plugins page. In emergency case, rename plugin folder und
61
  7. Emergency reCAPTCHA deactivate link
62
 
63
  == Changelog ==
 
 
 
 
64
  = 3.9 =
65
  * Bugfix: reCAPTCHA verification has been rewritten. More reliable and prevents brute force attacks now.
66
 
3
  Tags: recaptcha, spam, protect, google, invisible
4
  Requires at least: 4.6
5
  Tested up to: 6.0
6
+ Stable tag: 4.0
7
+ Requires PHP: 7.2
8
  License: GPLv3
9
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
10
  Donate link: https://www.paypal.me/NovaMi
40
 
41
  == Frequently Asked Questions ==
42
  = Why to install this plugin? =
43
+ * No ads, user tracking, send statistics neither survey
44
+ * Only arround 20kB size (without readme file)
45
+ * Google reCAPTCHA v3 (invisible) support
46
  * Possibility to replace v3 reCAPTCHA badge by text
47
  * reCAPTCHA language based on WordPress settings
48
  * Works in countries where Google domain is blocked
49
+ * Emergency reCAPTCHA deactivate link for admin
50
+ * Hooks - Possibility to customize where reCAPTCHA will be rendered and verified
51
+
52
+ = In case you have a problem =
53
+ 1. Important message could be shown in browser console (F12) on problematic page
54
+ 2. Double check if you have correct keys in settings - is those keys for correct version reCAPTCHA?
55
+ 3. Try to create new website in Google reCAPTCHA console (and use different website name)
56
 
57
  = How to disable this plugin? =
58
  Use standard WordPress Plugins page. In emergency case, rename plugin folder under /wp-content/plugins/ over FTP access or use emergency reCAPTCHA deactivate link.
59
 
60
+ = How to use hooks? =
61
+ For example, you can use this in your global functions.php file:
62
+
63
+ <pre><code>
64
+ function customSgrRenderList(array $list): array //Where reCAPTCHA is rendered
65
+ {
66
+ //unset($list[0]);
67
+ $list[] = 'register_form';
68
+
69
+ return $list;
70
+ }
71
+
72
+ add_action('sgr_render_list', 'customSgrRenderList');
73
+
74
+ function customSgrVerifyList(array $list): array //Where reCAPTCHA is verified
75
+ {
76
+ //unset($list[0]);
77
+ $list[] = 'lostpassword_post';
78
+
79
+ return $list;
80
+ }
81
+
82
+ add_action('sgr_verify_list', 'customSgrVerifyList');
83
+ </code></pre>
84
+
85
+ Variable $list is array of default hooks, indexed by numbers.
86
+
87
  == Screenshots ==
88
  1. New comment
89
  2. New password
94
  7. Emergency reCAPTCHA deactivate link
95
 
96
  == Changelog ==
97
+ = 4.0 =
98
+ * Warning: I don't recommend Google reCAPTCHA v2. You should enable v3 in plugin settings (requires different keys)!
99
+ * New: Hooks - sgr_render_list + sgr_verify_list. You are able to customize where Google reCAPTCHA will be rendered and verified - e.g. via functions.php.
100
+
101
  = 3.9 =
102
  * Bugfix: reCAPTCHA verification has been rewritten. More reliable and prevents brute force attacks now.
103
 
sgr.js CHANGED
@@ -2,7 +2,7 @@ function sgr_2() {
2
  console.log('SGR_2 loaded!');
3
  let recaptcha = document.getElementsByClassName('sgr-main');
4
  for (let i = 0; i < recaptcha.length; i++) {
5
- grecaptcha.render(recaptcha.item(i), {'sitekey': sgr_main.sgr_site_key});
6
  }
7
  }
8
 
@@ -10,7 +10,7 @@ function sgr_3() {
10
  console.log('SGR_3 loaded!');
11
  let actionName = window.location.pathname;
12
  actionName = actionName.replace(/[^a-zA-Z/]/g, '_');
13
- grecaptcha.execute(sgr_main.sgr_site_key, {action: 'sgr_' + actionName}).then(function (token) {
14
  let recaptcha = document.getElementsByClassName('sgr-main');
15
  for (let i = 0; i < recaptcha.length; i++) {
16
  recaptcha.item(i).value = token;
@@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', function (event) {
31
  let sgr_site_key = document.getElementById('sgr_site_key');
32
  let sgr_secret_key = document.getElementById('sgr_secret_key');
33
 
34
- if (sgr_site_key.value === sgr_main.sgr_site_key) {
35
  sgr_site_key.value = '';
36
  sgr_secret_key.value = '';
37
  }
2
  console.log('SGR_2 loaded!');
3
  let recaptcha = document.getElementsByClassName('sgr-main');
4
  for (let i = 0; i < recaptcha.length; i++) {
5
+ grecaptcha.render(recaptcha.item(i), {'sitekey': sgr.sgr_site_key});
6
  }
7
  }
8
 
10
  console.log('SGR_3 loaded!');
11
  let actionName = window.location.pathname;
12
  actionName = actionName.replace(/[^a-zA-Z/]/g, '_');
13
+ grecaptcha.execute(sgr.sgr_site_key, {action: 'sgr_' + actionName}).then(function (token) {
14
  let recaptcha = document.getElementsByClassName('sgr-main');
15
  for (let i = 0; i < recaptcha.length; i++) {
16
  recaptcha.item(i).value = token;
31
  let sgr_site_key = document.getElementById('sgr_site_key');
32
  let sgr_secret_key = document.getElementById('sgr_secret_key');
33
 
34
+ if (sgr_site_key.value === sgr.sgr_site_key) {
35
  sgr_site_key.value = '';
36
  sgr_secret_key.value = '';
37
  }
simple-google-recaptcha.php CHANGED
@@ -2,26 +2,24 @@
2
  /*
3
  * Plugin Name: Simple Google reCAPTCHA
4
  * Description: Simply protect your WordPress against spam comments and brute-force attacks, thanks to Google reCAPTCHA!
5
- * Version: 3.9
6
  * Author: Michal Novák
7
  * Author URI: https://www.novami.cz
8
  * License: GPLv3
9
  * Text Domain: simple-google-recaptcha
10
  */
11
 
12
- use SimpleGoogleRecaptchaEntity as Entity;
13
-
14
 
15
  if (!defined('ABSPATH')) {
16
  die('Direct access not allowed!');
17
  }
18
 
19
- include sprintf('%s/entity.php', dirname(__FILE__));
20
-
21
  /**
22
- * Class SimpleGoogleRecaptcha
 
23
  */
24
- class SimpleGoogleRecaptcha
25
  {
26
  /** @var string */
27
  const UPDATE = 'update';
@@ -30,21 +28,9 @@ class SimpleGoogleRecaptcha
30
  const DISABLE = 'disable';
31
 
32
  /** @var string */
33
- const KEY_SUFIX = '_key';
34
-
35
- /** @var string */
36
- const SGR_V2 = 'v2 "I\'m not a robot" Checkbox';
37
-
38
- /** @var string */
39
- const SGR_V3 = 'v3';
40
-
41
- /** @var string */
42
- const SGR_MAIN = 'sgr_main';
43
 
44
- /** @var string */
45
- const SGR_ACTION = 'sgr_action';
46
-
47
- /** @var SimpleGoogleRecaptcha */
48
  public static $instance;
49
 
50
  /** @var string */
@@ -54,7 +40,7 @@ class SimpleGoogleRecaptcha
54
  private $options;
55
 
56
  /**
57
- * SimpleGoogleRecaptcha constructor.
58
  */
59
  private function __construct()
60
  {
@@ -63,10 +49,12 @@ class SimpleGoogleRecaptcha
63
  }
64
 
65
  /**
66
- * @return SimpleGoogleRecaptcha
67
  */
68
- public static function getInstance()
69
  {
 
 
70
  if (!self::$instance instanceof self) {
71
  self::$instance = new self();
72
  }
@@ -78,31 +66,56 @@ class SimpleGoogleRecaptcha
78
  * @param int $type
79
  * @return int
80
  */
81
- private function getOptionFilter($type)
82
  {
83
  return $type === Entity::INT ? FILTER_SANITIZE_NUMBER_INT : FILTER_SANITIZE_FULL_SPECIAL_CHARS;
84
  }
85
 
86
  /**
 
 
 
 
 
 
 
 
 
 
 
87
  * @return void
88
  */
89
- private function loadSettings()
90
  {
91
- foreach ($this->options as $id => $option) {
92
- $type = $option->getType();
93
- $filter = $this->getOptionFilter($type);
94
- $filteredValue = filter_var(get_option($id), $filter);
95
- $option->setValue($type === Entity::INT ? intval($filteredValue) : strval($filteredValue));
 
 
 
 
96
  }
97
  }
98
 
99
  /**
100
- * @param string $id
101
- * @return int|string
102
  */
103
- private function getOptionValue($id)
104
  {
105
- return $this->options[$id]->getValue();
 
 
 
 
 
 
 
 
 
 
106
  }
107
 
108
  /**
@@ -115,18 +128,28 @@ class SimpleGoogleRecaptcha
115
  $this->options = [
116
  Entity::SITE_KEY => new Entity(__('Site Key', 'simple-google-recaptcha'), Entity::STRING),
117
  Entity::SECRET_KEY => new Entity(__('Secret Key', 'simple-google-recaptcha'), Entity::STRING),
118
- Entity::LOGIN_DISABLE => new Entity(__('Disable on login form', 'simple-google-recaptcha'), Entity::INT),
119
- Entity::VERSION => new Entity(__('Enable reCAPTCHA v3', 'simple-google-recaptcha'), Entity::INT),
120
- Entity::BADGE_HIDE => new Entity(__('Hide reCAPTCHA v3 badge', 'simple-google-recaptcha'), Entity::INT),
121
  ];
122
 
123
  $this->updateSettings();
124
 
125
- $this->loadSettings();
 
 
 
 
 
 
 
 
 
126
 
127
  $this->disableProtection();
128
 
129
- $this->enqueueMain();
 
130
 
131
  $this->frontend();
132
 
@@ -144,13 +167,14 @@ class SimpleGoogleRecaptcha
144
 
145
  if ($postAction === self::UPDATE && current_user_can('manage_options')) {
146
  $hash = null;
 
147
  foreach ($this->options as $key => $option) {
148
  $postValue = filter_input(INPUT_POST, $key, $this->getOptionFilter($option->getType()));
149
 
150
  if ($postValue) {
151
  update_option($key, $postValue);
152
 
153
- if (substr($key, -strlen(self::KEY_SUFIX)) === self::KEY_SUFIX) {
154
  $hash .= $postValue;
155
  }
156
  } else {
@@ -165,22 +189,24 @@ class SimpleGoogleRecaptcha
165
  }
166
 
167
  /**
168
- * @param $links
169
  * @return array
170
  */
171
- public function actionLinks($links)
172
  {
173
  return array_merge(['settings' => sprintf('<a href="options-general.php%s">%s</a>', Entity::PAGE_QUERY, __('Settings', 'simple-google-recaptcha'))], $links);
174
  }
175
 
176
  /**
177
- * @param $plugin
178
  * @return void
179
  */
180
- public function activation($plugin)
181
  {
182
  if ($plugin === plugin_basename(__FILE__) && (!get_option(Entity::SITE_KEY) || !get_option(Entity::SECRET_KEY))) {
183
- exit(wp_redirect(admin_url(sprintf('options-general.php%s', Entity::PAGE_QUERY))));
 
 
184
  }
185
  }
186
 
@@ -191,14 +217,14 @@ class SimpleGoogleRecaptcha
191
  {
192
  echo sprintf('<div class="wrap"><h1>%s - %s</h1><form method="post" action="%s">', $this->pluginName, __('Settings', 'simple-google-recaptcha'), Entity::PAGE_QUERY);
193
 
194
- settings_fields('sgr_header_section');
195
- do_settings_sections('sgr_options');
196
 
197
  echo sprintf('<input type="hidden" name="%s" value="%s">', self::SGR_ACTION, self::UPDATE);
198
 
199
  submit_button();
200
 
201
- echo sprintf('%s</form>%s</div>', PHP_EOL, $this->messageProtectionStatus());
202
  }
203
 
204
  /**
@@ -206,92 +232,80 @@ class SimpleGoogleRecaptcha
206
  */
207
  public function adminMenu()
208
  {
209
- add_submenu_page('options-general.php', $this->pluginName, 'Google reCAPTCHA', 'manage_options', 'sgr_options', [$this, 'optionsPage']);
210
  add_action('admin_init', [$this, 'displayOptions']);
211
  }
212
 
213
  /**
214
  * @return void
215
  */
216
- public function display_sgr_site_key()
217
  {
218
- echo sprintf('<input type="text" name="%1$s" class="regular-text" id="%1$s" value="%2$s" />', Entity::SITE_KEY, $this->getOptionValue(Entity::SITE_KEY));
219
- }
220
 
221
- /**
222
- * @return void
223
- */
224
- public function display_sgr_secret_key()
225
- {
226
- echo sprintf('<input type="text" name="%1$s" class="regular-text" id="%1$s" value="%2$s" />', Entity::SECRET_KEY, $this->getOptionValue(Entity::SECRET_KEY));
227
  }
228
 
229
  /**
230
  * @return void
231
  */
232
- public function display_sgr_login_disable()
233
  {
234
- echo sprintf('<input type="checkbox" name="%1$s" id="%1$s" value="1" %2$s />', Entity::LOGIN_DISABLE, checked(1, $this->getOptionValue(Entity::LOGIN_DISABLE), false));
235
- }
236
 
237
- /**
238
- * @return void
239
- */
240
- public function display_sgr_version()
241
- {
242
- echo sprintf('<input type="checkbox" name="%1$s" id="%1$s" value="3" %2$s />', Entity::VERSION, checked(3, $this->getOptionValue(Entity::VERSION), false));
243
- }
244
 
245
- /**
246
- * @return void
247
- */
248
- public function display_sgr_badge_hide()
249
- {
250
- echo sprintf('<input type="checkbox" name="%1$s" id="%1$s" value="1" %2$s />', Entity::BADGE_HIDE, checked(1, $this->getOptionValue(Entity::BADGE_HIDE), false));
251
  }
252
 
253
  /**
254
- * @return void
 
255
  */
256
- public function displayOptions()
257
  {
258
- add_settings_section('sgr_header_section', __('Google reCAPTCHA keys', 'simple-google-recaptcha'), [], 'sgr_options');
 
 
 
 
 
 
 
259
 
260
- foreach ($this->options as $key => $option) {
261
- add_settings_field($key, $option->getName(), [$this, sprintf('display_%s', $key)], 'sgr_options', 'sgr_header_section');
262
- register_setting('sgr_header_section', $key);
263
  }
264
- }
265
-
266
- /**
267
- * @return void
268
- */
269
- public function enqueueMain()
270
- {
271
- $jsName = 'sgr.js';
272
- $jsPath = sprintf('%s%s', plugin_dir_path(__FILE__), $jsName);
273
- wp_enqueue_script(self::SGR_MAIN, sprintf('%s%s', plugin_dir_url(__FILE__), $jsName), [], filemtime($jsPath));
274
-
275
- wp_localize_script(self::SGR_MAIN, self::SGR_MAIN, [Entity::SITE_KEY => $this->getOptionValue(Entity::SITE_KEY)]);
276
 
277
- $cssName = 'sgr.css';
278
- $cssPath = sprintf('%s%s', plugin_dir_path(__FILE__), $cssName);
279
- wp_enqueue_style(self::SGR_MAIN, sprintf('%s%s', plugin_dir_url(__FILE__), $cssName), [], filemtime($cssPath));
280
  }
281
 
282
  /**
283
- * @return void
 
284
  */
285
- public function enqueueScripts()
286
  {
287
- $apiUrlBase = sprintf('https://www.recaptcha.net/recaptcha/api.js?hl=%s', get_locale());
288
- $jsUrl = sprintf('%s&onload=sgr_2&render=explicit', $apiUrlBase);
 
 
 
 
 
289
 
290
- if ($this->getOptionValue(Entity::VERSION) === 3) {
291
- $jsUrl = sprintf('%s&render=%s&onload=sgr_3', $apiUrlBase, $this->getOptionValue(Entity::SITE_KEY));
292
  }
293
 
294
- wp_enqueue_script('sgr_recaptcha', $jsUrl, [], time());
295
  }
296
 
297
  /**
@@ -304,79 +318,50 @@ class SimpleGoogleRecaptcha
304
  $recaptchaSecretKey = $this->getOptionValue(Entity::SECRET_KEY);
305
 
306
  if ($rcpActivate && $recaptchaSiteKey && $recaptchaSecretKey) {
307
- $sgr_display_list = [
308
- 'bp_after_signup_profile_fields',
309
- 'comment_form_after_fields',
310
- 'lostpassword_form',
311
- 'register_form',
312
- 'woocommerce_lostpassword_form',
313
- 'woocommerce_register_form'
314
- ];
315
 
316
- $sgr_verify_list = [
317
- 'bp_signup_validate',
318
- 'lostpassword_post',
319
- 'preprocess_comment',
320
- 'registration_errors',
321
- 'woocommerce_register_post'
322
- ];
323
-
324
- if (!$this->getOptionValue(Entity::LOGIN_DISABLE)) {
325
- array_push($sgr_display_list, 'login_form', 'woocommerce_login_form');
326
- $sgr_verify_list[] = 'authenticate';
327
  }
328
 
329
- $sgrDisplay = $this->getOptionValue(Entity::VERSION) === 3 ? 'v3Display' : 'v2Display';
330
-
331
- foreach ($sgr_display_list as $sgr_display) {
332
- add_action($sgr_display, [$this, 'enqueueScripts']);
333
- add_action($sgr_display, [$this, $sgrDisplay]);
334
- }
335
-
336
- foreach ($sgr_verify_list as $sgr_verify) {
337
- add_action($sgr_verify, [$this, 'verify']);
338
  }
339
  }
340
  }
341
 
342
  /**
343
- * @return void
344
  */
345
- public function v2Display()
346
  {
347
- $this->displayDisableProtection();
 
 
 
348
 
349
- echo '<div class="sgr-main"></div>';
 
 
350
  }
351
 
352
  /**
353
- * @return void
354
  */
355
- public function v3Display()
356
  {
357
- $badgeText = null;
358
 
359
  if ($this->getOptionValue(Entity::BADGE_HIDE)) {
360
- $cssName = 'sgr_hide.css';
361
- $cssPath = sprintf('%s%s', plugin_dir_path(__FILE__), $cssName);
362
- wp_enqueue_style('sgr_hide', sprintf('%s%s', plugin_dir_url(__FILE__), $cssName), [], filemtime($cssPath));
363
 
364
- $badgeText = sprintf('%s<p class="sgr-infotext">%s</p>', PHP_EOL, __('This site is protected by reCAPTCHA and the Google <a href="https://policies.google.com/privacy">Privacy Policy</a> and <a href="https://policies.google.com/terms">Terms of Service</a> apply.', 'simple-google-recaptcha'));
 
365
  }
366
 
367
- $this->displayDisableProtection();
368
-
369
- echo sprintf('<input type="hidden" name="g-recaptcha-response" class="sgr-main">%s', $badgeText);
370
- }
371
-
372
- /**
373
- * @return void
374
- */
375
- private function displayDisableProtection()
376
- {
377
- if ($this->adminCookieHash()) {
378
- echo sprintf('<p class="sgr-infotext"><a href="?%s=%s">%s</a></p>', self::SGR_ACTION, self::DISABLE, __('Emergency reCAPTCHA deactivate', 'simple-google-recaptcha'));
379
- }
380
  }
381
 
382
  /**
@@ -394,16 +379,17 @@ class SimpleGoogleRecaptcha
394
 
395
  foreach ($keys as $key) {
396
  delete_option($key);
 
397
  $this->options[$key]->setValue('');
398
  }
399
  }
400
  }
401
 
402
  /**
403
- * @param $error_code
404
- * @return string|void
405
  */
406
- private function errorMessage($error_code)
407
  {
408
  switch ($error_code) {
409
  case 'missing-input-secret':
@@ -424,21 +410,39 @@ class SimpleGoogleRecaptcha
424
  }
425
 
426
  /**
427
- * @param $response
428
- * @return array|mixed
429
  */
430
- private function recaptchaResponseParse($response)
431
  {
432
  $secretKey = $this->getOptionValue(Entity::SECRET_KEY);
433
  $rcpUrl = sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s', $secretKey, $response);
434
- $response = (array)wp_remote_get($rcpUrl);
435
 
436
- $falseResponse = [
437
  'success' => false,
438
  'error-codes' => ['general-fail']
439
  ];
440
 
441
- return isset($response['body']) ? json_decode($response['body'], 1) : $falseResponse;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  }
443
 
444
  /**
@@ -449,23 +453,17 @@ class SimpleGoogleRecaptcha
449
  {
450
  if (!empty($_POST)) {
451
  $response = strval(filter_input(INPUT_POST, 'g-recaptcha-response', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
452
- $parsedResponse = $this->recaptchaResponseParse($response);
 
453
 
454
- if (isset($parsedResponse['success']) && $parsedResponse['success'] === true) {
455
- return $input;
456
  } else {
457
- $errorTitle = 'reCAPTCHA';
458
- $errorParams = ['response' => 403, 'back_link' => 1];
459
- $failedMsg = '<p><strong>%s:</strong> Google reCAPTCHA %s. %s</p>';
460
- $error = __('Error', 'simple-google-recaptcha');
461
- $verificationFailed = __('verification failed', 'simple-google-recaptcha');
462
-
463
- if (!$response) {
464
- wp_die(sprintf($failedMsg, $error, $verificationFailed, __('Do you have JavaScript enabled?', 'simple-google-recaptcha')), $errorTitle, $errorParams);
465
  }
466
 
467
- $recaptcha_error_code = isset($parsedResponse['error-codes'][0]) ? $parsedResponse['error-codes'][0] : null;
468
- wp_die(sprintf($failedMsg, $error, $verificationFailed, $this->errorMessage($recaptcha_error_code)), $errorTitle, $errorParams);
469
  }
470
  }
471
  }
@@ -473,7 +471,7 @@ class SimpleGoogleRecaptcha
473
  /**
474
  * @return string
475
  */
476
- public function messageProtectionStatus()
477
  {
478
  $class = 'warning';
479
  $name = __('Notice', 'simple-google-recaptcha');
@@ -487,7 +485,7 @@ class SimpleGoogleRecaptcha
487
  $msg = __('You have to <a href="https://www.google.com/recaptcha/admin" rel="external">register your domain</a>, get required Google reCAPTCHA keys %s and save them bellow.', 'simple-google-recaptcha');
488
  }
489
 
490
- $type = $this->getOptionValue(Entity::VERSION) === 3 ? self::SGR_V3 : self::SGR_V2;
491
 
492
  return sprintf('<div class="notice notice-%s"><p><strong>%s:</strong> Google reCAPTCHA %s!</p><p>%s</p></div>', $class, $name, $status, sprintf($msg, $type));
493
  }
@@ -495,16 +493,12 @@ class SimpleGoogleRecaptcha
495
  /**
496
  * @return bool
497
  */
498
- public function adminCookieHash()
499
  {
500
  $cookieHash = filter_input(INPUT_COOKIE, Entity::HASH, FILTER_SANITIZE_SPECIAL_CHARS);
501
 
502
- if ($cookieHash === md5($this->getOptionValue(Entity::SITE_KEY) . $this->getOptionValue(Entity::SECRET_KEY))) {
503
- return true;
504
- } else {
505
- return false;
506
- }
507
  }
508
  }
509
 
510
- SimpleGoogleRecaptcha::getInstance();
2
  /*
3
  * Plugin Name: Simple Google reCAPTCHA
4
  * Description: Simply protect your WordPress against spam comments and brute-force attacks, thanks to Google reCAPTCHA!
5
+ * Version: 4.0
6
  * Author: Michal Novák
7
  * Author URI: https://www.novami.cz
8
  * License: GPLv3
9
  * Text Domain: simple-google-recaptcha
10
  */
11
 
12
+ namespace NovaMi\WordPress\SimpleGoogleRecaptcha;
 
13
 
14
  if (!defined('ABSPATH')) {
15
  die('Direct access not allowed!');
16
  }
17
 
 
 
18
  /**
19
+ * Class Core
20
+ * @package NovaMi\WordPress\SimpleGoogleRecaptcha
21
  */
22
+ class Core
23
  {
24
  /** @var string */
25
  const UPDATE = 'update';
28
  const DISABLE = 'disable';
29
 
30
  /** @var string */
31
+ const SGR_ACTION = Entity::PREFIX . 'action';
 
 
 
 
 
 
 
 
 
32
 
33
+ /** @var Core */
 
 
 
34
  public static $instance;
35
 
36
  /** @var string */
40
  private $options;
41
 
42
  /**
43
+ * Core constructor.
44
  */
45
  private function __construct()
46
  {
49
  }
50
 
51
  /**
52
+ * @return Core
53
  */
54
+ public static function getInstance(): Core
55
  {
56
+ require_once dirname(__FILE__) . '/entity.php';
57
+
58
  if (!self::$instance instanceof self) {
59
  self::$instance = new self();
60
  }
66
  * @param int $type
67
  * @return int
68
  */
69
+ private function getOptionFilter(int $type): int
70
  {
71
  return $type === Entity::INT ? FILTER_SANITIZE_NUMBER_INT : FILTER_SANITIZE_FULL_SPECIAL_CHARS;
72
  }
73
 
74
  /**
75
+ * @param string $id
76
+ * @return int|string
77
+ */
78
+ private function getOptionValue(string $id)
79
+ {
80
+ return $this->options[$id]->getValue() ?? '';
81
+ }
82
+
83
+ /**
84
+ * @param string $ext
85
+ * @param string $name
86
  * @return void
87
  */
88
+ private function enqueue(string $ext = 'js', string $name = 'sgr')
89
  {
90
+ $fileName = $name . '.' . $ext;
91
+ $dirPath = plugin_dir_path(__FILE__) . $fileName;
92
+ $dirUrl = plugin_dir_url(__FILE__) . $fileName;
93
+
94
+ if ($ext === 'js') {
95
+ wp_enqueue_script($name, $dirUrl, [], filemtime($dirPath));
96
+ wp_localize_script($name, $name, [Entity::SITE_KEY => $this->getOptionValue(Entity::SITE_KEY)]);
97
+ } else {
98
+ wp_enqueue_style($name, $dirUrl, [], filemtime($dirPath));
99
  }
100
  }
101
 
102
  /**
103
+ * @param array $atts
104
+ * @return void
105
  */
106
+ public function displayInput(array $atts)
107
  {
108
+ $key = $atts['key'];
109
+ $type = $atts['type'];
110
+ $val = $this->getOptionValue($key);
111
+
112
+ if ($type === Entity::INT) {
113
+ $defaultVal = $key === Entity::VERSION ? 3 : 1;
114
+
115
+ echo sprintf('<input type="checkbox" name="%1$s" id="%1$s" value="%2$d" %3$s />', $key, $defaultVal, checked($defaultVal, $val, false));
116
+ } else {
117
+ echo sprintf('<input type="text" name="%1$s" class="regular-text" id="%1$s" value="%2$s" />', $key, $val);
118
+ }
119
  }
120
 
121
  /**
128
  $this->options = [
129
  Entity::SITE_KEY => new Entity(__('Site Key', 'simple-google-recaptcha'), Entity::STRING),
130
  Entity::SECRET_KEY => new Entity(__('Secret Key', 'simple-google-recaptcha'), Entity::STRING),
131
+ Entity::LOGIN_DISABLE => new Entity(__('Disable on login form', 'simple-google-recaptcha')),
132
+ Entity::VERSION => new Entity(__('Enable reCAPTCHA v3', 'simple-google-recaptcha')),
133
+ Entity::BADGE_HIDE => new Entity(__('Hide reCAPTCHA v3 badge', 'simple-google-recaptcha'))
134
  ];
135
 
136
  $this->updateSettings();
137
 
138
+ /**
139
+ * @var string $id
140
+ * @var Entity $option
141
+ */
142
+ foreach ($this->options as $id => $option) {
143
+ $type = $option->getType();
144
+ $filter = $this->getOptionFilter($type);
145
+ $filteredValue = filter_var(get_option($id), $filter);
146
+ $option->setValue($type === Entity::INT ? intval($filteredValue) : strval($filteredValue));
147
+ }
148
 
149
  $this->disableProtection();
150
 
151
+ $this->enqueue();
152
+ $this->enqueue('css');
153
 
154
  $this->frontend();
155
 
167
 
168
  if ($postAction === self::UPDATE && current_user_can('manage_options')) {
169
  $hash = null;
170
+
171
  foreach ($this->options as $key => $option) {
172
  $postValue = filter_input(INPUT_POST, $key, $this->getOptionFilter($option->getType()));
173
 
174
  if ($postValue) {
175
  update_option($key, $postValue);
176
 
177
+ if (substr($key, -strlen('_key')) === '_key') {
178
  $hash .= $postValue;
179
  }
180
  } else {
189
  }
190
 
191
  /**
192
+ * @param array $links
193
  * @return array
194
  */
195
+ public function actionLinks(array $links): array
196
  {
197
  return array_merge(['settings' => sprintf('<a href="options-general.php%s">%s</a>', Entity::PAGE_QUERY, __('Settings', 'simple-google-recaptcha'))], $links);
198
  }
199
 
200
  /**
201
+ * @param string $plugin
202
  * @return void
203
  */
204
+ public function activation(string $plugin)
205
  {
206
  if ($plugin === plugin_basename(__FILE__) && (!get_option(Entity::SITE_KEY) || !get_option(Entity::SECRET_KEY))) {
207
+ $adminUrl = admin_url('options-general.php' . Entity::PAGE_QUERY);
208
+
209
+ exit(wp_redirect($adminUrl));
210
  }
211
  }
212
 
217
  {
218
  echo sprintf('<div class="wrap"><h1>%s - %s</h1><form method="post" action="%s">', $this->pluginName, __('Settings', 'simple-google-recaptcha'), Entity::PAGE_QUERY);
219
 
220
+ settings_fields(Entity::PREFIX . 'header_section');
221
+ do_settings_sections(Entity::PREFIX . 'options');
222
 
223
  echo sprintf('<input type="hidden" name="%s" value="%s">', self::SGR_ACTION, self::UPDATE);
224
 
225
  submit_button();
226
 
227
+ echo sprintf('%s</form>%s</div>', PHP_EOL, $this->protectionStatus());
228
  }
229
 
230
  /**
232
  */
233
  public function adminMenu()
234
  {
235
+ add_submenu_page('options-general.php', $this->pluginName, 'Google reCAPTCHA', 'manage_options', Entity::PREFIX . 'options', [$this, 'optionsPage']);
236
  add_action('admin_init', [$this, 'displayOptions']);
237
  }
238
 
239
  /**
240
  * @return void
241
  */
242
+ public function displayOptions()
243
  {
244
+ add_settings_section(Entity::PREFIX . 'header_section', __('Google reCAPTCHA keys', 'simple-google-recaptcha'), [], Entity::PREFIX . 'options');
 
245
 
246
+ foreach ($this->options as $key => $option) {
247
+ $args = ['key' => $key, 'type' => $option->getType()];
248
+ add_settings_field($key, $option->getName(), [$this, 'displayInput'], Entity::PREFIX . 'options', Entity::PREFIX . 'header_section', $args);
249
+ register_setting(Entity::PREFIX . 'header_section', $key);
250
+ }
 
251
  }
252
 
253
  /**
254
  * @return void
255
  */
256
+ public function enqueueScripts()
257
  {
258
+ $apiUrlBase = sprintf('https://www.recaptcha.net/recaptcha/api.js?hl=%s', get_locale());
259
+ $jsUrl = sprintf('%s&onload=sgr_2&render=explicit', $apiUrlBase);
260
 
261
+ if ($this->getOptionValue(Entity::VERSION) === 3) {
262
+ $jsUrl = sprintf('%s&render=%s&onload=sgr_3', $apiUrlBase, $this->getOptionValue(Entity::SITE_KEY));
263
+ }
 
 
 
 
264
 
265
+ wp_enqueue_script(Entity::PREFIX . 'recaptcha', $jsUrl, [], time());
 
 
 
 
 
266
  }
267
 
268
  /**
269
+ * @param array|null $list
270
+ * @return array|string[]
271
  */
272
+ public function renderList(?array $list = []): array
273
  {
274
+ $list ?: $list = [
275
+ 'bp_after_signup_profile_fields',
276
+ 'comment_form_after_fields',
277
+ 'lostpassword_form',
278
+ 'register_form',
279
+ 'woocommerce_lostpassword_form',
280
+ 'woocommerce_register_form'
281
+ ];
282
 
283
+ if (!$this->getOptionValue(Entity::LOGIN_DISABLE)) {
284
+ array_push($list, 'login_form', 'woocommerce_login_form');
 
285
  }
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
+ return $list;
 
 
288
  }
289
 
290
  /**
291
+ * @param array|null $list
292
+ * @return array|string[]
293
  */
294
+ public function verifyList(?array $list = []): array
295
  {
296
+ $list ?: $list = [
297
+ 'bp_signup_validate',
298
+ 'lostpassword_post',
299
+ 'preprocess_comment',
300
+ 'registration_errors',
301
+ 'woocommerce_register_post'
302
+ ];
303
 
304
+ if (!$this->getOptionValue(Entity::LOGIN_DISABLE)) {
305
+ $list[] = 'authenticate';
306
  }
307
 
308
+ return $list;
309
  }
310
 
311
  /**
318
  $recaptchaSecretKey = $this->getOptionValue(Entity::SECRET_KEY);
319
 
320
  if ($rcpActivate && $recaptchaSiteKey && $recaptchaSecretKey) {
321
+ add_action(Entity::PREFIX . 'display_list', [$this, 'renderList']);
322
+ add_action(Entity::PREFIX . 'verify_list', [$this, 'verifyList']);
 
 
 
 
 
 
323
 
324
+ foreach (apply_filters(Entity::PREFIX . 'render_list', self::renderList()) as $display) {
325
+ add_action($display, [$this, 'enqueueScripts']);
326
+ add_action($display, [$this, 'render']);
 
 
 
 
 
 
 
 
327
  }
328
 
329
+ foreach (apply_filters(Entity::PREFIX . 'verify_list', self::verifyList()) as $verify) {
330
+ add_action($verify, [$this, 'verify']);
 
 
 
 
 
 
 
331
  }
332
  }
333
  }
334
 
335
  /**
336
+ * @return bool
337
  */
338
+ public function render(): bool
339
  {
340
+ if ($this->adminCookieHash()) {
341
+ $linkText = __('Emergency reCAPTCHA deactivate', 'simple-google-recaptcha');
342
+ echo sprintf('<p class="sgr-infotext"><a href="?%s=%s">%s</a></p>', self::SGR_ACTION, self::DISABLE, $linkText);
343
+ }
344
 
345
+ echo $this->getOptionValue(Entity::VERSION) === 3 ? self::v3Render() : '<div class="sgr-main"></div>';
346
+
347
+ return true;
348
  }
349
 
350
  /**
351
+ * @return string
352
  */
353
+ private function v3Render(): string
354
  {
355
+ $badgeReplacement = null;
356
 
357
  if ($this->getOptionValue(Entity::BADGE_HIDE)) {
358
+ $this->enqueue('css', Entity::PREFIX . 'hide');
 
 
359
 
360
+ $msg = __('This site is protected by reCAPTCHA and the Google <a href="https://policies.google.com/privacy">Privacy Policy</a> and <a href="https://policies.google.com/terms">Terms of Service</a> apply.', 'simple-google-recaptcha');
361
+ $badgeReplacement = sprintf('%s<p class="sgr-infotext">%s</p>', PHP_EOL, $msg);
362
  }
363
 
364
+ return sprintf('<input type="hidden" name="g-recaptcha-response" class="sgr-main">%s', $badgeReplacement);
 
 
 
 
 
 
 
 
 
 
 
 
365
  }
366
 
367
  /**
379
 
380
  foreach ($keys as $key) {
381
  delete_option($key);
382
+
383
  $this->options[$key]->setValue('');
384
  }
385
  }
386
  }
387
 
388
  /**
389
+ * @param string|null $error_code
390
+ * @return string
391
  */
392
+ private function errorMessage(?string $error_code): string
393
  {
394
  switch ($error_code) {
395
  case 'missing-input-secret':
410
  }
411
 
412
  /**
413
+ * @param string $response
414
+ * @return array
415
  */
416
+ private function responseParse(string $response): array
417
  {
418
  $secretKey = $this->getOptionValue(Entity::SECRET_KEY);
419
  $rcpUrl = sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s', $secretKey, $response);
420
+ $response = wp_remote_get($rcpUrl);
421
 
422
+ $failedResponse = [
423
  'success' => false,
424
  'error-codes' => ['general-fail']
425
  ];
426
 
427
+ if ($response instanceof \WP_Error) {
428
+ $failedResponse['error-msg'] = $response->get_error_message();
429
+ unset($response);
430
+ }
431
+
432
+ return isset($response['body']) ? (array)json_decode($response['body'], 1) : $failedResponse;
433
+ }
434
+
435
+ /**
436
+ * @param string $msg
437
+ * @return void
438
+ */
439
+ private function wpDie(string $msg)
440
+ {
441
+ $error = __('Error', 'simple-google-recaptcha');
442
+ $verificationFailed = __('verification failed', 'simple-google-recaptcha');
443
+ $errorParams = ['response' => 403, 'back_link' => 1];
444
+
445
+ wp_die(sprintf('<p><strong>%s:</strong> Google reCAPTCHA %s. %s</p>', $error, $verificationFailed, $msg), 'Forbidden by reCAPTCHA', $errorParams);
446
  }
447
 
448
  /**
453
  {
454
  if (!empty($_POST)) {
455
  $response = strval(filter_input(INPUT_POST, 'g-recaptcha-response', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
456
+ $parsedResponse = $this->responseParse($response);
457
+ $errorCode = $parsedResponse['error-codes'][0] ?? null;
458
 
459
+ if (($parsedResponse['success'] ?? null) !== true || $errorCode) {
460
+ $this->wpDie($parsedResponse['error-msg'] ?? $this->errorMessage($errorCode));
461
  } else {
462
+ if ($this->getOptionValue(Entity::VERSION) === 3 && floatval($parsedResponse['score'] ?? 0) < 0.5) {
463
+ $this->wpDie(__('You are probably not a human!', 'simple-google-recaptcha'));
 
 
 
 
 
 
464
  }
465
 
466
+ return $input;
 
467
  }
468
  }
469
  }
471
  /**
472
  * @return string
473
  */
474
+ public function protectionStatus(): string
475
  {
476
  $class = 'warning';
477
  $name = __('Notice', 'simple-google-recaptcha');
485
  $msg = __('You have to <a href="https://www.google.com/recaptcha/admin" rel="external">register your domain</a>, get required Google reCAPTCHA keys %s and save them bellow.', 'simple-google-recaptcha');
486
  }
487
 
488
+ $type = $this->getOptionValue(Entity::VERSION) === 3 ? 'v3' : 'v2 "I\'m not a robot" Checkbox';
489
 
490
  return sprintf('<div class="notice notice-%s"><p><strong>%s:</strong> Google reCAPTCHA %s!</p><p>%s</p></div>', $class, $name, $status, sprintf($msg, $type));
491
  }
493
  /**
494
  * @return bool
495
  */
496
+ public function adminCookieHash(): bool
497
  {
498
  $cookieHash = filter_input(INPUT_COOKIE, Entity::HASH, FILTER_SANITIZE_SPECIAL_CHARS);
499
 
500
+ return $cookieHash === md5($this->getOptionValue(Entity::SITE_KEY) . $this->getOptionValue(Entity::SECRET_KEY));
 
 
 
 
501
  }
502
  }
503
 
504
+ Core::getInstance();
uninstall.php CHANGED
@@ -1,33 +1,34 @@
1
  <?php
2
 
3
- use SimpleGoogleRecaptchaEntity as Entity;
4
-
5
 
6
  if (!defined('WP_UNINSTALL_PLUGIN')) {
7
  die('Direct access not allowed');
8
  }
9
 
10
- include sprintf('%s/entity.php', dirname(__FILE__));
11
-
12
  /**
13
- * Class SimpleGoogleRecaptchaUninstall
 
14
  */
15
- class SimpleGoogleRecaptchaUninstall
16
  {
17
- /** @var string */
18
- const OPTIONS_PREFIX = 'sgr_';
19
-
20
- public function __construct()
21
  {
22
- $constants = (new ReflectionClass(Entity::class))->getConstants();
 
 
23
 
24
  foreach ($constants as $constant) {
25
- if (substr($constant, 0, strlen(self::OPTIONS_PREFIX)) === self::OPTIONS_PREFIX) {
26
- delete_option($constant);
27
 
 
 
28
  }
29
  }
30
  }
31
  }
32
 
33
- new SimpleGoogleRecaptchaUninstall();
1
  <?php
2
 
3
+ namespace NovaMi\WordPress\SimpleGoogleRecaptcha;
 
4
 
5
  if (!defined('WP_UNINSTALL_PLUGIN')) {
6
  die('Direct access not allowed');
7
  }
8
 
 
 
9
  /**
10
+ * Class Uninstall
11
+ * @package NovaMi\WordPress\SimpleGoogleRecaptcha
12
  */
13
+ class Uninstall
14
  {
15
+ /**
16
+ * @return void
17
+ */
18
+ public static function run()
19
  {
20
+ require_once dirname(__FILE__) . '/entity.php';
21
+
22
+ $constants = (new \ReflectionClass(Entity::class))->getConstants();
23
 
24
  foreach ($constants as $constant) {
25
+ $constPrefix = substr($constant, 0, strlen(Entity::PREFIX));
 
26
 
27
+ if ($constPrefix === Entity::PREFIX) {
28
+ delete_option($constant);
29
  }
30
  }
31
  }
32
  }
33
 
34
+ Uninstall::run();