| Total Complexity | 102 |
| Total Lines | 541 |
| Duplicated Lines | 0 % |
| Changes | 4 | ||
| Bugs | 0 | Features | 2 |
Complex classes like PluginHost often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use PluginHost, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 2 | class PluginHost { |
||
| 3 | private $pdo; |
||
| 4 | private $hooks = array(); |
||
| 5 | private $plugins = array(); |
||
| 6 | private $handlers = array(); |
||
| 7 | private $commands = array(); |
||
| 8 | private $storage = array(); |
||
| 9 | private $feeds = array(); |
||
| 10 | private $api_methods = array(); |
||
| 11 | private $plugin_actions = array(); |
||
| 12 | private $owner_uid; |
||
| 13 | private $last_registered; |
||
| 14 | private static $instance; |
||
| 15 | |||
| 16 | const API_VERSION = 2; |
||
| 17 | |||
| 18 | // Hooks marked with *1 are run in global context and available |
||
| 19 | // to plugins loaded in config.php only |
||
| 20 | |||
| 21 | const HOOK_ARTICLE_BUTTON = 1; |
||
| 22 | const HOOK_ARTICLE_FILTER = 2; |
||
| 23 | const HOOK_PREFS_TAB = 3; |
||
| 24 | const HOOK_PREFS_TAB_SECTION = 4; |
||
| 25 | const HOOK_PREFS_TABS = 5; |
||
| 26 | const HOOK_FEED_PARSED = 6; |
||
| 27 | const HOOK_UPDATE_TASK = 7; // *1 |
||
| 28 | const HOOK_AUTH_USER = 8; |
||
| 29 | const HOOK_HOTKEY_MAP = 9; |
||
| 30 | const HOOK_RENDER_ARTICLE = 10; |
||
| 31 | const HOOK_RENDER_ARTICLE_CDM = 11; |
||
| 32 | const HOOK_FEED_FETCHED = 12; |
||
| 33 | const HOOK_SANITIZE = 13; |
||
| 34 | const HOOK_RENDER_ARTICLE_API = 14; |
||
| 35 | const HOOK_TOOLBAR_BUTTON = 15; |
||
| 36 | const HOOK_ACTION_ITEM = 16; |
||
| 37 | const HOOK_HEADLINE_TOOLBAR_BUTTON = 17; |
||
| 38 | const HOOK_HOTKEY_INFO = 18; |
||
| 39 | const HOOK_ARTICLE_LEFT_BUTTON = 19; |
||
| 40 | const HOOK_PREFS_EDIT_FEED = 20; |
||
| 41 | const HOOK_PREFS_SAVE_FEED = 21; |
||
| 42 | const HOOK_FETCH_FEED = 22; |
||
| 43 | const HOOK_QUERY_HEADLINES = 23; |
||
| 44 | const HOOK_HOUSE_KEEPING = 24; // *1 |
||
| 45 | const HOOK_SEARCH = 25; |
||
| 46 | const HOOK_FORMAT_ENCLOSURES = 26; |
||
| 47 | const HOOK_SUBSCRIBE_FEED = 27; |
||
| 48 | const HOOK_HEADLINES_BEFORE = 28; |
||
| 49 | const HOOK_RENDER_ENCLOSURE = 29; |
||
| 50 | const HOOK_ARTICLE_FILTER_ACTION = 30; |
||
| 51 | const HOOK_ARTICLE_EXPORT_FEED = 31; |
||
| 52 | const HOOK_MAIN_TOOLBAR_BUTTON = 32; |
||
| 53 | const HOOK_ENCLOSURE_ENTRY = 33; |
||
| 54 | const HOOK_FORMAT_ARTICLE = 34; |
||
| 55 | const HOOK_FORMAT_ARTICLE_CDM = 35; /* RIP */ |
||
| 56 | const HOOK_FEED_BASIC_INFO = 36; |
||
| 57 | const HOOK_SEND_LOCAL_FILE = 37; |
||
| 58 | const HOOK_UNSUBSCRIBE_FEED = 38; |
||
| 59 | const HOOK_SEND_MAIL = 39; |
||
| 60 | const HOOK_FILTER_TRIGGERED = 40; |
||
| 61 | const HOOK_GET_FULL_TEXT = 41; |
||
| 62 | const HOOK_ARTICLE_IMAGE = 42; |
||
| 63 | const HOOK_FEED_TREE = 43; |
||
| 64 | const HOOK_IFRAME_WHITELISTED = 44; |
||
| 65 | |||
| 66 | const KIND_ALL = 1; |
||
| 67 | const KIND_SYSTEM = 2; |
||
| 68 | const KIND_USER = 3; |
||
| 69 | |||
| 70 | public static function object_to_domain($plugin) { |
||
| 71 | return strtolower(get_class($plugin)); |
||
| 72 | } |
||
| 73 | |||
| 74 | public function __construct() { |
||
| 78 | } |
||
| 79 | |||
| 80 | private function __clone() { |
||
| 81 | // |
||
| 82 | } |
||
| 83 | |||
| 84 | public static function getInstance() { |
||
| 85 | if (self::$instance == null) { |
||
| 86 | self::$instance = new self(); |
||
| 87 | } |
||
| 88 | |||
| 89 | return self::$instance; |
||
| 90 | } |
||
| 91 | |||
| 92 | private function register_plugin($name, $plugin) { |
||
| 95 | } |
||
| 96 | |||
| 97 | // needed for compatibility with API 1 |
||
| 98 | public function get_link() { |
||
| 100 | } |
||
| 101 | |||
| 102 | public function get_dbh() { |
||
| 103 | return Db::get(); |
||
| 104 | } |
||
| 105 | |||
| 106 | public function get_pdo() { |
||
| 107 | return $this->pdo; |
||
| 108 | } |
||
| 109 | |||
| 110 | public function get_plugin_names() { |
||
| 111 | $names = array(); |
||
| 112 | |||
| 113 | foreach ($this->plugins as $p) { |
||
| 114 | array_push($names, get_class($p)); |
||
| 115 | } |
||
| 116 | |||
| 117 | return $names; |
||
| 118 | } |
||
| 119 | |||
| 120 | public function get_plugins() { |
||
| 121 | return $this->plugins; |
||
| 122 | } |
||
| 123 | |||
| 124 | public function get_plugin($name) { |
||
| 125 | return $this->plugins[strtolower($name)]; |
||
| 126 | } |
||
| 127 | |||
| 128 | public function run_hooks($type, $method, $args) { |
||
| 129 | foreach ($this->get_hooks($type) as $hook) { |
||
| 130 | $hook->$method($args); |
||
| 131 | } |
||
| 132 | } |
||
| 133 | |||
| 134 | public function add_hook($type, $sender, $priority = 50) { |
||
| 135 | $priority = (int) $priority; |
||
| 136 | |||
| 137 | if (!is_array($this->hooks[$type])) { |
||
| 138 | $this->hooks[$type] = []; |
||
| 139 | } |
||
| 140 | |||
| 141 | if (!is_array($this->hooks[$type][$priority])) { |
||
| 142 | $this->hooks[$type][$priority] = []; |
||
| 143 | } |
||
| 144 | |||
| 145 | array_push($this->hooks[$type][$priority], $sender); |
||
| 146 | ksort($this->hooks[$type]); |
||
| 147 | } |
||
| 148 | |||
| 149 | public function del_hook($type, $sender) { |
||
| 150 | if (is_array($this->hooks[$type])) { |
||
| 151 | foreach (array_keys($this->hooks[$type]) as $prio) { |
||
| 152 | $key = array_search($sender, $this->hooks[$type][$prio]); |
||
| 153 | |||
| 154 | if ($key !== false) { |
||
| 155 | unset($this->hooks[$type][$prio][$key]); |
||
| 156 | } |
||
| 157 | } |
||
| 158 | } |
||
| 159 | } |
||
| 160 | |||
| 161 | public function get_hooks($type) { |
||
| 162 | if (isset($this->hooks[$type])) { |
||
| 163 | $tmp = []; |
||
| 164 | |||
| 165 | foreach (array_keys($this->hooks[$type]) as $prio) { |
||
| 166 | $tmp = array_merge($tmp, $this->hooks[$type][$prio]); |
||
| 167 | } |
||
| 168 | |||
| 169 | return $tmp; |
||
| 170 | } else { |
||
| 171 | return []; |
||
| 172 | } |
||
| 173 | } |
||
| 174 | public function load_all($kind, $owner_uid = false, $skip_init = false) { |
||
| 175 | |||
| 176 | $plugins = array_merge(glob("plugins/*"), glob("plugins.local/*")); |
||
| 177 | $plugins = array_filter($plugins, "is_dir"); |
||
| 178 | $plugins = array_map("basename", $plugins); |
||
| 179 | |||
| 180 | asort($plugins); |
||
| 181 | |||
| 182 | $this->load(join(",", $plugins), $kind, $owner_uid, $skip_init); |
||
| 183 | } |
||
| 184 | |||
| 185 | public function load($classlist, $kind, $owner_uid = false, $skip_init = false) { |
||
| 186 | $plugins = explode(",", $classlist); |
||
| 187 | |||
| 188 | $this->owner_uid = (int) $owner_uid; |
||
| 189 | |||
| 190 | foreach ($plugins as $class) { |
||
| 191 | $class = trim($class); |
||
| 192 | $class_file = strtolower(clean_filename($class)); |
||
| 193 | |||
| 194 | if (!is_dir(__DIR__."/../plugins/$class_file") && |
||
| 195 | !is_dir(__DIR__."/../plugins.local/$class_file")) { |
||
| 196 | continue; |
||
| 197 | } |
||
| 198 | |||
| 199 | // try system plugin directory first |
||
| 200 | $file = __DIR__."/../plugins/$class_file/init.php"; |
||
| 201 | $vendor_dir = __DIR__."/../plugins/$class_file/vendor"; |
||
| 202 | |||
| 203 | if (!file_exists($file)) { |
||
| 204 | $file = __DIR__."/../plugins.local/$class_file/init.php"; |
||
| 205 | $vendor_dir = __DIR__."/../plugins.local/$class_file/vendor"; |
||
| 206 | } |
||
| 207 | |||
| 208 | if (!isset($this->plugins[$class])) { |
||
| 209 | if (file_exists($file)) { |
||
| 210 | require_once $file; |
||
| 211 | } |
||
| 212 | |||
| 213 | if (class_exists($class) && is_subclass_of($class, "Plugin")) { |
||
| 214 | |||
| 215 | // register plugin autoloader if necessary, for namespaced classes ONLY |
||
| 216 | // layout corresponds to tt-rss main /vendor/author/Package/Class.php |
||
| 217 | |||
| 218 | if (file_exists($vendor_dir)) { |
||
| 219 | spl_autoload_register(function($class) use ($vendor_dir) { |
||
| 220 | |||
| 221 | if (strpos($class, '\\') !== false) { |
||
| 222 | list ($namespace, $class_name) = explode('\\', $class, 2); |
||
| 223 | |||
| 224 | if ($namespace && $class_name) { |
||
| 225 | $class_file = "$vendor_dir/$namespace/".str_replace('\\', '/', $class_name).".php"; |
||
| 226 | |||
| 227 | if (file_exists($class_file)) { |
||
| 228 | require_once $class_file; |
||
| 229 | } |
||
| 230 | } |
||
| 231 | } |
||
| 232 | }); |
||
| 233 | } |
||
| 234 | |||
| 235 | $plugin = new $class($this); |
||
| 236 | |||
| 237 | $plugin_api = $plugin->api_version(); |
||
| 238 | |||
| 239 | if ($plugin_api < PluginHost::API_VERSION) { |
||
| 240 | user_error("plugin $class is not compatible with current API version (need: ".PluginHost::API_VERSION.", got: $plugin_api)", E_USER_WARNING); |
||
| 241 | continue; |
||
| 242 | } |
||
| 243 | |||
| 244 | if (file_exists(dirname($file)."/locale")) { |
||
| 245 | _bindtextdomain($class, dirname($file)."/locale"); |
||
| 246 | _bind_textdomain_codeset($class, "UTF-8"); |
||
| 247 | } |
||
| 248 | |||
| 249 | $this->last_registered = $class; |
||
| 250 | |||
| 251 | switch ($kind) { |
||
| 252 | case $this::KIND_SYSTEM: |
||
| 253 | if ($this->is_system($plugin)) { |
||
| 254 | if (!$skip_init) { |
||
| 255 | $plugin->init($this); |
||
| 256 | } |
||
| 257 | $this->register_plugin($class, $plugin); |
||
| 258 | } |
||
| 259 | break; |
||
| 260 | case $this::KIND_USER: |
||
| 261 | if (!$this->is_system($plugin)) { |
||
| 262 | if (!$skip_init) { |
||
| 263 | $plugin->init($this); |
||
| 264 | } |
||
| 265 | $this->register_plugin($class, $plugin); |
||
| 266 | } |
||
| 267 | break; |
||
| 268 | case $this::KIND_ALL: |
||
| 269 | if (!$skip_init) { |
||
| 270 | $plugin->init($this); |
||
| 271 | } |
||
| 272 | $this->register_plugin($class, $plugin); |
||
| 273 | break; |
||
| 274 | } |
||
| 275 | } |
||
| 276 | } |
||
| 277 | } |
||
| 278 | } |
||
| 279 | |||
| 280 | public function is_system($plugin) { |
||
| 281 | $about = $plugin->about(); |
||
| 282 | |||
| 283 | return @$about[3]; |
||
| 284 | } |
||
| 285 | |||
| 286 | // only system plugins are allowed to modify routing |
||
| 287 | public function add_handler($handler, $method, $sender) { |
||
| 288 | $handler = str_replace("-", "_", strtolower($handler)); |
||
| 289 | $method = strtolower($method); |
||
| 290 | |||
| 291 | if ($this->is_system($sender)) { |
||
| 292 | if (!is_array($this->handlers[$handler])) { |
||
| 293 | $this->handlers[$handler] = array(); |
||
| 294 | } |
||
| 295 | |||
| 296 | $this->handlers[$handler][$method] = $sender; |
||
| 297 | } |
||
| 298 | } |
||
| 299 | |||
| 300 | public function del_handler($handler, $method, $sender) { |
||
| 301 | $handler = str_replace("-", "_", strtolower($handler)); |
||
| 302 | $method = strtolower($method); |
||
| 303 | |||
| 304 | if ($this->is_system($sender)) { |
||
| 305 | unset($this->handlers[$handler][$method]); |
||
| 306 | } |
||
| 307 | } |
||
| 308 | |||
| 309 | public function lookup_handler($handler, $method) { |
||
| 310 | $handler = str_replace("-", "_", strtolower($handler)); |
||
| 311 | $method = strtolower($method); |
||
| 312 | |||
| 313 | if (is_array($this->handlers[$handler])) { |
||
| 314 | if (isset($this->handlers[$handler]["*"])) { |
||
| 315 | return $this->handlers[$handler]["*"]; |
||
| 316 | } else { |
||
| 317 | return $this->handlers[$handler][$method]; |
||
| 318 | } |
||
| 319 | } |
||
| 320 | |||
| 321 | return false; |
||
| 322 | } |
||
| 323 | |||
| 324 | public function add_command($command, $description, $sender, $suffix = "", $arghelp = "") { |
||
| 325 | $command = str_replace("-", "_", strtolower($command)); |
||
| 326 | |||
| 327 | $this->commands[$command] = array("description" => $description, |
||
| 328 | "suffix" => $suffix, |
||
| 329 | "arghelp" => $arghelp, |
||
| 330 | "class" => $sender); |
||
| 331 | } |
||
| 332 | |||
| 333 | public function del_command($command) { |
||
| 334 | $command = "-".strtolower($command); |
||
| 335 | |||
| 336 | unset($this->commands[$command]); |
||
| 337 | } |
||
| 338 | |||
| 339 | public function lookup_command($command) { |
||
| 340 | $command = "-".strtolower($command); |
||
| 341 | |||
| 342 | if (is_array($this->commands[$command])) { |
||
| 343 | return $this->commands[$command]["class"]; |
||
| 344 | } else { |
||
| 345 | return false; |
||
| 346 | } |
||
| 347 | } |
||
| 348 | |||
| 349 | public function get_commands() { |
||
| 350 | return $this->commands; |
||
| 351 | } |
||
| 352 | |||
| 353 | public function run_commands($args) { |
||
| 354 | foreach ($this->get_commands() as $command => $data) { |
||
| 355 | if (isset($args[$command])) { |
||
| 356 | $command = str_replace("-", "", $command); |
||
| 357 | $data["class"]->$command($args); |
||
| 358 | } |
||
| 359 | } |
||
| 360 | } |
||
| 361 | |||
| 362 | public function load_data() { |
||
| 363 | if ($this->owner_uid) { |
||
| 364 | $sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage |
||
| 365 | WHERE owner_uid = ?"); |
||
| 366 | $sth->execute([$this->owner_uid]); |
||
| 367 | |||
| 368 | while ($line = $sth->fetch()) { |
||
| 369 | $this->storage[$line["name"]] = unserialize($line["content"]); |
||
| 370 | } |
||
| 371 | } |
||
| 372 | } |
||
| 373 | |||
| 374 | private function save_data($plugin) { |
||
| 375 | if ($this->owner_uid) { |
||
| 376 | $this->pdo->beginTransaction(); |
||
| 377 | |||
| 378 | $sth = $this->pdo->prepare("SELECT id FROM ttrss_plugin_storage WHERE |
||
| 379 | owner_uid= ? AND name = ?"); |
||
| 380 | $sth->execute([$this->owner_uid, $plugin]); |
||
| 381 | |||
| 382 | if (!isset($this->storage[$plugin])) { |
||
| 383 | $this->storage[$plugin] = array(); |
||
| 384 | } |
||
| 385 | |||
| 386 | $content = serialize($this->storage[$plugin]); |
||
| 387 | |||
| 388 | if ($sth->fetch()) { |
||
| 389 | $sth = $this->pdo->prepare("UPDATE ttrss_plugin_storage SET content = ? |
||
| 390 | WHERE owner_uid= ? AND name = ?"); |
||
| 391 | $sth->execute([(string) $content, $this->owner_uid, $plugin]); |
||
| 392 | |||
| 393 | } else { |
||
| 394 | $sth = $this->pdo->prepare("INSERT INTO ttrss_plugin_storage |
||
| 395 | (name,owner_uid,content) VALUES |
||
| 396 | (?, ?, ?)"); |
||
| 397 | $sth->execute([$plugin, $this->owner_uid, (string) $content]); |
||
| 398 | } |
||
| 399 | |||
| 400 | $this->pdo->commit(); |
||
| 401 | } |
||
| 402 | } |
||
| 403 | |||
| 404 | public function set($sender, $name, $value, $sync = true) { |
||
| 405 | $idx = get_class($sender); |
||
| 406 | |||
| 407 | if (!isset($this->storage[$idx])) { |
||
| 408 | $this->storage[$idx] = array(); |
||
| 409 | } |
||
| 410 | |||
| 411 | $this->storage[$idx][$name] = $value; |
||
| 412 | |||
| 413 | if ($sync) { |
||
| 414 | $this->save_data(get_class($sender)); |
||
| 415 | } |
||
| 416 | } |
||
| 417 | |||
| 418 | public function get($sender, $name, $default_value = false) { |
||
| 419 | $idx = get_class($sender); |
||
| 420 | |||
| 421 | if (isset($this->storage[$idx][$name])) { |
||
| 422 | return $this->storage[$idx][$name]; |
||
| 423 | } else { |
||
| 424 | return $default_value; |
||
| 425 | } |
||
| 426 | } |
||
| 427 | |||
| 428 | public function get_all($sender) { |
||
| 429 | $idx = get_class($sender); |
||
| 430 | |||
| 431 | $data = $this->storage[$idx]; |
||
| 432 | |||
| 433 | return $data ? $data : []; |
||
| 434 | } |
||
| 435 | |||
| 436 | public function clear_data($sender) { |
||
| 437 | if ($this->owner_uid) { |
||
| 438 | $idx = get_class($sender); |
||
| 439 | |||
| 440 | unset($this->storage[$idx]); |
||
| 441 | |||
| 442 | $sth = $this->pdo->prepare("DELETE FROM ttrss_plugin_storage WHERE name = ? |
||
| 443 | AND owner_uid = ?"); |
||
| 444 | $sth->execute([$idx, $this->owner_uid]); |
||
| 445 | } |
||
| 446 | } |
||
| 447 | |||
| 448 | // Plugin feed functions are *EXPERIMENTAL*! |
||
| 449 | |||
| 450 | // cat_id: only -1 is supported (Special) |
||
| 451 | public function add_feed($cat_id, $title, $icon, $sender) { |
||
| 452 | if (!$this->feeds[$cat_id]) { |
||
| 453 | $this->feeds[$cat_id] = array(); |
||
| 454 | } |
||
| 455 | |||
| 456 | $id = count($this->feeds[$cat_id]); |
||
| 457 | |||
| 458 | array_push($this->feeds[$cat_id], |
||
| 459 | array('id' => $id, 'title' => $title, 'sender' => $sender, 'icon' => $icon)); |
||
| 460 | |||
| 461 | return $id; |
||
| 462 | } |
||
| 463 | |||
| 464 | public function get_feeds($cat_id) { |
||
| 465 | return $this->feeds[$cat_id]; |
||
| 466 | } |
||
| 467 | |||
| 468 | // convert feed_id (e.g. -129) to pfeed_id first |
||
| 469 | public function get_feed_handler($pfeed_id) { |
||
| 470 | foreach ($this->feeds as $cat) { |
||
| 471 | foreach ($cat as $feed) { |
||
| 472 | if ($feed['id'] == $pfeed_id) { |
||
| 473 | return $feed['sender']; |
||
| 474 | } |
||
| 475 | } |
||
| 476 | } |
||
| 477 | } |
||
| 478 | |||
| 479 | public static function pfeed_to_feed_id($label) { |
||
| 481 | } |
||
| 482 | |||
| 483 | public static function feed_to_pfeed_id($feed) { |
||
| 484 | return PLUGIN_FEED_BASE_INDEX - 1 + abs($feed); |
||
| 485 | } |
||
| 486 | |||
| 487 | public function add_api_method($name, $sender) { |
||
| 488 | if ($this->is_system($sender)) { |
||
| 489 | $this->api_methods[strtolower($name)] = $sender; |
||
| 490 | } |
||
| 491 | } |
||
| 492 | |||
| 493 | public function get_api_method($name) { |
||
| 494 | return $this->api_methods[$name]; |
||
| 495 | } |
||
| 496 | |||
| 497 | public function add_filter_action($sender, $action_name, $action_desc) { |
||
| 498 | $sender_class = get_class($sender); |
||
| 499 | |||
| 500 | if (!isset($this->plugin_actions[$sender_class])) { |
||
| 501 | $this->plugin_actions[$sender_class] = array(); |
||
| 502 | } |
||
| 503 | |||
| 504 | array_push($this->plugin_actions[$sender_class], |
||
| 505 | array("action" => $action_name, "description" => $action_desc, "sender" => $sender)); |
||
| 506 | } |
||
| 507 | |||
| 508 | public function get_filter_actions() { |
||
| 509 | return $this->plugin_actions; |
||
| 510 | } |
||
| 511 | |||
| 512 | public function get_owner_uid() { |
||
| 513 | return $this->owner_uid; |
||
| 514 | } |
||
| 515 | |||
| 516 | // handled by classes/pluginhandler.php, requires valid session |
||
| 517 | public function get_method_url($sender, $method, $params) { |
||
| 518 | return get_self_url_prefix()."/backend.php?". |
||
| 519 | http_build_query( |
||
| 520 | array_merge( |
||
| 521 | [ |
||
| 522 | "op" => "pluginhandler", |
||
| 523 | "plugin" => strtolower(get_class($sender)), |
||
| 524 | "method" => $method |
||
| 525 | ], |
||
| 526 | $params)); |
||
| 527 | } |
||
| 528 | |||
| 529 | // WARNING: endpoint in public.php, exposed to unauthenticated users |
||
| 530 | public function get_public_method_url($sender, $method, $params) { |
||
| 543 | } |
||
| 544 | } |
||
| 545 | } |
||
| 546 |