| 1 |
<?php |
| 2 |
/* |
| 3 |
* SOFTWARE LICENSE INFORMATION |
| 4 |
* |
| 5 |
* Copyright (c) 2017 Buttonizer, all rights reserved. |
| 6 |
* |
| 7 |
* This file is part of Buttonizer |
| 8 |
* |
| 9 |
* For detailed information regarding to the licensing of |
| 10 |
* this software, please review the license.txt or visit: |
| 11 |
* https://buttonizer.pro/license/ |
| 12 |
*/ |
| 13 |
|
| 14 |
namespace Buttonizer\Migration; |
| 15 |
|
| 16 |
use Buttonizer\Core\PluginConfig; |
| 17 |
|
| 18 |
# No script kiddies |
| 19 |
defined('ABSPATH') or die('No script kiddies please!'); |
| 20 |
|
| 21 |
/** |
| 22 |
* Serves an absorbed plugin's own code from inside Buttonizer. |
| 23 |
* |
| 24 |
* Sites still running the acquired plugin's pre-Buttonizer system keep their |
| 25 |
* buttons, their settings and their admin screens exactly as they were: the |
| 26 |
* code is copied under modules/ untouched, and Buttonizer loads it in place of |
| 27 |
* the plugin that used to. |
| 28 |
* |
| 29 |
* Runs at plugin boot, not on admin_init: the module registers hooks on |
| 30 |
* plugins_loaded, wp_footer and admin_menu, so it has to be in place before |
| 31 |
* WordPress gets there. |
| 32 |
*/ |
| 33 |
class ModuleLoader |
| 34 |
{ |
| 35 |
/** |
| 36 |
* Marks a visit to Buttonizer's own page as the point of the click. |
| 37 |
* |
| 38 |
* Without it there is no way to tell "the user asked for Buttonizer" from |
| 39 |
* "this link was built before the module took over", and the two need |
| 40 |
* opposite answers. |
| 41 |
*/ |
| 42 |
const OWN_DASHBOARD_PARAM = 'bz_signup'; |
| 43 |
|
| 44 |
/** |
| 45 |
* @var bool Whether a module is being served this request. |
| 46 |
*/ |
| 47 |
private static $serving = false; |
| 48 |
|
| 49 |
/** |
| 50 |
* @var SourcePlugin|null The module being served this request, if any. |
| 51 |
*/ |
| 52 |
private static $servingSource = null; |
| 53 |
|
| 54 |
/** |
| 55 |
* @var bool Whether Buttonizer keeps itself out of the admin sidebar. |
| 56 |
*/ |
| 57 |
private static $hostMenuHidden = false; |
| 58 |
|
| 59 |
/** |
| 60 |
* Load every module this site still needs. |
| 61 |
*/ |
| 62 |
public static function boot(): void |
| 63 |
{ |
| 64 |
foreach (SourcePlugins::all() as $source) { |
| 65 |
if (!self::shouldLoad($source)) { |
| 66 |
continue; |
| 67 |
} |
| 68 |
|
| 69 |
// Decided before the module registers anything, because its |
| 70 |
// adapter needs the answer while wiring itself up. |
| 71 |
self::decideOwnMenu(); |
| 72 |
|
| 73 |
require_once $source->modulePath(); |
| 74 |
|
| 75 |
self::$serving = true; |
| 76 |
self::$servingSource = $source; |
| 77 |
|
| 78 |
// Everything the copied code cannot know about Buttonizer lives in |
| 79 |
// the adapter, so the copy itself stays untouched. |
| 80 |
$adapter = $source->moduleAdapter(); |
| 81 |
|
| 82 |
if ($adapter) { |
| 83 |
$adapter::attach(); |
| 84 |
} |
| 85 |
} |
| 86 |
|
| 87 |
// Only when the module took Buttonizer's own place in the sidebar: |
| 88 |
// that is exactly when a link built before this request — a redirect |
| 89 |
// after signup, a bookmark, "Open Buttonizer" — still points at |
| 90 |
// Buttonizer's own page. That page is never unregistered, so it would |
| 91 |
// render its own connect screen right over the module the user is |
| 92 |
// supposed to still be looking at. |
| 93 |
if (self::$hostMenuHidden) { |
| 94 |
add_action('admin_init', [self::class, 'redirectOwnDashboard']); |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
/** |
| 99 |
* Send a direct hit on Buttonizer's own page to the module instead. |
| 100 |
* |
| 101 |
* Runs on admin_init, ahead of admin_menu: the menu is irrelevant here, |
| 102 |
* this is about which page callback actually renders. |
| 103 |
*/ |
| 104 |
public static function redirectOwnDashboard(): void |
| 105 |
{ |
| 106 |
if (!self::$servingSource || !isset($_GET['page']) || $_GET['page'] !== PluginConfig::pageSlug()) { |
| 107 |
return; |
| 108 |
} |
| 109 |
|
| 110 |
// The user asked to be here. "Try now" is the one way to reach the |
| 111 |
// signup screen while the old system is still being served, so sending |
| 112 |
// it back to the module would make signing up impossible. |
| 113 |
if (isset($_GET[self::OWN_DASHBOARD_PARAM])) { |
| 114 |
return; |
| 115 |
} |
| 116 |
|
| 117 |
$moduleSlug = self::$servingSource->modulePageSlug(); |
| 118 |
|
| 119 |
if (!$moduleSlug) { |
| 120 |
return; |
| 121 |
} |
| 122 |
|
| 123 |
wp_safe_redirect(admin_url('admin.php?page=' . $moduleSlug)); |
| 124 |
exit; |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Is Buttonizer serving an absorbed plugin's own system right now? |
| 129 |
*/ |
| 130 |
public static function isServingModule(): bool |
| 131 |
{ |
| 132 |
return self::$serving; |
| 133 |
} |
| 134 |
|
| 135 |
/** |
| 136 |
* Leave the absorbed plugin's menu as the only one in the sidebar. |
| 137 |
* |
| 138 |
* The point of the module is that nothing changed for this user, and |
| 139 |
* before the migration their sidebar had exactly one entry. Buttonizer's |
| 140 |
* own page stays registered and reachable — by direct URL and from the |
| 141 |
* "Open Buttonizer" button in the migration notice — it just does not add |
| 142 |
* a second entry next to the one they already know. |
| 143 |
* |
| 144 |
* Only when Buttonizer has nothing of its own to show: an install with |
| 145 |
* buttons in the cloud keeps its menu, hiding it would take away something |
| 146 |
* the user actively uses. |
| 147 |
*/ |
| 148 |
private static function decideOwnMenu(): void |
| 149 |
{ |
| 150 |
if (!is_admin() || ConnectionAdopter::isTargetConnected()) { |
| 151 |
return; |
| 152 |
} |
| 153 |
|
| 154 |
self::$hostMenuHidden = true; |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* Is Buttonizer keeping itself out of the sidebar? |
| 159 |
* |
| 160 |
* The module asks before registering its screens: hanging them off a menu |
| 161 |
* that is about to be removed would take them off the sidebar too, so in |
| 162 |
* that case it registers itself top-level, exactly as the plugin did. |
| 163 |
*/ |
| 164 |
public static function hostMenuHidden(): bool |
| 165 |
{ |
| 166 |
return self::$hostMenuHidden; |
| 167 |
} |
| 168 |
|
| 169 |
/** |
| 170 |
* Should this source plugin's module be served by Buttonizer? |
| 171 |
*/ |
| 172 |
private static function shouldLoad(SourcePlugin $source): bool |
| 173 |
{ |
| 174 |
if (!$source->hasModule()) { |
| 175 |
return false; |
| 176 |
} |
| 177 |
|
| 178 |
// Only a system Buttonizer took over is served from here, and it keeps |
| 179 |
// being served regardless of the source plugin's own flags: going by |
| 180 |
// those means anything that flips one takes the user's buttons off the |
| 181 |
// site. Old options on their own prove nothing either way — whether |
| 182 |
// they are absorbed is MigrationManager's call, and this only follows it. |
| 183 |
if (!self::wasEmbedded($source)) { |
| 184 |
return false; |
| 185 |
} |
| 186 |
|
| 187 |
// The user has moved on to the cloud |
| 188 |
if (self::hasMovedToCloud($source)) { |
| 189 |
return false; |
| 190 |
} |
| 191 |
|
| 192 |
// The source plugin already loaded this exact code — it is the same |
| 193 |
// classes and functions in the global namespace, so a second copy is a |
| 194 |
// fatal redeclaration. |
| 195 |
// |
| 196 |
// Deliberately about the code and not about the plugin: a source plugin |
| 197 |
// reactivated after the migration steps aside for the module, and going |
| 198 |
// by its active flag instead would hand that user Buttonizer's signup |
| 199 |
// screen in place of the screens they have been using all along. |
| 200 |
if ($source->isModuleLoaded()) { |
| 201 |
return false; |
| 202 |
} |
| 203 |
|
| 204 |
return true; |
| 205 |
} |
| 206 |
|
| 207 |
/** |
| 208 |
* Has this site graduated from the old system to the cloud? |
| 209 |
* |
| 210 |
* The module was taken over while Buttonizer had no site of its own, and |
| 211 |
* Buttonizer got connected: the only way to get there is the user deciding |
| 212 |
* to move. Serving both would put two sets of buttons on their site — the |
| 213 |
* old ones from these options and the new ones from the cloud. |
| 214 |
* |
| 215 |
* The answer is remembered rather than re-read from the connection, so a |
| 216 |
* disconnect does not quietly put the old system back. Someone whose |
| 217 |
* session expired should not find a different plugin rendering on their |
| 218 |
* site; coming back is a decision, and LegacyFallback is where it is made. |
| 219 |
* |
| 220 |
* The options are never touched, so the buttons return exactly as they |
| 221 |
* were left. |
| 222 |
*/ |
| 223 |
private static function hasMovedToCloud(SourcePlugin $source): bool |
| 224 |
{ |
| 225 |
$state = MigrationState::get($source->id()); |
| 226 |
|
| 227 |
if (($state['result'] ?? '') !== MigrationManager::RESULT_LEGACY_MODULE) { |
| 228 |
return false; |
| 229 |
} |
| 230 |
|
| 231 |
// Already on the cloud before the migration: this site always had both |
| 232 |
if (!empty($state['target_connected'])) { |
| 233 |
return false; |
| 234 |
} |
| 235 |
|
| 236 |
if (!empty($state['moved_to_cloud'])) { |
| 237 |
return true; |
| 238 |
} |
| 239 |
|
| 240 |
if (!ConnectionAdopter::isTargetConnected()) { |
| 241 |
return false; |
| 242 |
} |
| 243 |
|
| 244 |
// First request after the move. Written once, and only ever unwritten |
| 245 |
// by the user asking for the old system back. |
| 246 |
MigrationState::set($source->id(), ['moved_to_cloud' => true]); |
| 247 |
|
| 248 |
return true; |
| 249 |
} |
| 250 |
|
| 251 |
/** |
| 252 |
* Did Buttonizer already take this source plugin's old system over? |
| 253 |
*/ |
| 254 |
private static function wasEmbedded(SourcePlugin $source): bool |
| 255 |
{ |
| 256 |
$state = MigrationState::get($source->id()); |
| 257 |
|
| 258 |
return ($state['result'] ?? '') === MigrationManager::RESULT_LEGACY_MODULE; |
| 259 |
} |
| 260 |
} |
| 261 |
|