Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/vendors/Textpattern/Plugin/Plugin.php - 561 lines - 18130 bytes - Summary - Text - Print

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

title

Description

title

Description

title

title

Body