PluginProbe
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP / trunk
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP vtrunk
3.1.3 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 All 110 releases
betterlinks / betterlinks.php

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

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