PluginProbe
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP / 3.1.2
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP v3.1.2
3.1.2 3.1.1 3.1.0 3.0.1 3.0.0 2.4.13 2.4.12 2.4.11 2.4.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 109 releases
betterlinks / betterlinks.php

betterlinks.php in BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP 3.1.2, at betterlinks.php

286 lines 12.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: BetterLinks
4 * Plugin URI: https://betterlinks.io/
5 * Description: Create, shorten, cloak, track and manage any URL. Gather click analytics, run marketing campaigns, and connect AI assistants over MCP.
6 * Version: 3.1.2
7 * Author: WPDeveloper
8 * Author URI: https://wpdeveloper.com
9 * License: GPL-3.0+
10 * License URI: http://www.gnu.org/licenses/gpl-3.0.txt
11 * Author URI: https://wpdeveloper.com
12 * Text Domain: betterlinks
13 * Domain Path: /languages
14 */
15
16 use BetterLinks\Admin\Cache;
17
18 if (!defined('ABSPATH')) {
19 exit();
20 }
21
22 if (file_exists(dirname(__FILE__) . '/vendor/autoload.php')) {
23 require_once dirname(__FILE__) . '/vendor/autoload.php';
24 }
25
26 /**
27 * Bundled MCP runtime (WordPress Abilities API).
28 *
29 * Loaded through the Jetpack Autoloader so that if the same library is also
30 * shipped by another plugin — or lands in WordPress core — the newest copy
31 * wins and loads once, with no fatal class collisions. This lets BetterLinks
32 * serve its MCP connector out of the box, without the standalone Abilities API
33 * plugin. See docs/mcp-server.md for the update procedure.
34 */
35 $betterlinks_mcp_runtime = dirname(__FILE__) . '/dependencies/vendor/autoload_packages.php';
36 if (is_readable($betterlinks_mcp_runtime)) {
37 require_once $betterlinks_mcp_runtime;
38 }
39 unset($betterlinks_mcp_runtime);
40
41 if (!class_exists('BetterLinks')) {
42 final class BetterLinks
43 {
44 private $Installer;
45 private $upload_dir;
46 private function __construct()
47 {
48 $this->upload_dir_path();
49 $this->define_constants();
50 $this->set_global_settings();
51 $this->Installer = new BetterLinks\Installer();
52 register_activation_hook(__FILE__, [$this, 'activate']);
53 register_deactivation_hook(__FILE__, [$this, 'deactivate']);
54 add_action('plugins_loaded', [$this, 'on_plugins_loaded']);
55 add_action('betterlinks_loaded', [$this, 'init_plugin']);
56 add_action('admin_init', [$this, 'run_migrator']);
57 add_action('admin_init', [$this->Installer, 'heal_missing_tables'], 5);
58 add_action('admin_init', [$this, 'do_the_works_if_failed_during_activation'], 100);
59 add_action('admin_init', [$this, 'maybe_complete_legacy_quick_setup'], 9);
60 add_action('admin_init', [$this, 'quick_setup']);
61 $this->dispatch_hook();
62 add_action( 'wp_enqueue_scripts', [$this, 'frontend_scripts'] );
63 }
64
65 public function do_the_works_if_failed_during_activation()
66 {
67 $betterlinks_activation_flag = BetterLinks\Helper::btl_get_option("betterlinks_activation_flag");
68 if(isset($betterlinks_activation_flag["last_activation_background_processes_firing_timestamp"]) && isset($betterlinks_activation_flag["last_activation_timestamp"])) {
69 if($betterlinks_activation_flag["last_activation_background_processes_firing_timestamp"]){
70 return false;
71 }
72 $waiting_time_in_seconds = 5;
73 if((absInt($betterlinks_activation_flag["last_activation_timestamp"]) + $waiting_time_in_seconds) > time()){
74 // don't go any further and return false here if,
75 // $waiting_time_in_seconds (in this case 5 seconds) haven't passed yet since the activation flag was setted
76 return false;
77 }
78 $all_tasks = array_merge(
79 $this->Installer->activation,
80 $this->Installer->migration
81 );
82 foreach ($all_tasks as $task) {
83 $this->Installer->$task();
84 }
85 }
86 }
87
88 public static function init()
89 {
90 static $instance = false;
91
92 if (!$instance) {
93 $instance = new self();
94 }
95
96 return $instance;
97 }
98 public function define_constants()
99 {
100 /**
101 * Defines CONSTANTS for Whole plugins.
102 */
103 define('BETTERLINKS_VERSION', '3.1.2');
104 define('BETTERLINKS_DB_VERSION', '1.6.11');
105 define('BETTERLINKS_MENU_NOTICE', '10');
106 define('BETTERLINKS_SETTINGS_NAME', 'betterlinks_settings');
107 define('BETTERLINKS_PLUGIN_FILE', __FILE__);
108 define('BETTERLINKS_PLUGIN_BASENAME', plugin_basename(__FILE__));
109 define('BETTERLINKS_PLUGIN_SLUG', 'betterlinks');
110 define('BETTERLINKS_PLUGIN_ROOT_URI', plugins_url('/', __FILE__));
111 define('BETTERLINKS_ROOT_DIR_PATH', plugin_dir_path(__FILE__));
112 define('BETTERLINKS_ASSETS_DIR_PATH', BETTERLINKS_ROOT_DIR_PATH . 'assets/');
113 define('BETTERLINKS_ASSETS_URI', BETTERLINKS_PLUGIN_ROOT_URI . 'assets/');
114 define('BETTERLINKS_UPLOAD_DIR_PATH', $this->upload_dir['basedir'] . '/betterlinks_uploads');
115 define('BETTERLINKS_EXISTS_LINKS_JSON', defined('BETTERLINKS_ALLOW_JSON_REDIRECT') ? file_exists(BETTERLINKS_UPLOAD_DIR_PATH . '/links.json') && BETTERLINKS_ALLOW_JSON_REDIRECT : file_exists(BETTERLINKS_UPLOAD_DIR_PATH . '/links.json'));
116 define('BETTERLINKS_EXISTS_CLICKS_JSON', file_exists(BETTERLINKS_UPLOAD_DIR_PATH . '/clicks.json'));
117 define('BETTERLINKS_EXISTS_SETTINGS_JSON', defined('BETTERLINKS_ALLOW_JSON_REDIRECT') ? file_exists(BETTERLINKS_UPLOAD_DIR_PATH . '/settings.json') && BETTERLINKS_ALLOW_JSON_REDIRECT : file_exists(BETTERLINKS_UPLOAD_DIR_PATH . '/settings.json'));
118 define('BETTERLINKS_LINKS_OPTION_NAME', 'betterlinks_links');
119 define('BETTERLINKS_CACHE_LINKS_NAME', 'betterlinks_cache_links_data');
120 define('BETTERLINKS_DB_ALTER_OPTIONS', 'betterlinks_db_alter_options');
121 define('BETTERLINKS_CUSTOM_DOMAIN_MENU', 'betterlinks_custom_domain_menu');
122 define('BETTERLINKS_AI_API_KEYS_OPTION_NAME', 'betterlinks_ai_api_keys');
123 }
124
125 public function upload_dir_path()
126 {
127 $this->upload_dir = wp_get_upload_dir();
128 }
129
130
131 public function on_plugins_loaded()
132 {
133 do_action('betterlinks_loaded');
134 }
135
136 /**
137 * Initialize the plugin
138 *
139 * @return void
140 */
141 public function init_plugin()
142 {
143 BetterLinks\API::init();
144 if (is_admin()) {
145 new BetterLinks\Admin();
146 }
147 BetterLinks\Integration::init();
148 new BetterLinks\Link();
149 new BetterLinks\Tools();
150 new BetterLinks\Frontend;
151 new BetterLinks\Elementor();
152
153 // MCP connector: register abilities (always, so generic Abilities
154 // clients can discover BetterLinks) and the MCP server surface (which
155 // gates serving on the enable_mcp setting). Guarded so a build without
156 // the bundled Abilities runtime still boots.
157 ( new BetterLinks\Abilities\Abilities_Registrar() )->init();
158 ( new BetterLinks\Mcp\Mcp_Manager() )->init();
159 }
160
161 public function dispatch_hook()
162 {
163 BetterLinks\API::dispatch_hook();
164 BetterLinks\Cron::init();
165 }
166
167 public function set_global_settings()
168 {
169 $GLOBALS['betterlinks'] = BetterLinks\Helper::get_links();
170 $settings = Cache::get_json_settings();
171 $auto_create_link_settings = defined('BETTERLINKS_PRO_AUTO_LINK_CREATE_OPTION_NAME') ? get_option( BETTERLINKS_PRO_AUTO_LINK_CREATE_OPTION_NAME, array() ) : array();
172 if ( is_string( $auto_create_link_settings ) ) {
173 $auto_create_link_settings = json_decode( $auto_create_link_settings, true );
174 }
175 $settings = is_array($settings) ? $settings : array();
176 $auto_create_link_settings = is_array( $auto_create_link_settings ) ? $auto_create_link_settings : array();
177 $GLOBALS['betterlinks_settings'] = array_merge( $settings, $auto_create_link_settings );
178 }
179
180 public function run_migrator()
181 {
182 $btl_version = BetterLinks\Helper::btl_get_option('betterlinks_version');
183 $should_insert = $btl_version===false;
184 if ($btl_version != BETTERLINKS_VERSION && BetterLinks\Helper::btl_update_option('betterlinks_version', BETTERLINKS_VERSION, $should_insert, !$should_insert)) {
185 // The admin links payload is cached in a transient with no expiry, and
186 // which categories it contains depends on the running code (e.g. the
187 // Link in Bio category exclusion). A payload written by the previous
188 // version can therefore describe categories wrongly forever — drop it
189 // once per upgrade so the first dashboard load rebuilds it fresh.
190 BetterLinks\Helper::clear_query_cache();
191 $this->Installer->data($this->Installer->migration)->save()->dispatch();
192 BetterLinks\Helper::btl_update_option('betterlinks_activation_flag', [
193 "last_activation_timestamp" => time(),
194 "last_activation_background_processes_firing_timestamp" => false,
195 ]);
196 }
197 }
198
199 public function activate()
200 {
201 $this->Installer->data($this->Installer->activation)->save()->dispatch();
202 BetterLinks\Helper::btl_update_option('betterlinks_activation_flag', [
203 "last_activation_timestamp" => time(),
204 "last_activation_background_processes_firing_timestamp" => false,
205 ]);
206 add_option('betterlinks_quick_setup', true);
207 }
208
209 /**
210 * Retire the Quick Setup entry on sites that were already set up.
211 *
212 * Only the wizard's final step ever writes 'complete', so any site configured
213 * before the wizard existed — or where an admin skipped it — kept showing
214 * "Quick Setup" forever no matter how configured it was. Runs once per install:
215 * if links already exist, the site is demonstrably past onboarding.
216 */
217 public function maybe_complete_legacy_quick_setup() {
218 if ( get_option( 'betterlinks_quick_setup_backfilled' ) ) return;
219 update_option( 'betterlinks_quick_setup_backfilled', 1, false );
220
221 if ( 'complete' === get_option( 'betterlinks_quick_setup_step' ) ) return;
222
223 global $wpdb;
224 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
225 $has_links = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->prefix}betterlinks" );
226 if ( $has_links > 0 ) {
227 update_option( 'betterlinks_quick_setup_step', 'complete' );
228 delete_option( 'betterlinks_quick_setup' );
229 }
230 }
231
232 public function quick_setup() {
233 if( 'complete' === get_option('betterlinks_quick_setup_step') ) return;
234 if( get_option( 'betterlinks_quick_setup' ) && is_admin() ) {
235 delete_option( 'betterlinks_quick_setup' );
236
237 $redirect_url = admin_url('admin.php?page=betterlinks-quick-setup');
238 wp_safe_redirect($redirect_url);
239 exit;
240 }
241 }
242
243 public function deactivate()
244 {
245 new BetterLinks\Uninstall();
246 }
247
248 public function frontend_scripts() {
249 $dependencies = include_once BETTERLINKS_ASSETS_DIR_PATH . 'js/betterlinks.app.core.min.asset.php';
250
251 // Enqueue main app script (geolocation logic is bundled inside)
252 wp_enqueue_script( 'betterlinks-app', BETTERLINKS_ASSETS_URI . 'js/betterlinks.app.core.min.js', [ 'jquery' ], $dependencies['version'], true );
253
254 // Deliberately no `betterlinks_admin_nonce` here. This runs on every
255 // public page, and a nonce is bound to the session rather than to a
256 // capability — localizing it handed any logged-in visitor, down to a
257 // Subscriber, a valid admin-AJAX nonce. The only consumer of this
258 // bundle is the click-tracking beacon, which is a nopriv handler and
259 // verifies no nonce at all, so nothing needs it.
260 wp_localize_script('betterlinks-app', 'betterLinksApp', [
261 'ajaxurl' => admin_url('admin-ajax.php'),
262 'site_url' => apply_filters('betterlinks/site_url', site_url()),
263 'rest_url' => rest_url(),
264 'nonce' => wp_create_nonce('wp_rest'),
265 'betterlinkspro_version' => defined('BETTERLINKS_PRO_VERSION') ? BETTERLINKS_PRO_VERSION : null,
266 ]);
267 }
268 }
269 }
270
271 /**
272 * Initializes the main plugin
273 *
274 * @return \BetterLinks
275 */
276 if (!function_exists('BetterLinks_Start')) {
277 function BetterLinks_Start()
278 {
279 return BetterLinks::init();
280
281 }
282 }
283
284 // Plugin Start
285 BetterLinks_Start();
286