Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/include/txp_plugin.php - 666 lines - 21951 bytes - Summary - Text - Print

Description: Plugins panel.

   1  <?php
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * http://textpattern.com
   6   *
   7   * Copyright (C) 2005 Dean Allen
   8   * Copyright (C) 2016 The Textpattern Development Team
   9   *
  10   * This file is part of Textpattern.
  11   *
  12   * Textpattern is free software; you can redistribute it and/or
  13   * modify it under the terms of the GNU General Public License
  14   * as published by the Free Software Foundation, version 2.
  15   *
  16   * Textpattern is distributed in the hope that it will be useful,
  17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19   * GNU General Public License for more details.
  20   *
  21   * You should have received a copy of the GNU General Public License
  22   * along with Textpattern. If not, see <http://www.gnu.org/licenses/>.
  23   */
  24  
  25  /**
  26   * Plugins panel.
  27   *
  28   * @package Admin\Plugin
  29   */
  30  
  31  if (!defined('txpinterface')) {
  32      die('txpinterface is undefined.');
  33  }
  34  
  35  if ($event == 'plugin') {
  36      require_privs('plugin');
  37  
  38      $available_steps = array(
  39          'plugin_edit'       => true,
  40          'plugin_help'       => false,
  41          'plugin_list'       => false,
  42          'plugin_install'    => true,
  43          'plugin_save'       => true,
  44          'plugin_verify'     => true,
  45          'switch_status'     => true,
  46          'plugin_multi_edit' => true,
  47      );
  48  
  49      if ($step && bouncer($step, $available_steps)) {
  50          $step();
  51      } else {
  52          plugin_list();
  53      }
  54  }
  55  
  56  /**
  57   * The main panel listing all installed plugins.
  58   *
  59   * @param string|array $message The activity message
  60   */
  61  
  62  function plugin_list($message = '')
  63  {
  64      global $event;
  65  
  66      pagetop(gTxt('tab_plugins'), $message);
  67  
  68      extract(gpsa(array(
  69          'sort',
  70          'dir',
  71      )));
  72  
  73      if ($sort === '') {
  74          $sort = get_pref('plugin_sort_column', 'name');
  75      } else {
  76          if (!in_array($sort, array('name', 'status', 'author', 'version', 'modified', 'load_order'))) {
  77              $sort = 'name';
  78          }
  79  
  80          set_pref('plugin_sort_column', $sort, 'plugin', 2, '', 0, PREF_PRIVATE);
  81      }
  82  
  83      if ($dir === '') {
  84          $dir = get_pref('plugin_sort_dir', 'asc');
  85      } else {
  86          $dir = ($dir == 'desc') ? "desc" : "asc";
  87          set_pref('plugin_sort_dir', $dir, 'plugin', 2, '', 0, PREF_PRIVATE);
  88      }
  89  
  90      $sort_sql = "$sort $dir";
  91  
  92      $switch_dir = ($dir == 'desc') ? 'asc' : 'desc';
  93  
  94      echo n.'<div class="txp-layout">'.
  95          n.tag(
  96              hed(gTxt('tab_plugins'), 1, array('class' => 'txp-heading')),
  97              'div', array('class' => 'txp-layout-1col')
  98          ).
  99          n.tag_start('div', array(
 100              'class' => 'txp-layout-1col',
 101              'id'    => $event.'_container',
 102          )).
 103          n.tag(plugin_form(), 'div', array('class' => 'txp-control-panel'));
 104  
 105      $rs = safe_rows_start(
 106          "name, status, author, author_uri, version, description, length(help) AS help, ABS(STRCMP(MD5(code), code_md5)) AS modified, load_order, flags",
 107          'txp_plugin',
 108          "1 = 1 ORDER BY $sort_sql"
 109      );
 110  
 111      if ($rs and numRows($rs) > 0) {
 112          echo
 113              n.tag_start('form', array(
 114                  'class'  => 'multi_edit_form',
 115                  'id'     => 'plugin_form',
 116                  'name'   => 'longform',
 117                  'method' => 'post',
 118                  'action' => 'index.php',
 119              )).
 120              n.tag_start('div', array('class' => 'txp-listtables')).
 121              n.tag_start('table', array('class' => 'txp-list')).
 122              n.tag_start('thead').
 123              tr(
 124                  hCell(
 125                      fInput('checkbox', 'select_all', 0, '', '', '', '', '', 'select_all'),
 126                          '', ' class="txp-list-col-multi-edit" scope="col" title="'.gTxt('toggle_all_selected').'"'
 127                  ).
 128                  column_head(
 129                      'plugin', 'name', 'plugin', true, $switch_dir, '', '',
 130                          (('name' == $sort) ? "$dir " : '').'txp-list-col-name'
 131                  ).
 132                  column_head(
 133                      'author', 'author', 'plugin', true, $switch_dir, '', '',
 134                          (('author' == $sort) ? "$dir " : '').'txp-list-col-author'
 135                  ).
 136                  column_head(
 137                      'version', 'version', 'plugin', true, $switch_dir, '', '',
 138                          (('version' == $sort) ? "$dir " : '').'txp-list-col-version'
 139                  ).
 140                  column_head(
 141                      'plugin_modified', 'modified', 'plugin', true, $switch_dir, '', '',
 142                          (('modified' == $sort) ? "$dir " : '').'txp-list-col-modified'
 143                  ).
 144                  hCell(gTxt(
 145                      'description'), '', ' class="txp-list-col-description" scope="col"'
 146                  ).
 147                  column_head(
 148                      'active', 'status', 'plugin', true, $switch_dir, '', '',
 149                          (('status' == $sort) ? "$dir " : '').'txp-list-col-status'
 150                  ).
 151                  column_head(
 152                      'order', 'load_order', 'plugin', true, $switch_dir, '', '',
 153                          (('load_order' == $sort) ? "$dir " : '').'txp-list-col-load-order'
 154                  ).
 155                  hCell(
 156                      gTxt('manage'), '',  ' class="txp-list-col-manage" scope="col"'
 157                  )
 158              ).
 159              n.tag_end('thead').
 160              n.tag_start('tbody');
 161  
 162          while ($a = nextRow($rs)) {
 163              foreach ($a as $key => $value) {
 164                  $$key = txpspecialchars($value);
 165              }
 166  
 167              // Fix up the description for clean cases.
 168              $description = preg_replace(
 169                  array(
 170                      '#&lt;br /&gt;#',
 171                      '#&lt;(/?(a|b|i|em|strong))&gt;#',
 172                      '#&lt;a href=&quot;(https?|\.|\/|ftp)([A-Za-z0-9:/?.=_]+?)&quot;&gt;#',
 173                  ),
 174                  array(
 175                      '<br />',
 176                      '<$1>',
 177                      '<a href="$1$2">',
 178                  ),
 179                  $description
 180              );
 181  
 182              if (!empty($help)) {
 183                  $help = href(gTxt('help'), array(
 184                      'event' => 'plugin',
 185                      'step'  => 'plugin_help',
 186                      'name'  => $name,
 187                  ), array('class' => 'plugin-help'));
 188              }
 189  
 190              if ($flags & PLUGIN_HAS_PREFS) {
 191                  $plugin_prefs = span(
 192                      sp.span('&#124;', array('role' => 'separator')).
 193                      sp.href(gTxt('plugin_prefs'), array('event' => 'plugin_prefs.'.$name)),
 194                      array('class' => 'plugin-prefs')
 195                  );
 196              } else {
 197                  $plugin_prefs = '';
 198              }
 199  
 200              $manage = array();
 201  
 202              if ($help) {
 203                  $manage[] = $help;
 204              }
 205  
 206              if ($plugin_prefs) {
 207                  $manage[] = $plugin_prefs;
 208              }
 209  
 210              $manage_items = ($manage) ? join($manage) : '-';
 211              $edit_url = eLink('plugin', 'plugin_edit', 'name', $name, $name);
 212  
 213              echo tr(
 214                  td(
 215                      fInput('checkbox', 'selected[]', $name), '', 'txp-list-col-multi-edit'
 216                  ).
 217                  hCell(
 218                      $edit_url, '', ' class="txp-list-col-name" scope="row"'
 219                  ).
 220                  td(
 221                      href($author, $a['author_uri'], array('rel' => 'external')), '', 'txp-list-col-author'
 222                  ).
 223                  td(
 224                      $version, '', 'txp-list-col-version'
 225                  ).
 226                  td(
 227                      ($modified ? span(gTxt('yes'), array('class' => 'warning')) : ''), '', 'txp-list-col-modified'
 228                  ).
 229                  td(
 230                      $description, '', 'txp-list-col-description'
 231                  ).
 232                  td(
 233                      status_link($status, $name, yes_no($status)), '', 'txp-list-col-status'
 234                  ).
 235                  td(
 236                      $load_order, '', 'txp-list-col-load-order'
 237                  ).
 238                  td(
 239                      $manage_items, '', 'txp-list-col-manage'
 240                  ),
 241                  $status ? ' class="active"' : ''
 242              );
 243  
 244              unset($name, $page, $deletelink);
 245          }
 246  
 247          echo
 248              n.tag_end('tbody').
 249              n.tag_end('table').
 250              n.tag_end('div'). // End of .txp-listtables.
 251              plugin_multiedit_form('', $sort, $dir, '', '').
 252              tInput().
 253              n.tag_end('form');
 254      }
 255  
 256      echo n.tag_end('div'). // End of .txp-layout-1col.
 257          n.'</div>'; // End of .txp-layout.
 258  }
 259  
 260  /**
 261   * Toggles a plugin's status.
 262   */
 263  
 264  function switch_status()
 265  {
 266      extract(array_map('assert_string', gpsa(array('thing', 'value'))));
 267      $change = ($value == gTxt('yes')) ? 0 : 1;
 268  
 269      safe_update('txp_plugin', "status = $change", "name = '".doSlash($thing)."'");
 270  
 271      if (safe_field('flags', 'txp_plugin', "name = '".doSlash($thing)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
 272          load_plugin($thing, true);
 273          $message = callback_event("plugin_lifecycle.$thing", $change ? 'enabled' : 'disabled');
 274      }
 275  
 276      echo gTxt($change ? 'yes' : 'no');
 277  }
 278  
 279  /**
 280   * Renders and outputs the plugin editor panel.
 281   */
 282  
 283  function plugin_edit()
 284  {
 285      global $event;
 286  
 287      $name = gps('name');
 288      pagetop(gTxt('edit_plugins'));
 289  
 290      echo plugin_edit_form($name);
 291  }
 292  
 293  /**
 294   * Plugin help viewer panel.
 295   */
 296  
 297  function plugin_help()
 298  {
 299      global $event;
 300  
 301      $name = gps('name');
 302      pagetop(gTxt('plugin_help'));
 303      $help = ($name) ? safe_field('help', 'txp_plugin', "name = '".doSlash($name)."'") : '';
 304      echo n.tag($help, 'div', array('class' => 'txp-layout-textbox'));
 305  }
 306  
 307  /**
 308   * Renders an editor form for plugins.
 309   *
 310   * @param  string $name The plugin
 311   * @return string HTML
 312   */
 313  
 314  function plugin_edit_form($name = '')
 315  {
 316      assert_string($name);
 317      $code = ($name) ? fetch('code', 'txp_plugin', 'name', $name) : '';
 318      $thing = ($code) ? $code : '';
 319  
 320      return
 321          form(
 322              hed(gTxt('edit_plugin', array('{name}' => $name)), 2).
 323              graf('<textarea class="code" id="plugin_code" name="code" cols="'.INPUT_XLARGE.'" rows="'.TEXTAREA_HEIGHT_LARGE.'" dir="ltr">'.txpspecialchars($thing).'</textarea>', ' class="edit-plugin-code"').
 324              graf(
 325                  sLink('plugin', '', gTxt('cancel'), 'txp-button').
 326                  fInput('submit', '', gTxt('save'), 'publish'),
 327                  array('class' => 'txp-edit-actions')
 328              ).
 329              eInput('plugin').
 330              sInput('plugin_save').
 331              hInput('name', $name), '', '', 'post', '', '', 'plugin_details');
 332  }
 333  
 334  /**
 335   * Saves edited plugin code.
 336   */
 337  
 338  function plugin_save()
 339  {
 340      extract(doSlash(array_map('assert_string', gpsa(array('name', 'code')))));
 341  
 342      safe_update('txp_plugin', "code = '$code'", "name = '$name'");
 343  
 344      $message = gTxt('plugin_saved', array('{name}' => $name));
 345  
 346      plugin_list($message);
 347  }
 348  
 349  /**
 350   * Renders a status link.
 351   *
 352   * @param  string $status   The new status
 353   * @param  string $name     The plugin
 354   * @param  string $linktext The label
 355   * @return string HTML
 356   * @access private
 357   * @see    asyncHref()
 358   */
 359  
 360  function status_link($status, $name, $linktext)
 361  {
 362      return asyncHref(
 363          $linktext,
 364          array('step' => 'switch_status', 'thing' => $name)
 365      );
 366  }
 367  
 368  /**
 369   * Plugin installation's preview step.
 370   *
 371   * Outputs a panel displaying the plugin's source code
 372   * and the included help file.
 373   */
 374  
 375  function plugin_verify()
 376  {
 377      global $event;
 378  
 379      if (ps('txt_plugin')) {
 380          $plugin = join("\n", file($_FILES['theplugin']['tmp_name']));
 381      } else {
 382          $plugin = assert_string(ps('plugin'));
 383      }
 384  
 385      // Check for pre-4.0 style plugin.
 386      if (strpos($plugin, '$plugin=\'') !== false) {
 387          // Try to increase PCRE's backtrack limit in PHP 5.2+ to accommodate to
 388          // x-large plugins. See https://bugs.php.net/bug.php?id=40846.
 389          @ini_set('pcre.backtrack_limit', '1000000');
 390          $plugin = preg_replace('@.*\$plugin=\'([\w=+/]+)\'.*@s', '$1', $plugin);
 391          // Have we hit yet another PCRE restriction?
 392          if ($plugin === null) {
 393              plugin_list(array(gTxt('plugin_pcre_error', array('{errno}' => preg_last_error())), E_ERROR));
 394  
 395              return;
 396          }
 397      }
 398  
 399      // Strip out #comment lines.
 400      $plugin = preg_replace('/^#.*$/m', '', $plugin);
 401  
 402      if ($plugin === null) {
 403          plugin_list(array(gTxt('plugin_pcre_error', array('{errno}' => preg_last_error())), E_ERROR));
 404  
 405          return;
 406      }
 407  
 408      if (isset($plugin)) {
 409          $plugin_encoded = $plugin;
 410          $plugin = base64_decode($plugin);
 411  
 412          if (strncmp($plugin, "\x1F\x8B", 2) === 0) {
 413              if (function_exists('gzinflate')) {
 414                  $plugin = gzinflate(substr($plugin, 10));
 415              } else {
 416                  plugin_list(array(gTxt('plugin_compression_unsupported'), E_ERROR));
 417  
 418                  return;
 419              }
 420          }
 421  
 422          if ($plugin = @unserialize($plugin)) {
 423              if (is_array($plugin)) {
 424                  $source = '';
 425  
 426                  if (isset($plugin['help_raw']) && empty($plugin['allow_html_help'])) {
 427                      $textile = new \Textpattern\Textile\Parser();
 428                      $help_source = $textile->textileRestricted($plugin['help_raw'], 0, 0);
 429                  } else {
 430                      $help_source = highlight_string($plugin['help'], true);
 431                  }
 432  
 433                  $source .= highlight_string('<?php'.$plugin['code'].'?>', true);
 434                  $sub = graf(
 435                      sLink('plugin', '', gTxt('cancel'), 'txp-button').
 436                      fInput('submit', '', gTxt('install'), 'publish'),
 437                      array('class' => 'txp-edit-actions')
 438                  );
 439  
 440                  pagetop(gTxt('verify_plugin'));
 441                  echo form(
 442                      hed(gTxt('previewing_plugin'), 2).
 443                      tag($source, 'div', ' class="code" id="preview-plugin" dir="ltr"').
 444                      hed(gTxt('plugin_help').':', 2).
 445                      tag($help_source, 'div', ' class="code" id="preview-help" dir="ltr"').
 446                      $sub.
 447                      sInput('plugin_install').
 448                      eInput('plugin').
 449                      hInput('plugin64', $plugin_encoded), '', '', 'post', 'plugin-info', '', 'plugin_preview'
 450                  );
 451  
 452                  return;
 453              }
 454          }
 455      }
 456  
 457      plugin_list(array(gTxt('bad_plugin_code'), E_ERROR));
 458  }
 459  
 460  /**
 461   * Installs a plugin.
 462   */
 463  
 464  function plugin_install()
 465  {
 466      $plugin = assert_string(ps('plugin64'));
 467  
 468      if (strpos($plugin, '$plugin=\'') !== false) {
 469          @ini_set('pcre.backtrack_limit', '1000000');
 470          $plugin = preg_replace('@.*\$plugin=\'([\w=+/]+)\'.*@s', '$1', $plugin);
 471      }
 472  
 473      $plugin = preg_replace('/^#.*$/m', '', $plugin);
 474  
 475      if (trim($plugin)) {
 476          $plugin = base64_decode($plugin);
 477  
 478          if (strncmp($plugin, "\x1F\x8B", 2) === 0) {
 479              $plugin = gzinflate(substr($plugin, 10));
 480          }
 481  
 482          if ($plugin = unserialize($plugin)) {
 483              if (is_array($plugin)) {
 484                  extract($plugin);
 485  
 486                  $type = empty($type) ? 0 : min(max(intval($type), 0), 5);
 487                  $order = empty($order) ? 5 : min(max(intval($order), 1), 9);
 488                  $flags = empty($flags) ? 0 : intval($flags);
 489                  $exists = fetch('name', 'txp_plugin', 'name', $name);
 490  
 491                  if (isset($help_raw) && empty($plugin['allow_html_help'])) {
 492                      // Default: help is in Textile format.
 493                      $textile = new \Textpattern\Textile\Parser();
 494                      $help = $textile->textileRestricted($help_raw, 0, 0);
 495                  }
 496  
 497                  if ($exists) {
 498                      $rs = safe_update(
 499                         'txp_plugin',
 500                          "type        = $type,
 501                          author       = '".doSlash($author)."',
 502                          author_uri   = '".doSlash($author_uri)."',
 503                          version      = '".doSlash($version)."',
 504                          description  = '".doSlash($description)."',
 505                          help         = '".doSlash($help)."',
 506                          code         = '".doSlash($code)."',
 507                          code_restore = '".doSlash($code)."',
 508                          code_md5     = '".doSlash($md5)."',
 509                          flags        = $flags",
 510                          "name        = '".doSlash($name)."'"
 511                      );
 512                  } else {
 513                      $rs = safe_insert(
 514                         'txp_plugin',
 515                         "name         = '".doSlash($name)."',
 516                          status       = 0,
 517                          type         = $type,
 518                          author       = '".doSlash($author)."',
 519                          author_uri   = '".doSlash($author_uri)."',
 520                          version      = '".doSlash($version)."',
 521                          description  = '".doSlash($description)."',
 522                          help         = '".doSlash($help)."',
 523                          code         = '".doSlash($code)."',
 524                          code_restore = '".doSlash($code)."',
 525                          code_md5     = '".doSlash($md5)."',
 526                          load_order   = '".$order."',
 527                          flags        = $flags"
 528                      );
 529                  }
 530  
 531                  if ($rs and $code) {
 532                      if (!empty($textpack)) {
 533                          // Plugins tag their Textpack by plugin name.
 534                          // The ownership may be overridden in the Textpack itself.
 535                          $textpack = "#@owner {$name}".n.$textpack;
 536                          install_textpack($textpack, false);
 537                      }
 538  
 539                      if ($flags & PLUGIN_LIFECYCLE_NOTIFY) {
 540                          load_plugin($name, true);
 541                          $message = callback_event("plugin_lifecycle.$name", 'installed');
 542                      }
 543  
 544                      if (empty($message)) {
 545                          $message = gTxt('plugin_installed', array('{name}' => $name));
 546                      }
 547  
 548                      plugin_list($message);
 549  
 550                      return;
 551                  } else {
 552                      $message = array(gTxt('plugin_install_failed', array('{name}' => $name)), E_ERROR);
 553                      plugin_list($message);
 554  
 555                      return;
 556                  }
 557              }
 558          }
 559      }
 560  
 561      plugin_list(array(gTxt('bad_plugin_code'), E_ERROR));
 562  }
 563  
 564  /**
 565   * Renders a plugin installation form.
 566   *
 567   * @return string HTML
 568   * @access private
 569   * @see    form()
 570   */
 571  
 572  function plugin_form()
 573  {
 574      return form(
 575          tag(gTxt('install_plugin'), 'label', ' for="plugin-install"').popHelp('install_plugin').
 576          '<textarea class="code" id="plugin-install" name="plugin" cols="'.INPUT_LARGE.'" rows="'.TEXTAREA_HEIGHT_SMALL.'" dir="ltr"></textarea>'.
 577          fInput('submit', 'install_new', gTxt('upload')).
 578          eInput('plugin').
 579          sInput('plugin_verify'), '', '', 'post', 'plugin-data', '', 'plugin_install_form');
 580  }
 581  
 582  /**
 583   * Renders a multi-edit form widget for plugins.
 584   *
 585   * @param  int    $page          The current page
 586   * @param  string $sort          The sort criteria
 587   * @param  string $dir           The sort direction
 588   * @param  string $crit          The search term
 589   * @param  string $search_method The search method
 590   * @return string HTML
 591   */
 592  
 593  function plugin_multiedit_form($page, $sort, $dir, $crit, $search_method)
 594  {
 595      $orders = selectInput('order', array(
 596          1 => 1,
 597          2 => 2,
 598          3 => 3,
 599          4 => 4,
 600          5 => 5,
 601          6 => 6,
 602          7 => 7,
 603          8 => 8,
 604          9 => 9,
 605      ), 5, false);
 606  
 607      $methods = array(
 608          'changestatus' => gTxt('changestatus'),
 609          'changeorder'  => array('label' => gTxt('changeorder'), 'html' => $orders),
 610          'delete'       => gTxt('delete'),
 611      );
 612  
 613      return multi_edit($methods, 'plugin', 'plugin_multi_edit', $page, $sort, $dir, $crit, $search_method);
 614  }
 615  
 616  /**
 617   * Processes multi-edit actions.
 618   */
 619  
 620  function plugin_multi_edit()
 621  {
 622      $selected = ps('selected');
 623      $method = assert_string(ps('edit_method'));
 624  
 625      if (!$selected or !is_array($selected)) {
 626          return plugin_list();
 627      }
 628  
 629      $where = "name IN ('".join("','", doSlash($selected))."')";
 630  
 631      switch ($method) {
 632          case 'delete':
 633              foreach ($selected as $name) {
 634                  if (safe_field("flags", 'txp_plugin', "name = '".doSlash($name)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
 635                      load_plugin($name, true);
 636                      callback_event("plugin_lifecycle.$name", 'disabled');
 637                      callback_event("plugin_lifecycle.$name", 'deleted');
 638                  }
 639              }
 640              // Remove plugins.
 641              safe_delete('txp_plugin', $where);
 642              // Remove plugin's l10n strings.
 643              safe_delete('txp_lang', "owner IN ('".join("','", doSlash($selected))."')");
 644              break;
 645          case 'changestatus':
 646              foreach ($selected as $name) {
 647                  if (safe_field("flags", 'txp_plugin', "name = '".doSlash($name)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
 648                      $status = safe_field("status", 'txp_plugin', "name = '".doSlash($name)."'");
 649                      load_plugin($name, true);
 650                      // Note: won't show returned messages anywhere due to
 651                      // potentially overwhelming verbiage.
 652                      callback_event("plugin_lifecycle.$name", $status ? 'disabled' : 'enabled');
 653                  }
 654              }
 655              safe_update('txp_plugin', "status = (1 - status)", $where);
 656              break;
 657          case 'changeorder':
 658              $order = min(max(intval(ps('order')), 1), 9);
 659              safe_update('txp_plugin', "load_order = $order", $where);
 660              break;
 661      }
 662  
 663      $message = gTxt('plugin_'.($method == 'delete' ? 'deleted' : 'updated'), array('{name}' => join(', ', $selected)));
 664  
 665      plugin_list($message);
 666  }

title

Description

title

Description

title

Description

title

title

Body