Textpattern | PHP Cross Reference | Content Management Systems |
Description: Plugin
1 <?php 2 3 /* 4 * Textpattern Content Management System 5 * https://textpattern.com/ 6 * 7 * Copyright (C) 2020 The Textpattern Development Team 8 * 9 * This file is part of Textpattern. 10 * 11 * Textpattern is free software; you can redistribute it and/or 12 * modify it under the terms of the GNU General Public License 13 * as published by the Free Software Foundation, version 2. 14 * 15 * Textpattern is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with Textpattern. If not, see <https://www.gnu.org/licenses/>. 22 */ 23 24 /** 25 * Plugin 26 * 27 * @since 4.7.0 28 * @package Plugin 29 */ 30 31 namespace Textpattern\Plugin; 32 33 class Plugin 34 { 35 private static $metaData = array( 36 'type' => 0, 37 'author' => '', 38 'author_uri' => '', 39 'version' => '1', 40 'description' => '', 41 'order' => 5, 42 'flags' => 0 43 ); 44 45 /** 46 * Constructor. 47 */ 48 49 public function __construct() 50 { 51 52 } 53 54 /** 55 * Install plugin to the database. 56 * 57 * If the plugin has been programmed to respond to lifecycle events, 58 * the following callback is raised upon installation: 59 * plugin_lifecycle.{plugin_name} > installed 60 * 61 * @param string $plugin Plugin_base64 62 * @param int $status Plugin status 63 * 64 * @return string|array 65 */ 66 67 public function install($plugin, $status = null, $write = true) 68 { 69 if ($encoded = !is_array($plugin)) { 70 $plugin = $this->extract($plugin); 71 } 72 73 if (!empty($plugin['name'])) { 74 extract($plugin + self::$metaData + array( 75 'help' => '', 76 'code' => '', 77 'textpack' => '', 78 'data' => '' 79 )); 80 81 $name = sanitizeForFile($name); 82 $exists = fetch('name', 'txp_plugin', 'name', $name); 83 isset($md5) or $md5 = md5($code); 84 85 if (isset($help_raw) && empty($plugin['allow_html_help'])) { 86 // Default: help is in Textile format. 87 $textile = new \Textpattern\Textile\RestrictedParser(); 88 $help = $textile->setLite(false)->setImages(true)->parse($help_raw); 89 } 90 91 $fields = " 92 type = $type, 93 author = '".doSlash($author)."', 94 author_uri = '".doSlash($author_uri)."', 95 version = '".doSlash($version)."', 96 description = '".doSlash($description)."', 97 help = '".doSlash($help)."', 98 code = '".doSlash($code)."', 99 code_restore = '".doSlash($code)."', 100 code_md5 = '".doSlash($md5)."', 101 textpack = '".doSlash($textpack)."', 102 data = '".doSlash($data)."', 103 flags = $flags 104 "; 105 106 if ($exists) { 107 if (isset($status)) { 108 $fields .= ", status = ".(empty($status) ? 0 : 1); 109 } 110 $rs = safe_update( 111 'txp_plugin', 112 $fields, 113 "name = '".doSlash($name)."'" 114 ); 115 } else { 116 $rs = safe_insert( 117 'txp_plugin', 118 "name = '".doSlash($name)."', 119 status = ".(empty($status) ? 0 : 1).", 120 load_order = '".$order."',". 121 $fields 122 ); 123 } 124 125 if ($rs && ($code || !$encoded)) { 126 $this->installTextpack($name, true); 127 128 if ($write) { 129 $this->updateFile($name, $plugin); 130 } 131 132 if ($flags & PLUGIN_LIFECYCLE_NOTIFY) { 133 load_plugin($name, true); 134 set_error_handler("pluginErrorHandler"); 135 $message = callback_event("plugin_lifecycle.$name", 'installed'); 136 restore_error_handler(); 137 } 138 139 if (empty($message)) { 140 $message = gTxt('plugin_installed', array('{name}' => $name)); 141 } 142 } else { 143 $message = array(gTxt('plugin_install_failed', array('{name}' => $name)), E_ERROR); 144 } 145 } 146 147 if (empty($message)) { 148 $message = array(gTxt('bad_plugin_code'), E_ERROR); 149 } 150 151 return $message; 152 } 153 154 /** 155 * Unpack a plugin from its base64-encoded/gzipped state. 156 * 157 * @param string $plugin Plugin_base64 158 * @param boolean $normalize Check/normalize some fields 159 * @return array 160 */ 161 162 public function extract($plugin, $normalize = true) 163 { 164 if (strpos($plugin, '$plugin=\'') !== false) { 165 @ini_set('pcre.backtrack_limit', '1000000'); 166 $plugin = preg_replace('@.*\$plugin=\'([\w=+/]+)\'.*@s', '$1', $plugin); 167 } 168 169 $plugin = preg_replace('/^#.*$/m', '', $plugin); 170 $plugin = base64_decode($plugin); 171 172 if (strncmp($plugin, "\x1F\x8B", 2) === 0) { 173 $plugin = @gzinflate(substr($plugin, 10)); 174 } 175 176 $plugin = @unserialize($plugin); 177 178 if (empty($plugin['name'])) { 179 return false; 180 } 181 182 if ($normalize) { 183 $plugin['type'] = empty($plugin['type']) ? 0 : min(max(intval($plugin['type']), 0), 5); 184 $plugin['order'] = empty($plugin['order']) ? 5 : min(max(intval($plugin['order']), 1), 9); 185 $plugin['flags'] = empty($plugin['flags']) ? 0 : intval($plugin['flags']); 186 } 187 188 return $plugin; 189 } 190 191 /** 192 * Extract a section from plugin template. 193 * 194 * @param string $pack Plugin template 195 * @param array|string $section Section 196 * @return array 197 */ 198 199 public function extractSection($pack, $section = 'CODE') 200 { 201 $result = array(false); 202 203 foreach ((array)$section as $s) { 204 $code = ''; 205 $pack = preg_split('/^\#\s*\-{3,}\s*BEGIN PLUGIN '.$s.'\s*\-{3,}\s*$(.*)^\#\s*\-{3,}\s*END PLUGIN '.$s.'\s*\-{3,}\s*$/Ums', $pack, null, PREG_SPLIT_DELIM_CAPTURE); 206 207 foreach ($pack as $i => $chunk) { 208 if ($i % 2) { 209 $code .= $chunk; 210 $pack[$i] = ''; 211 } 212 } 213 214 $result[] = $code; 215 $pack = implode('', $pack); 216 } 217 218 return array($pack) + $result; 219 } 220 221 /** 222 * Read a plugin from file. 223 * 224 * @param string|array $name|$path Plugin name 225 * @param boolean $normalize Check/normalize some fields 226 * @return array 227 */ 228 229 public function read($name, $normalize = true) 230 { 231 global $txp_user; 232 233 $plugin = array(); 234 235 if (is_array($name)) { 236 list($name, $target_path) = $name + array(null, null); 237 238 if (!($pack = txp_get_contents($target_path))) { 239 return false; 240 } 241 242 list($pack, $code, $help_raw) = $this->extractSection($pack, array('CODE', 'HELP')); 243 $plugin = array_filter(compact('code', 'help_raw')); 244 245 if (!empty($code)) { 246 file_put_contents($target_path, $pack); 247 include $target_path; 248 } 249 } else { 250 $name = sanitizeForFile($name); 251 $dir = PLUGINPATH.DS.$name; 252 253 if (!is_dir($dir)) { 254 return false; 255 } 256 257 $dir .= DS; 258 $target_path = $dir.$name.'.php'; 259 } 260 261 if (empty($plugin['code']) && $code = txp_get_contents($target_path)) { 262 $code = preg_replace('/^\s*<\?(?:php)?\s*|\s*\?>\s*$/i', '', $code); 263 $plugin['code'] = $code; 264 } 265 266 if (!empty($dir)) { 267 if ($info = txp_get_contents($dir.'manifest.json')) { 268 $plugin += json_decode($info, true); 269 } 270 271 if ($textpack = txp_get_contents($dir.'textpack.txp')) { 272 $plugin['textpack'] = $textpack; 273 } 274 275 if ($data = txp_get_contents($dir.'data.txp')) { 276 $plugin['data'] = $data; 277 } 278 279 if ($help = txp_get_contents($dir.'help.html')) { 280 $plugin['help'] = $help; 281 } elseif ($help = txp_get_contents($dir.'help.textile')) { 282 $plugin['help_raw'] = $help; 283 } 284 } 285 286 $plugin += array('name' => $name, 'author' => get_author_name($txp_user)); 287 288 if ($normalize) { 289 $plugin['type'] = empty($plugin['type']) ? 0 : min(max(intval($plugin['type']), 0), 5); 290 $plugin['order'] = empty($plugin['order']) ? 5 : min(max(intval($plugin['order']), 1), 9); 291 $plugin['flags'] = empty($plugin['flags']) ? 0 : intval($plugin['flags']); 292 } 293 294 return $plugin; 295 } 296 297 /** 298 * Delete plugin from the database. 299 * 300 * If the plugin has been programmed to respond to lifecycle events, 301 * the following callbacks are raised, in this order: 302 * plugin_lifecycle.{plugin_name} > disabled 303 * plugin_lifecycle.{plugin_name} > deleted 304 * 305 * @param string $name Plugin name 306 */ 307 308 public function delete($name) 309 { 310 if (! empty($name)) { 311 if (safe_field("flags", 'txp_plugin', "name = '".doSlash($name)."'") & PLUGIN_LIFECYCLE_NOTIFY) { 312 load_plugin($name, true); 313 set_error_handler("pluginErrorHandler"); 314 callback_event("plugin_lifecycle.$name", 'disabled'); 315 callback_event("plugin_lifecycle.$name", 'deleted'); 316 restore_error_handler(); 317 } 318 319 safe_delete('txp_plugin', "name = '".doSlash($name)."'"); 320 safe_delete('txp_lang', "owner = '".doSlash($name)."'"); 321 gps('sync') and $this->updateFile($name, null); 322 } 323 } 324 325 /** 326 * Change plugin status: enabled or disabled. 327 * 328 * If the plugin has been programmed to respond to lifecycle events, 329 * the following callbacks are raised depending on the given status: 330 * plugin_lifecycle.{plugin_name} > disabled 331 * plugin_lifecycle.{plugin_name} > enabled 332 * 333 * @param string $name Plugin name 334 * @param int $setStatus Plugin status. Toggle status, if null 335 */ 336 337 public function changeStatus($name, $setStatus = null) 338 { 339 if ($row = safe_row("flags, status", 'txp_plugin', "name = '".doSlash($name)."'")) { 340 if ($row['flags'] & PLUGIN_LIFECYCLE_NOTIFY) { 341 load_plugin($name, true); 342 set_error_handler("pluginErrorHandler"); 343 344 if ($setStatus === null) { 345 callback_event("plugin_lifecycle.$name", $row['status'] ? 'disabled' : 'enabled'); 346 } else { 347 callback_event("plugin_lifecycle.$name", $setStatus ? 'enabled' : 'disabled'); 348 } 349 350 restore_error_handler(); 351 } 352 353 if ($setStatus === null) { 354 $setStatus = "status = (1 - status)"; 355 } else { 356 $setStatus = "status = ". ($setStatus ? 1 : 0); 357 } 358 359 safe_update('txp_plugin', $setStatus, "name = '".doSlash($name)."'"); 360 } 361 } 362 363 /** 364 * Change plugin load priority. 365 * 366 * Plugins with a lower number are loaded first. 367 * 368 * @param string $name Plugin name 369 * @param int $order Plugin load priority 370 */ 371 372 public function changeOrder($name, $order) 373 { 374 $order = min(max(intval($order), 1), 9); 375 safe_update('txp_plugin', "load_order = $order", "name = '".doSlash($name)."'"); 376 } 377 378 /** 379 * Install/update a plugin Textpack. 380 * 381 * The process may be intercepted (for example, to fetch data from the 382 * filesystem) via the "txp.plugin > textpack.fetch" callback. 383 * 384 * @param string $name Plugin name 385 * @param boolean $reset Delete old strings 386 */ 387 388 public function installTextpack($name, $reset = false) 389 { 390 $owner = doSlash($name); 391 392 if ($reset) { 393 safe_delete('txp_lang', "owner = '{$owner}'"); 394 } 395 396 if (has_handler('txp.plugin', 'textpack.fetch')) { 397 $textpack = callback_event('txp.plugin', 'textpack.fetch', false, compact('name')); 398 } else { 399 $textpack = safe_field('textpack', 'txp_plugin', "name = '{$owner}'"); 400 } 401 402 $packParser = \Txp::get('\Textpattern\Textpack\Parser'); 403 $packParser->parse($textpack); 404 $packLanguages = $packParser->getLanguages(); 405 406 if (empty($packLanguages)) { 407 return; 408 } 409 410 $allpacks = array(); 411 412 foreach ($packLanguages as $lang_code) { 413 $allpacks[$lang_code] = $packParser->getStrings($lang_code); 414 } 415 416 if (in_array(TEXTPATTERN_DEFAULT_LANG, $packLanguages)) { 417 $fallback = TEXTPATTERN_DEFAULT_LANG; 418 } else { 419 // Use first language as default if possible. 420 $fallback = !empty($packLanguages[0]) ? $packLanguages[0] : TEXTPATTERN_DEFAULT_LANG; 421 } 422 423 $installed_langs = \Txp::get('\Textpattern\L10n\Lang')->installed(); 424 425 foreach ($installed_langs as $lang) { 426 if (!isset($allpacks[$lang])) { 427 $langpack = $allpacks[$fallback]; 428 } else { 429 $langpack = array(); 430 $done = array(); 431 432 // Manual merge since array_merge/array_merge_recursive don't work as expected 433 // on these multi-dimensional structures. 434 // There must be a more efficient way to do this... 435 foreach ($allpacks[$fallback] as $idx => $packEntry) { 436 if (isset($allpacks[$lang][$idx]['name']) && $allpacks[$lang][$idx]['name'] === $packEntry['name']) { 437 // Great! keys in the same order. 438 $done[] = $idx; 439 $langpack[] = $allpacks[$lang][$idx]; 440 } else { 441 // Drat, gotta search for it. 442 $found = false; 443 444 foreach ($allpacks[$lang] as $offset => $packSet) { 445 if (in_array($offset, $done)) { 446 continue; 447 } 448 449 if ($packSet['name'] === $packEntry['name']) { 450 $langpack[] = $packSet; 451 $found = true; 452 $done[] = $offset; 453 break; 454 } 455 } 456 457 if (!$found) { 458 $langpack[] = $packEntry; 459 } 460 } 461 } 462 } 463 464 // Ensure the language code in the pack, which may contain fallback strings, 465 // reflects the desired (to be installed) language code. 466 foreach ($langpack as $idx => $packBlock) { 467 $langpack[$idx]['lang'] = $lang; 468 } 469 470 \Txp::get('\Textpattern\L10n\Lang')->upsertPack($langpack, $name); 471 $langDir = PLUGINPATH.DS.$name.DS.'lang'.DS; 472 473 if (is_dir($langDir) && is_readable($langDir)) { 474 $plugLang = new \Textpattern\L10n\Lang($langDir); 475 $plugLang->installFile($lang, $name); 476 } 477 } 478 } 479 480 /** 481 * Install/update ALL plugin Textpacks. 482 * 483 * Used when a new language is added. 484 */ 485 486 public function installTextpacks() 487 { 488 if ($plugins = safe_column_num('name', 'txp_plugin', "textpack != '' ORDER BY load_order")) { 489 foreach ($plugins as $name) { 490 $this->installTextpack($name); 491 } 492 } 493 } 494 495 /** 496 * Create/update/delete plugin file. 497 * 498 * @param string $name The plugin 499 * @param string $code The code 500 */ 501 502 public function updateFile($name, $code = null) 503 { 504 if (!is_writable(PLUGINPATH)) { 505 return; 506 } 507 508 $filename = sanitizeForFile($name); 509 510 if (!isset($code)) { 511 return \Txp::get('\Textpattern\Admin\Tools')->removeFiles(PLUGINPATH, $filename); 512 } 513 514 if (!is_dir($dir = PLUGINPATH.DS.$filename)) { 515 mkdir($dir); 516 } 517 518 if (is_array($code)) { 519 if ($manifest = array_intersect_key($code, self::$metaData)) { 520 file_put_contents($dir.DS.'manifest.json', json_encode($manifest), LOCK_EX); 521 } 522 523 foreach (array( 524 'help' => 'help.html', 525 'help_raw' => 'help.textile', 526 'textpack' => 'textpack.txp', 527 'data' => 'data.txp' 528 ) as $key => $file 529 ) { 530 if (isset($code[$key])) { 531 file_put_contents($dir.DS.$file, $code[$key], LOCK_EX); 532 } 533 } 534 535 $code = isset($code['code']) ? $code['code'] : ''; 536 } 537 538 return file_put_contents($dir.DS.$filename.'.php', '<?php'.n.$code, LOCK_EX); 539 } 540 541 /** 542 * Fetch the plugin's 'data' field. 543 * 544 * The call can be intercepted (for example, to fetch data from the 545 * filesystem) via the "txp.plugin > data.fetch" callback. 546 * 547 * @param string $name The plugin 548 * @return string 549 */ 550 551 public function fetchData($name) 552 { 553 if (has_handler('txp.plugin', 'data.fetch')) { 554 $data = callback_event('txp.plugin', 'data.fetch', false, compact('name')); 555 } else { 556 $data = safe_field('data', 'txp_plugin', "name = '".doSlash($name)."'"); 557 } 558 559 return $data; 560 } 561 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title