PluginProbe
CryptX / 4.1.1
CryptX v4.1.1
4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 All 92 releases
cryptx / cryptx.php

cryptx.php in CryptX 4.1.1, at cryptx.php

381 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: CryptX
4 * Plugin URI: https://wordpress.org/plugins/cryptx/
5 * Description: CryptX encrypts email addresses in your posts, pages, comments, and text widgets to protect them from spam bots while keeping them readable for your visitors.
6 * Version: 4.1.1
7 * Requires at least: 6.7
8 * Tested up to: 7.0
9 * Requires PHP: 8.1
10 * Author: Ralf Weber
11 * Author URI: https://weber-nrw.de/
12 * License: GPL v2 or later
13 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
14 * Text Domain: cryptx
15 *
16 * CryptX is free software: you can redistribute it and/or modify
17 * it under the terms of the GNU General Public License as published by
18 * the Free Software Foundation, either version 2 of the License, or
19 * any later version.
20 *
21 * CryptX is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * You should have received a copy of the GNU General Public License
27 * along with CryptX. If not, see https://www.gnu.org/licenses/gpl-2.0.html.
28 *
29 * @package CryptX
30 * @since 1.0.0
31 */
32
33 // Prevent direct access
34 if (!defined('ABSPATH')) {
35 exit;
36 }
37
38 // Plugin constants
39 define('CRYPTX_VERSION', '4.1.1');
40 define('CRYPTX_PLUGIN_FILE', __FILE__);
41 define('CRYPTX_PLUGIN_BASENAME', plugin_basename(__FILE__));
42 define('CRYPTX_BASENAME', plugin_basename(__FILE__)); // Add this missing constant
43 define('CRYPTX_DIR_PATH', plugin_dir_path(__FILE__));
44 define('CRYPTX_DIR_URL', plugin_dir_url(__FILE__));
45 define('CRYPTX_BASEFOLDER', dirname(CRYPTX_PLUGIN_BASENAME));
46
47 // Minimum requirements. Keep these in sync with the plugin header above and
48 // with "Requires at least" / "Requires PHP" in readme.txt. They are defined
49 // once and used both for the check and for the notice, so the number shown to
50 // the user cannot drift away from the number actually enforced.
51 define('CRYPTX_MIN_PHP', '8.1');
52 define('CRYPTX_MIN_WP', '6.7');
53
54 /**
55 * Shows an admin notice to whoever is in a position to act on it.
56 *
57 * Registered on both admin_notices and network_admin_notices. Without the
58 * second, a network administrator working in the network backend -- the only
59 * person who can deactivate a network-activated plugin -- never saw that the
60 * server runs too old a PHP, or that a class file is missing. The capability
61 * differs per screen: on a site it is activate_plugins, in the network backend
62 * manage_network_plugins, which a mere site administrator does not hold.
63 *
64 * @param string $message The message, already translated and unescaped.
65 *
66 * @return void
67 */
68 function cryptx_admin_notice(string $message): void
69 {
70 $render = static function () use ($message): void {
71 $capability = is_network_admin() ? 'manage_network_plugins' : 'activate_plugins';
72
73 if (!current_user_can($capability)) {
74 return;
75 }
76
77 printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($message));
78 };
79
80 add_action('admin_notices', $render);
81 add_action('network_admin_notices', $render);
82 }
83
84 if (version_compare(PHP_VERSION, CRYPTX_MIN_PHP, '<')) {
85 cryptx_admin_notice(sprintf(
86 /* translators: %1$s: Required PHP version, %2$s: Current PHP version */
87 __('CryptX requires PHP version %1$s or higher. You are running version %2$s. Please update PHP.', 'cryptx'),
88 CRYPTX_MIN_PHP,
89 PHP_VERSION
90 ));
91 return;
92 }
93
94 // WordPress version check
95 global $wp_version;
96 if (version_compare($wp_version, CRYPTX_MIN_WP, '<')) {
97 cryptx_admin_notice(sprintf(
98 /* translators: %1$s: Required WordPress version, %2$s: Current WordPress version */
99 __('CryptX requires WordPress version %1$s or higher. You are running version %2$s. Please update WordPress.', 'cryptx'),
100 CRYPTX_MIN_WP,
101 $GLOBALS['wp_version']
102 ));
103 return;
104 }
105
106 // Autoloader for plugin classes
107 spl_autoload_register(function ($class) {
108 // Check if the class belongs to our namespace
109 if (strpos($class, 'CryptX\\') !== 0) {
110 return;
111 }
112
113 // Remove namespace prefix
114 $class = substr($class, 7);
115
116 // Convert namespace separators to directory separators
117 $class = str_replace('\\', DIRECTORY_SEPARATOR, $class);
118
119 // Build the full path
120 $file = CRYPTX_DIR_PATH . 'classes' . DIRECTORY_SEPARATOR . $class . '.php';
121
122 // Include the file if it exists
123 if (file_exists($file)) {
124 require_once $file;
125 }
126 });
127
128 // The settings screen posts nothing: it talks to the REST routes in
129 // CryptX\Admin\RestController, which carry their own capability check and are
130 // covered by WordPress' REST nonce. The global cryptx_nonce_check() that used
131 // to sit here guarded $_POST['cryptX_var'], a key nothing has sent since the
132 // old settings form was removed in 4.1.0 -- a dead guard next to a live one is
133 // a trap for whoever wires up the next form.
134
135 // Initialize the plugin
136 add_action('plugins_loaded', function() {
137 // Check if all required classes can be loaded
138 $requiredClasses = [
139 'CryptX\\CryptX',
140 'CryptX\\Config',
141 'CryptX\\SecureEncryption',
142 'CryptX\\Admin\\SettingsPage',
143 'CryptX\\Admin\\SettingsSchema',
144 'CryptX\\Admin\\RestController',
145 ];
146
147 $missingClasses = [];
148 foreach ($requiredClasses as $class) {
149 if (!class_exists($class)) {
150 $missingClasses[] = $class;
151 }
152 }
153
154 if (!empty($missingClasses)) {
155 cryptx_admin_notice(
156 __('CryptX: Missing required classes: ', 'cryptx') . implode(', ', $missingClasses)
157 );
158 return;
159 }
160
161 // Initialize the main plugin class
162 try {
163 $cryptx_instance = CryptX\CryptX::get_instance();
164 $cryptx_instance->startCryptX();
165 cryptx_register_action_links();
166 } catch (Exception $e) {
167 cryptx_admin_notice(
168 __('CryptX initialization failed: ', 'cryptx') . $e->getMessage()
169 );
170 }
171 });
172
173 /**
174 * Runs a callback once for every site of the network, in batches.
175 *
176 * get_sites() without a limit pulls the whole network into memory, and a
177 * network can have thousands of sites. Same construction as uninstall.php,
178 * deliberately: the two do the same job at opposite ends of the plugin's life,
179 * and one of them being cleverer than the other only makes both harder to
180 * trust.
181 *
182 * On a single site the callback simply runs once.
183 *
184 * @param callable $callback Receives nothing; runs with the site switched in.
185 *
186 * @return void
187 */
188 function cryptx_for_each_site(callable $callback): void
189 {
190 if (!is_multisite()) {
191 $callback();
192
193 return;
194 }
195
196 $batch_size = 100;
197 $offset = 0;
198
199 do {
200 $site_ids = get_sites([
201 'fields' => 'ids',
202 'number' => $batch_size,
203 'offset' => $offset,
204 'orderby' => 'id',
205 'update_site_meta_cache' => false,
206 ]);
207
208 foreach ($site_ids as $site_id) {
209 switch_to_blog($site_id);
210
211 // finally, so a failing callback does not leave the blog stack
212 // switched for whatever runs next. It does not keep the loop
213 // going: an exception still travels upwards and the remaining
214 // sites are skipped. Catching it here would hide a broken site
215 // instead, and that is a trade to make deliberately, not in
216 // passing.
217 try {
218 $callback();
219 } finally {
220 restore_current_blog();
221 }
222 }
223
224 $offset += $batch_size;
225 } while (count($site_ids) === $batch_size);
226 }
227
228 /**
229 * Removes the plugin's transients from the site that is currently switched in.
230 *
231 * @return void
232 */
233 function cryptx_delete_transients(): void
234 {
235 global $wpdb;
236
237 // The LIKE patterns run through $wpdb->esc_like() and $wpdb->prepare().
238 // No user input is involved here, so this is not a hole, but "_" is a
239 // single-character wildcard in LIKE: unescaped, '_transient_cryptx_%' also
240 // matches names like 'Xtransient1cryptxZ...' belonging to other plugins.
241 // esc_like() turns those underscores into literal ones. The table name is
242 // an identifier, not a value, and therefore must stay outside prepare().
243 $transientLike = $wpdb->esc_like('_transient_cryptx_') . '%';
244 $transientTimeoutLike = $wpdb->esc_like('_transient_timeout_cryptx_') . '%';
245
246 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
247 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $transientLike));
248 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
249 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $transientTimeoutLike));
250 }
251
252 /**
253 * Sets up the plugin's options for the site that is currently switched in.
254 *
255 * @return void
256 */
257 function cryptx_install_site(): void
258 {
259 if (!class_exists('CryptX\\CryptX')) {
260 return;
261 }
262
263 CryptX\CryptX::get_instance()->installCryptX();
264 }
265
266 // Activation.
267 //
268 // $network_wide is true when someone ticks "Network Activate". WordPress then
269 // fires this hook exactly once, not once per site -- so without the loop, every
270 // site but the current one is left without its stored options and, more to the
271 // point, without the one-time migration of the pre-4.0 "cryptxoff" post meta.
272 // The plugin still works there, because the defaults fill in, but a site that
273 // carried excluded posts from an old version would silently lose them.
274 register_activation_hook(__FILE__, function ($network_wide = false) {
275 if ($network_wide) {
276 cryptx_for_each_site('cryptx_install_site');
277 } else {
278 cryptx_install_site();
279 }
280
281 flush_rewrite_rules();
282 });
283
284 // Deactivation. Same reason, other direction: the transients of every site but
285 // the current one used to survive a network deactivation.
286 register_deactivation_hook(__FILE__, function ($network_wide = false) {
287 if ($network_wide) {
288 cryptx_for_each_site('cryptx_delete_transients');
289 } else {
290 cryptx_delete_transients();
291 }
292 });
293
294 // A site created while the plugin is network-active gets the same treatment as
295 // one that existed at activation time. Without this the new site works -- the
296 // defaults see to that -- but nothing is ever written until someone saves, and
297 // the behaviour differs from every other site in the network for no reason
298 // anybody could see.
299 add_action('wp_initialize_site', function ($site) {
300 // The network option directly, not is_plugin_active_for_network(): that
301 // lives in wp-admin/includes/plugin.php, which is not loaded when a site is
302 // created over the REST API or WP-CLI -- exactly the paths that create sites
303 // in bulk.
304 $network_active = get_site_option('active_sitewide_plugins', []);
305
306 if (!isset($network_active[CRYPTX_PLUGIN_BASENAME])) {
307 return;
308 }
309
310 $site_id = is_object($site) ? (int) $site->blog_id : (int) $site;
311
312 switch_to_blog($site_id);
313 cryptx_install_site();
314 restore_current_blog();
315 }, 20);
316
317 // Add plugin action links.
318 //
319 // Registered inside the successful-initialisation path on purpose, not at the
320 // top level of this file. It refers to a class constant, and the plugins screen
321 // is exactly where someone goes to switch off a plugin whose class files are
322 // missing -- a fatal error there would take away the only lever they have.
323 // A plugin that did not initialise has no settings page to link to anyway.
324 function cryptx_register_action_links(): void
325 {
326 add_filter('plugin_action_links_' . CRYPTX_PLUGIN_BASENAME, function ($links) {
327 $settings_link = '<a href="' .
328 esc_url(admin_url('options-general.php?page=' . CryptX\Admin\SettingsPage::MENU_SLUG)) .
329 '">' . esc_html__('Settings', 'cryptx') . '</a>';
330 array_unshift($links, $settings_link);
331
332 return $links;
333 });
334 }
335
336 /**
337 * Encrypts the given content using the CryptX library and wraps it with a shortcode.
338 *
339 * @param string $content The content to be encrypted.
340 * @param array|null $args Optional arguments to customize the encryption process.
341 *
342 * @return string The encrypted content wrapped in the appropriate shortcode.
343 */
344 if (!function_exists('cryptx_encrypt')) {
345 function cryptx_encrypt(string $content, ?array $args = []): string
346 {
347 $cryptXInstance = Cryptx\CryptX::get_instance();
348 // $attributesString contains the escaped (esc_attr()) shortcode attributes from $args
349 // The signature allows null, convertArrayToArgumentString() does not.
350 $attributesString = $cryptXInstance->convertArrayToArgumentString($args ?? []);
351
352 // wp_kses_post() and not esc_html(): the caller passes content, and
353 // content in WordPress may carry markup. esc_html() turned a "<br>" in
354 // a theme field into a visible "&lt;br&gt;" -- reported in the support
355 // forum, and worked around there with html_entity_decode(), which
356 // undoes the plugin's own protection in the Unicode and entity modes.
357 // wp_kses_post keeps what a post may contain and drops the rest.
358 $shortcode = '[cryptx' . $attributesString . ']' . wp_kses_post($content) . '[/cryptx]';
359
360 return do_shortcode($shortcode);
361 }
362 }
363
364 /**
365 * Encrypts the given content using the CryptX library and wraps it with a shortcode.
366 *
367 * @deprecated 4.0.5 Use cryptx_encrypt() instead.
368 * @see cryptx_encrypt()
369 */
370 if (!function_exists('encryptx')) {
371 function encryptx(string $content, ?array $args = []): string
372 {
373 _doing_it_wrong(
374 'encryptx',
375 esc_html__('This function is deprecated. Use cryptx_encrypt() instead.', 'cryptx'),
376 '4.0.5'
377 );
378
379 return cryptx_encrypt($content, $args);
380 }
381 }