Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/publish.php - 1321 lines - 42683 bytes - Summary - Text - Print

Description: Site hostname.

   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  if (!defined('txpath')) {
  25      define("txpath", dirname(__FILE__));
  26  }
  27  
  28  if (!defined("txpinterface")) {
  29      die('If you just updated and expect to see your site here, please also update the files in your main installation directory.'.
  30          ' (Otherwise note that publish.php cannot be called directly.)');
  31  }
  32  
  33  global $trace;
  34  
  35  $trace->start('[PHP includes, stage 2]');
  36  include_once txpath.'/vendors/Textpattern/Loader.php';
  37  
  38  $loader = new \Textpattern\Loader(txpath.'/vendors');
  39  $loader->register();
  40  
  41  $loader = new \Textpattern\Loader(txpath.'/lib');
  42  $loader->register();
  43  
  44  include_once txpath.'/lib/txplib_db.php';
  45  include_once txpath.'/lib/admin_config.php';
  46  include_once txpath.'/publish/log.php';
  47  
  48  $trace->stop();
  49  
  50  set_error_handler('publicErrorHandler', error_reporting());
  51  
  52  ob_start();
  53  
  54  // Get logged user.
  55  $userInfo = is_logged_in();
  56  
  57  // Initialise the current user.
  58  $txp_user = empty($userInfo) ? null : $userInfo['name'];
  59  
  60  // Get all prefs as an array.
  61  $prefs = get_prefs(empty($userInfo['name']) ? '' : array('', $userInfo['name']));
  62  plug_privs(null, $userInfo);
  63  
  64  // Add prefs to globals.
  65  extract($prefs);
  66  
  67  // Check the size of the URL request.
  68  bombShelter();
  69  
  70  $txp_sections = array();
  71  $txp_current_tag = '';
  72  $txp_parsed = $txp_else = $txp_item = $txp_context = $txp_yield = $yield = array();
  73  $txp_atts = null;
  74  
  75  isset($pretext) or $pretext = array();
  76  
  77  // Set a higher error level during initialisation.
  78  set_error_level(@$production_status == 'live' ? 'testing' : @$production_status);
  79  
  80  // disable tracing in live environment.
  81  if ($production_status == 'live') {
  82      Trace::setQuiet(true);
  83  }
  84  
  85  // Use the current URL path if $siteurl is unknown.
  86  if (empty($siteurl)) {
  87      $httphost = preg_replace('/[^-_a-zA-Z0-9.:]/', '', $_SERVER['HTTP_HOST']);
  88      $prefs['siteurl'] = $siteurl = $httphost.rtrim(dirname($_SERVER['SCRIPT_NAME']), DS);
  89  }
  90  
  91  if (empty($path_to_site)) {
  92      updateSitePath(dirname(dirname(__FILE__)));
  93  }
  94  
  95  if (!defined('PROTOCOL')) {
  96      switch (serverSet('HTTPS')) {
  97          case '':
  98          case 'off': // ISAPI with IIS.
  99              define('PROTOCOL', 'http://');
 100              break;
 101          default:
 102              define('PROTOCOL', 'https://');
 103              break;
 104      }
 105  }
 106  
 107  // Definitive HTTP address of the site.
 108  if (!defined('hu')) {
 109      define('hu', PROTOCOL.$siteurl.'/');
 110  }
 111  
 112  // Relative URL global.
 113  if (!defined('rhu')) {
 114      define('rhu', preg_replace('|^https?://[^/]+|', '', hu));
 115  }
 116  
 117  // HTTP address of the site serving images.
 118  if (!defined('ihu')) {
 119      define('ihu', hu);
 120  }
 121  
 122  // HTTP address of Textpattern admin URL.
 123  if (!defined('ahu')) {
 124      if (empty($txpcfg['admin_url'])) {
 125          $adminurl = hu.'textpattern/';
 126      } else {
 127          $adminurl = PROTOCOL.rtrim(preg_replace('|^https?://|', '', $txpcfg['admin_url']), '/').'/';
 128      }
 129  
 130      define('ahu', $adminurl);
 131  }
 132  
 133  // Shared admin and public cookie_domain when using multisite admin URL.
 134  if (!defined('cookie_domain')) {
 135      if (!isset($txpcfg['cookie_domain'])) {
 136          $txpcfg['cookie_domain'] = '';
 137      }
 138  
 139      define('cookie_domain', $txpcfg['cookie_domain']);
 140  }
 141  
 142  if (!defined('SITE_HOST')) {
 143      /**
 144       * Site hostname.
 145       *
 146       * @package Network
 147       * @since   4.6.0
 148       */
 149  
 150      define('SITE_HOST', (string) @parse_url(hu, PHP_URL_HOST));
 151  }
 152  
 153  if (!defined('IMPATH')) {
 154      /**
 155       * Path to image directory.
 156       *
 157       * @package Image
 158       */
 159  
 160      define('IMPATH', $path_to_site.DS.$img_dir.DS);
 161  }
 162  
 163  // 1.0: a new $here variable in the top-level index.php should let us know the
 164  // server path to the live site let's save it to prefs.
 165  if (isset($here) and $path_to_site != $here) {
 166      updateSitePath($here);
 167  }
 168  
 169  if (!defined('LANG')) {
 170      /**
 171       * Currently active language.
 172       *
 173       * @package L10n
 174       */
 175  
 176      define('LANG', $language);
 177  }
 178  
 179  if (!defined('TXP_PATTERN')) {
 180      define('TXP_PATTERN', get_pref('enable_short_tags', false) ? 'txp|[a-z]+:' : 'txp:?');
 181  }
 182  
 183  if (!empty($locale)) {
 184      setlocale(LC_ALL, $locale);
 185  }
 186  
 187  // For backwards-compatibility (sort of) with plugins that expect the
 188  // $textarray global to be present.
 189  // Will remove in future.
 190  $textarray = array();
 191  
 192  // Here come the early plugins.
 193  if ($use_plugins) {
 194      load_plugins(false, 5);
 195  }
 196  
 197  // Request URI rewrite, anyone?
 198  callback_event('pretext', '', 1);
 199  $pretext = preText($pretext, null) + array('secondpass' => 0, '_txp_atts' => false);
 200  
 201  // Send 304 Not Modified if appropriate.
 202  
 203  if (empty($pretext['feed'])) {
 204      handle_lastmod();
 205  }
 206  
 207  if (txpinterface === 'css') {
 208      output_css(gps('s'), gps('n'), gps('t'));
 209  
 210      exit;
 211  }
 212  
 213  $txp_sections = safe_column(array('name'), 'txp_section');
 214  
 215  $trace->start('[PHP includes, stage 3]');
 216  
 217  include_once txpath.'/lib/txplib_publish.php';
 218  include_once txpath.'/lib/txplib_html.php';
 219  include_once txpath.'/lib/txplib_forms.php';
 220  include_once txpath.'/publish/comment.php';
 221  include_once txpath.'/publish/taghandlers.php';
 222  
 223  $trace->stop();
 224  
 225  // i18n.
 226  //load_lang(LANG);
 227  
 228  // Tidy up the site.
 229  janitor();
 230  
 231  // Here come the regular plugins.
 232  if ($use_plugins) {
 233      load_plugins();
 234  }
 235  
 236  callback_event('pretext');
 237  $pretext = preText($pretext, $prefs);
 238  callback_event('pretext_end');
 239  extract($pretext);
 240  
 241  // Now that everything is initialised, we can crank down error reporting.
 242  set_error_level($production_status);
 243  
 244  if (!empty($feed) && in_array($feed, array('atom', 'rss'), true)) {
 245      include txpath."/publish/{$feed}.php";
 246      echo $feed();
 247  
 248      if ($production_status !== 'live') {
 249          echo $trace->summary();
 250  
 251          if ($production_status === 'debug') {
 252              echo $trace->result();
 253          }
 254      }
 255  
 256      exit;
 257  }
 258  
 259  if (gps('parentid')) {
 260      if (ps('submit')) {
 261          saveComment();
 262      } elseif (ps('preview')) {
 263          checkCommentRequired(getComment());
 264      } elseif ($comments_mode == 1) {
 265          // Popup comments?
 266          header("Content-Type: text/html; charset=utf-8");
 267          exit(parse_form('popup_comments'));
 268      }
 269  }
 270  
 271  // We are dealing with a download.
 272  if ($s == 'file_download') {
 273      empty($filename) or output_file_download($filename);
 274      exit(0);
 275  }
 276  
 277  // Log the page view.
 278  log_hit($status);
 279  
 280  // -------------------------------------------------------------
 281  
 282  function preText($store, $prefs = null)
 283  {
 284      global $thisarticle, $txp_sections;
 285      static $url = array(), $out = null;
 286  
 287      if (empty($url)) {
 288          // Some useful vars for taghandlers, plugins.
 289          $out['request_uri'] = preg_replace("|^https?://[^/]+|i", "", serverSet('REQUEST_URI'));
 290          $out['qs'] = serverSet('QUERY_STRING');
 291  
 292          // IIS fix.
 293          if (!$out['request_uri'] and serverSet('SCRIPT_NAME')) {
 294              $out['request_uri'] = serverSet('SCRIPT_NAME').((serverSet('QUERY_STRING')) ? '?'.serverSet('QUERY_STRING') : '');
 295          }
 296  
 297          // Another IIS fix.
 298          if (!$out['request_uri'] and serverSet('argv')) {
 299              $argv = serverSet('argv');
 300              $out['request_uri'] = @substr($argv[0], strpos($argv[0], ';') + 1);
 301          }
 302  
 303          // Define the usable url, minus any subdirectories.
 304          // This is pretty ugly, if anyone wants to have a go at it.
 305          $out['subpath'] = $subpath = preg_quote(preg_replace("/https?:\/\/.*(\/.*)/Ui", "$1", hu), "/");
 306          $out['req'] = $req = preg_replace("/^$subpath/i", "/", $out['request_uri']);
 307  
 308          $url = chopUrl($req, 4);
 309  
 310          for ($out[0] = 0; isset($url['u'.($out[0]+1)]); $out[++$out[0]] = $url['u'.$out[0]]);
 311  
 312          if ($url['u1'] == 'rss' || gps('rss')) {
 313              $out['feed'] = 'rss';
 314          } elseif ($url['u1'] == 'atom' || gps('atom')) {
 315              $out['feed'] = 'atom';
 316          }
 317      }
 318  
 319      if (is_array($store)) {
 320          $out = $store + $out;
 321      }
 322  
 323      if (!isset($prefs)) {
 324          return $out;
 325      }
 326  
 327      extract($prefs);
 328  
 329      // Set messy variables.
 330      $out += makeOut('id', 's', 'c', 'context', 'q', 'm', 'pg', 'p', 'month', 'author', 'f');
 331      $out['skin'] = $out['page'] = $out['css'] = '';
 332  
 333      $is_404 = ($out['status'] == '404');
 334      $title = null;
 335  
 336      // If messy vars exist, bypass URL parsing.
 337      if (!$is_404 && !$out['id'] && !$out['s'] && txpinterface != 'css' && txpinterface != 'admin') {
 338          // Return clean URL test results for diagnostics.
 339          if (gps('txpcleantest')) {
 340              exit(show_clean_test($out));
 341          }
 342  
 343          // First we sniff out some of the preset URL schemes.
 344          extract($url);
 345  
 346          if (strlen($u1)) {
 347              $n = $out[0];
 348              $un = $out[$n];
 349  
 350              switch ($u1) {
 351                  case 'atom':
 352                      $out['feed'] = 'atom';
 353                      break;
 354  
 355                  case 'rss':
 356                      $out['feed'] = 'rss';
 357                      break;
 358  
 359                  // urldecode(strtolower(urlencode())) looks ugly but is the
 360                  // only way to make it multibyte-safe without breaking
 361                  // backwards-compatibility.
 362                  case 'section':
 363                  case urldecode(strtolower(urlencode(gTxt('section')))):
 364                      $out['s'] = $u2;
 365                      break;
 366  
 367                  case 'category':
 368                  case urldecode(strtolower(urlencode(gTxt('category')))):
 369                      $out['context'] = $u3 ? validContext($u2) : 'article';
 370                      if ($permlink_mode == 'breadcrumb_title') {
 371                          $n < 2 or $out['c'] = $un ? $un : $out[$n-1];
 372                      } else {
 373                          $out['c'] = $u3 ? $u3 : $u2;
 374                      }
 375                      break;
 376  
 377                  case 'author':
 378                  case urldecode(strtolower(urlencode(gTxt('author')))):
 379                      if ($u3) {
 380                          $out['context'] = validContext($u2);
 381                          $out['author'] = $u3;
 382                      } else {
 383                          $out['context'] = 'article';
 384                          $out['author'] = $u2;
 385                      }
 386  
 387                      $out['author'] = (!empty($out['author'])) ? $out['author'] : '';
 388                      break;
 389                      // AuthorID gets resolved from Name further down.
 390  
 391                  case 'file_download':
 392                  case urldecode(strtolower(urlencode(gTxt('file_download')))):
 393                      $out['s'] = 'file_download';
 394                      $out['id'] = (!empty($u2)) ? $u2 : '';
 395                      $out['filename'] = (!empty($u3)) ? $u3 : '';
 396                      break;
 397  
 398                  default:
 399                      $permlink_modes = array('default' => $permlink_mode) + array_column($txp_sections, 'permlink_mode', 'name');
 400                      $custom_modes = array_filter($permlink_modes, function ($v) use ($permlink_mode) {
 401                          return $v && $v !== $permlink_mode;
 402                      });
 403  
 404                      if (empty($custom_modes)) {
 405                          $permlink_guess = $permlink_mode;
 406                      } elseif (!empty($un) && empty($no_trailing_slash)) {// ID or url_title
 407                          $safe_un = doSlash($un);
 408  
 409                          $guessarticles = safe_rows(
 410                              '*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod',
 411                              'textpattern',
 412                              "url_title='$safe_un'".($n < 3 && is_numeric($un) ? " OR ID='$safe_un'" : '')
 413                          );
 414  
 415                          foreach ($guessarticles as $a) {
 416                              populateArticleData($a);
 417  
 418                              if (permlinkurl($thisarticle, '/') === $u0) {
 419                                  $permlink_guess = $permlink_modes[$a['Section']];
 420                                  break;
 421                              }
 422                          }
 423  
 424                          if (!isset($permlink_guess)) {
 425                              unset($thisarticle);
 426                              $is_404 = true;
 427                          } else {
 428                              $out['id'] = $thisarticle['thisid'];
 429                              $out['s'] = $thisarticle['section'];
 430                              $title = $thisarticle['url_title'];
 431                              $month = explode('-', strftime('%Y-%m-%d', $thisarticle['posted']));
 432                          }
 433                      }
 434  
 435                      if (empty($un) && is_numeric($u1) && strlen($u1) === 4 && !isset($permlink_modes[$u1])) {
 436                          // Could be a year.
 437                          $permlink_guess = 'year_month_day_title';
 438                      } elseif (!isset($permlink_guess) && isset($permlink_modes[$u1]) && ($n > 1 || !empty($no_trailing_slash))) {
 439                          $permlink_guess = $permlink_modes[$u1];
 440                      }
 441  
 442                      if (!$is_404 && empty($out['id'])) {
 443                          // Then see if the prefs-defined permlink scheme is usable.
 444                          switch (empty($permlink_guess) ? $permlink_mode : $permlink_guess) {
 445                              case 'section_id_title':
 446                                  $out['s'] = $u1;
 447  
 448                                  if (is_numeric($u2)) {
 449                                      $out['id'] = $u2;
 450                                  } else {
 451                                      $title = empty($u2) ? null : $u2;
 452                                  }
 453  
 454                                  break;
 455  
 456                              case 'section_category_title':
 457                              case 'breadcrumb_title':
 458                                  $out['s'] = $u1;
 459                                  $title = $n < 2 || empty($un) ? null : $un;
 460                                  isset($title) || $n <= 2 or $out['c'] = $out[$n-1];
 461  
 462                                  break;
 463  
 464                              case 'year_month_day_title':
 465                                  if (@checkdate(!empty($u2) ? $u2 : 1, !empty($u3) ? $u3 : 1, $u1)) {
 466                                      $title = empty($u4) ? null : $u4;
 467                                      $month = array($u1);
 468  
 469                                      if (!empty($u2)) {
 470                                          $month[] = str_pad(ltrim($u2), 2, '0', STR_PAD_LEFT);
 471                                          empty($u3) or $month[] = str_pad(ltrim($u3), 2, '0', STR_PAD_LEFT);
 472                                      }
 473                                  } elseif (@checkdate(!empty($u3) ? $u3 : 1, !empty($u4) ? $u4 : 1, $u2)) {
 474                                      $title = empty($u5) ? null : $u5;
 475                                      $out['s'] = $u1;
 476                                      $month = array($u2);
 477  
 478                                      if (!empty($u3)) {
 479                                          $month[] = str_pad(ltrim($u3), 2, '0', STR_PAD_LEFT);
 480                                          empty($u4) or $month[] = str_pad(ltrim($u4), 2, '0', STR_PAD_LEFT);
 481                                      }
 482                                  } elseif (empty($u3)) {
 483                                      $out['s'] = $u1;
 484                                      $title = empty($u2) ? null : $u2;
 485                                  } else {
 486                                      $is_404 = true;
 487                                  }
 488  
 489                                  break;
 490  
 491                              case 'section_title':
 492                                  $out['s'] = $u1;
 493                                  $title = empty($u2) ? null : $u2;
 494  
 495                                  break;
 496  
 497                              case 'id_title':
 498                                  if (is_numeric($u1)) {
 499                                      $out['id'] = $u1;
 500                                  } else {
 501                                      // We don't want to miss the /section/ pages.
 502                                      $out['s'] = $u1;
 503                                      $title = empty($u2) ? null : $u2;
 504                                  }
 505  
 506                                  break;
 507  
 508                              default:
 509                                  if (isset($u2)) {
 510                                      $out['s'] = $u1;
 511                                      $title = empty($u2) ? null : $u2;
 512                                  } else {
 513                                      $title = $u1;
 514                                  }
 515                          }
 516                      }
 517              }
 518          } else {
 519              $out['s'] = 'default';
 520          }
 521      }
 522  
 523      $out['context'] = validContext($out['context']);
 524  
 525      // Validate dates
 526      if ($out['month']) {
 527          $date = empty($month) ? '' : implode('-', $month);
 528          $month = explode('-', $out['month'], 3) + (!empty($month) ? $month : array());
 529  
 530          if (!$date || strpos($date, $out['month']) === 0 || strpos($out['month'], $date) === 0) {
 531              $month = implode('-', $month);
 532          } else {
 533              $out['month'] = $month = '';
 534              $is_404 = true;
 535          }
 536      } elseif (isset($month)) {
 537          $month = implode('-', $month);
 538          !empty($title) or $out['month'] = $month;
 539      }
 540  
 541      // Resolve AuthorID from Authorname.
 542      if ($out['author']) {
 543          $name = safe_field('name', 'txp_users', "RealName LIKE '".doSlash($out['author'])."'");
 544  
 545          if ($name) {
 546              $out['realname'] = $out['author'];
 547              $out['author'] = $name;
 548          } else {
 549              $out['author'] = $out['realname'] = '';
 550              $is_404 = true;
 551          }
 552      } else {
 553          $out['realname'] = '';
 554      }
 555  
 556      // Prevent to get the id for file_downloads.
 557      if ($out['s'] == 'file_download') {
 558          if (is_numeric($out['id'])) {
 559              global $thisfile;
 560  
 561              // Undo the double-encoding workaround for .gz files;
 562              // @see filedownloadurl().
 563              if (!empty($out['filename'])) {
 564                  $out['filename'] = preg_replace('/gz&$/i', 'gz', $out['filename']);
 565              }
 566  
 567              $fn = empty($out['filename']) ? '' : " AND filename = '".doSlash($out['filename'])."'";
 568              $rs = safe_row('*', 'txp_file', "id = ".intval($out['id'])." AND status = ".STATUS_LIVE." AND created <= ".now('created').$fn);
 569  
 570              $thisfile = $rs ? file_download_format_info($rs) : null;
 571          }
 572  
 573          $is_404 = $is_404 || empty($rs);
 574          $out = array_merge($out, $is_404 ? array('id' => '', 'file_error' => 404, 'status' => 404) : $rs);
 575      }
 576  
 577      // Allow article preview.
 578      elseif (gps('txpreview')) {
 579          doAuth();
 580  
 581          if (!has_privs('article.preview')) {
 582              txp_status_header('401 Unauthorized');
 583              exit(hed('401 Unauthorized', 1).graf(gTxt('restricted_area')));
 584          }
 585  
 586          global $nolog;
 587  
 588          $nolog = true;
 589          header('Cache-Control: no-cache, no-store, max-age=0');
 590          $rs = safe_row("ID AS id, Section AS s", 'textpattern', "ID = ".intval(gps('txpreview'))." LIMIT 1");
 591  
 592          if ($rs) {
 593              $is_404 = false;
 594              $out = array_merge($out, $rs);
 595          }
 596      } elseif ($out['context'] == 'article') {
 597          if (!$is_404 && empty($thisarticle) && (!empty($out['id']) || !empty($title))) {
 598              if (empty($out['s']) || $out['s'] === 'default') {
 599                  $rs = !empty($out['id']) ?
 600                      lookupByID($out['id']) :
 601                      lookupByDateTitle(isset($month) ? $month : '', $title);
 602              } else {
 603                  $rs = !empty($out['id']) ?
 604                      lookupByIDSection($out['id'], $out['s']) :
 605                      lookupByTitleSection($title, $out['s']);
 606              }
 607  
 608              $out['id'] = (!empty($rs['ID'])) ? $rs['ID'] : '';
 609              $out['s'] = (!empty($rs['Section'])) ? $rs['Section'] : '';
 610              $is_404 = $is_404 || (empty($out['s']) || empty($out['id']));
 611          }
 612  
 613          if (!empty($out['s']) && $out['s'] !== 'default') {
 614              if (!isset($txp_sections[$out['s']])) {
 615                  $out['s'] = '';
 616                  $is_404 = true;
 617              }
 618          }
 619      }
 620  
 621      // Existing category in messy or clean URL?
 622      if (!empty($out['c'])) {
 623          global $thiscategory;
 624  
 625          if (!($thiscategory = ckCat($out['context'], $out['c']))) {
 626              $is_404 = true;
 627              $out['c'] = '';
 628              $thiscategory = null;
 629          } else {
 630              $thiscategory += array('is_first' => true, 'is_last' => true, 'section' => $out['s']);
 631          }
 632      }
 633  
 634      // Stats: found or not.
 635      $out['status'] = ($is_404 ? '404' : '200');
 636      $out['pg'] = is_numeric($out['pg']) ? intval($out['pg']) : '';
 637      $out['id'] = is_numeric($out['id']) ? intval($out['id']) : '';
 638      $id = $out['id'];
 639  
 640      if (!$is_404) {
 641          $out['s'] = empty($out['s']) ? 'default' : $out['s'];
 642      }
 643  
 644      // Hackish.
 645      global $is_article_list;
 646  
 647      if (empty($id)) {
 648          $is_article_list = true;
 649      }
 650  
 651      if (!$is_404 && $id && $out['s'] !== 'file_download') {
 652          if (empty($thisarticle)) {
 653              $a = safe_row(
 654                  "*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod",
 655                  'textpattern',
 656                  "ID = $id".(gps('txpreview') ? '' : " AND Status IN (".STATUS_LIVE.",".STATUS_STICKY.")")
 657              );
 658  
 659              if ($a) {
 660                  populateArticleData($a);
 661              }
 662          } elseif (!gps('txpreview') && !in_array($thisarticle['status'], array(STATUS_LIVE, STATUS_STICKY))) {
 663              unset($thisarticle);
 664          }
 665  
 666          if (!empty($thisarticle)) {
 667              unset($thiscategory);
 668              $uExpires = $thisarticle['expires'];
 669              $out['id_keywords'] = $thisarticle['keywords'];
 670              $out['id_author']   = $thisarticle['authorid'];
 671  
 672              if (!$publish_expired_articles && $uExpires && time() > $uExpires) {
 673                  $out['status'] = '410';
 674              }
 675          } else {
 676              $is_404 = true;
 677          }
 678      }
 679  
 680      // By this point we should know the section, so grab its page and CSS.
 681      // Logged-in users with enough privs use the skin they're currently editing.
 682      if (txpinterface != 'css') {
 683          $userInfo = is_logged_in();
 684  
 685          if ($userInfo && has_privs('skin.preview', $userInfo)) {
 686              foreach ($txp_sections as &$rs) {
 687                  empty($rs['dev_skin']) or $rs['skin'] = $rs['dev_skin'];
 688                  empty($rs['dev_page']) or $rs['page'] = $rs['dev_page'];
 689                  empty($rs['dev_css']) or $rs['css'] = $rs['dev_css'];
 690              }
 691  
 692              unset($rs);
 693          }
 694  
 695          $s = empty($out['s']) || $is_404 || !isset($txp_sections[$out['s']]) ? 'default' : $out['s'];
 696          $rs = $txp_sections[$s];
 697  
 698          $out['skin'] = isset($rs['skin']) ? $rs['skin'] : '';
 699          $out['page'] = isset($rs['page']) ? $rs['page'] : '';
 700          $out['css'] = isset($rs['css']) ? $rs['css'] : '';
 701      }
 702  
 703      // These are deprecated as of Textpattern v1.0 - leaving them here for
 704      // plugin compatibility.
 705      $out['path_from_root'] = rhu;
 706      $out['pfr']            = rhu;
 707  
 708      $out['path_to_site']   = $path_to_site;
 709      $out['permlink_mode']  = $permlink_mode;
 710      $out['sitename']       = $sitename;
 711  
 712      return $out;
 713  }
 714  
 715  //    textpattern() is the function that assembles a page, based on
 716  //    the variables passed to it by pretext();
 717  
 718  // -------------------------------------------------------------
 719  
 720  function textpattern()
 721  {
 722      global $pretext, $production_status, $has_article_tag;
 723  
 724      $has_article_tag = false;
 725  
 726      callback_event('textpattern');
 727  
 728      if ($pretext['status'] == '404') {
 729          txp_die(gTxt('404_not_found'), '404');
 730      }
 731  
 732      if ($pretext['status'] == '410') {
 733          txp_die(gTxt('410_gone'), '410');
 734      }
 735  
 736      // Useful for clean URLs with error-handlers.
 737      txp_status_header('200 OK');
 738  
 739      set_error_handler('tagErrorHandler');
 740      $html = parse_page($pretext['page'], $pretext['skin']);
 741  
 742      if ($html === false) {
 743          txp_die(gTxt('unknown_section'), '404');
 744      }
 745  
 746      // Make sure the page has an article tag if necessary.
 747      if (!$has_article_tag && $production_status != 'live' && $pretext['context'] == 'article' && (!empty($pretext['id']) || !empty($pretext['c']) || !empty($pretext['q']) || !empty($pretext['pg']))) {
 748          trigger_error(gTxt('missing_article_tag', array('{page}' => $pretext['page'])));
 749      }
 750  
 751      restore_error_handler();
 752      set_headers();
 753      echo ltrim($html);
 754  
 755      callback_event('textpattern_end');
 756  }
 757  
 758  // -------------------------------------------------------------
 759  function output_component($n = '')
 760  {
 761      global $pretext;
 762      static $mimetypes = null, $typequery = null;
 763  
 764      if (!isset($mimetypes)) {
 765          $null = null;
 766          $mimetypes = Txp::get('Textpattern\Skin\Form')->getMimeTypes();
 767          $typequery = " AND type IN ('".implode("','", doSlash(array_keys($mimetypes)))."')";
 768      }
 769  
 770      if (!$n || !is_scalar($n) || empty($mimetypes)) {
 771          return;
 772      }
 773  
 774      $t = $pretext['skin'];
 775      $skinquery = $t ? " AND skin='".doSlash($t)."'" : '';
 776  
 777      $n = do_list_unique(doSlash($n));
 778      $name = join("','", $n);
 779      $order = count($n) > 1 ? " ORDER BY FIELD(name, '$name')" : '';
 780      $mimetype = null;
 781      $assets = array();
 782  
 783      if (!empty($name) && $rs = safe_rows('Form, type', 'txp_form', "name IN ('$name')".$typequery.$skinquery.$order)) {
 784          foreach ($rs as $row) {
 785              if (!isset($mimetype) || $mimetypes[$row['type']] == $mimetype) {
 786                  $assets[] = $row['Form'];
 787                  $mimetype = $mimetypes[$row['type']];
 788              }
 789          }
 790  
 791          set_error_handler('tagErrorHandler');
 792          @header('Content-Type: '.$mimetype.'; charset=utf-8');
 793          echo ltrim(parse_page(null, null, implode(n, $assets)));
 794          restore_error_handler();
 795      }
 796  }
 797  
 798  // -------------------------------------------------------------
 799  function output_css($s = '', $n = '', $t = '')
 800  {
 801      $order = '';
 802  
 803      if ($n) {
 804          if (!is_array($n)) {
 805              $n = do_list_unique($n);
 806          }
 807  
 808          $cssname = join("','", doSlash($n));
 809  
 810          if (count($n) > 1) {
 811              $order = " ORDER BY FIELD(name, '$cssname')";
 812          }
 813      } elseif ($s && $res = safe_row('css, skin', 'txp_section', "name='".doSlash($s)."'")) {
 814          $cssname = $res['css'];
 815          $t or $t = $res['skin'];
 816      }
 817  
 818      if (!empty($cssname)) {
 819          $skinquery = $t ? " AND skin='".doSlash($t)."'" : '';
 820          $css = join(n, safe_column_num('css', 'txp_css', "name IN ('$cssname')".$skinquery.$order));
 821          set_error_handler('tagErrorHandler');
 822          @header('Content-Type: text/css; charset=utf-8');
 823          echo $css;
 824          restore_error_handler();
 825      }
 826  }
 827  
 828  // -------------------------------------------------------------
 829  function output_file_download($filename)
 830  {
 831      global $file_error, $file_base_path, $pretext;
 832  
 833      set_headers(array(
 834          'last-modified' => false,
 835          'etag' => false
 836      ), true);
 837  
 838      callback_event('file_download');
 839  
 840      if (!isset($file_error)) {
 841          $filename = sanitizeForFile($filename);
 842          $fullpath = build_file_path($file_base_path, $filename);
 843  
 844          if (is_file($fullpath)) {
 845              // Discard any error PHP messages.
 846              ob_clean();
 847              $filesize = filesize($fullpath);
 848              $sent = 0;
 849  
 850              set_headers(array(
 851                  'content-type' => 'application/octet-stream',
 852                  'content-disposition' => 'attachment; filename="'.$filename.'"',
 853                  'content-length' => $filesize,
 854                  // Fix for IE6 PDF bug on servers configured to send cache headers.
 855                  'cache-control' => 'private'
 856              ));
 857  
 858              @ini_set("zlib.output_compression", "Off");
 859              @set_time_limit(0);
 860              @ignore_user_abort(true);
 861  
 862              if ($file = fopen($fullpath, 'rb')) {
 863                  while (!feof($file) and (connection_status() == 0)) {
 864                      echo fread($file, 1024 * 64);
 865                      $sent += (1024 * 64);
 866                      ob_flush();
 867                      flush();
 868                  }
 869  
 870                  fclose($file);
 871  
 872                  // Record download.
 873                  if ((connection_status() == 0) and !connection_aborted()) {
 874                      safe_update('txp_file', "downloads = downloads + 1", "id = ".intval($pretext['id']));
 875                  } else {
 876                      $pretext['request_uri'] .= ($sent >= $filesize)
 877                          ? '#aborted'
 878                          : "#aborted-at-".floor($sent * 100 / $filesize)."%";
 879                  }
 880  
 881                  log_hit('200');
 882              }
 883          } else {
 884              $file_error = 404;
 885          }
 886      }
 887  
 888      // Deal with error.
 889      if (isset($file_error)) {
 890          switch ($file_error) {
 891              case 403:
 892                  txp_die(gTxt('403_forbidden'), '403');
 893                  break;
 894              case 404:
 895                  txp_die(gTxt('404_not_found'), '404');
 896                  break;
 897              default:
 898                  txp_die(gTxt('500_internal_server_error'), '500');
 899                  break;
 900          }
 901      }
 902  }
 903  
 904  // article() is called when parse() finds a <txp:article /> tag.
 905  // If an $id has been established, we output a single article,
 906  // otherwise, output a list.
 907  
 908  // -------------------------------------------------------------
 909  function article($atts, $thing = null)
 910  {
 911      global $is_article_body, $has_article_tag;
 912  
 913      if ($is_article_body) {
 914          trigger_error(gTxt('article_tag_illegal_body'));
 915  
 916          return '';
 917      }
 918  
 919      $has_article_tag = true;
 920  
 921      return parseArticles($atts, '0', $thing);
 922  }
 923  
 924  // -------------------------------------------------------------
 925  
 926  function doArticles($atts, $iscustom, $thing = null)
 927  {
 928      global $pretext, $thispage, $trace, $txp_item, $txp_sections;
 929      static $date_fields = array('posted' => 'Posted', 'modified' => 'LastMod', 'expires' => 'Expires');
 930  
 931      extract($pretext);
 932  
 933      if ($iscustom) {
 934          // Custom articles must not render search results.
 935          $q = '';
 936      }
 937  
 938      // Getting attributes.
 939      if (isset($thing) && !isset($atts['form'])) {
 940          $atts['form'] = '';
 941      }
 942  
 943      $theAtts = filterAtts($atts, $iscustom);
 944      extract($theAtts);
 945      $issticky = $theAtts['status'] == STATUS_STICKY;
 946  
 947      $pg or $pg = 1;
 948      $custom_pg = $pgonly && $pgonly !== true && !is_numeric($pgonly);
 949      $pgby = intval(empty($pageby) || $pageby === true ? ($custom_pg ? 1 : $limit) : $pageby);
 950  
 951      if ($offset === true || !$iscustom && !$issticky) {
 952          $offset = $offset === true ? 0 : intval($offset);
 953          $pgoffset = ($pg - 1) * $pgby + $offset;
 954      } else {
 955          $pgoffset = $offset = intval($offset);
 956      }
 957  
 958      if (isset($fields)) {
 959          $what = $groupby = $sortby = array();
 960          $column_map = $date_fields + article_column_map();
 961          $reg_fields = implode('|', array_keys($column_map));
 962  
 963          foreach (do_list_unique(strtolower($fields)) as $field) {
 964              if (preg_match("/^(?:(avg|max|min|sum)\s*\(\s*)?($reg_fields)(?:\s*\))?$/", $field, $matches)) {
 965                  $field = $matches[2];
 966                  $column = $column_map[$field];
 967                  $sortby[$field] = $column;
 968  
 969                  if (!empty($matches[1])) {
 970                      $alias = ' AS '.$column;
 971                      $what[$field] = strtoupper($matches[1]).'('.$column.')';
 972                  } else {
 973                      $alias = '';
 974                      $what[$field] = $column;
 975                      !is_array($groupby) or $groupby[$field] = $column;
 976                  }
 977  
 978                  if (isset($date_fields[$field])) {
 979                      $what[$field] .= $alias.', UNIX_TIMESTAMP('.$what[$field].') AS u'.$column;
 980                  } elseif ($alias) {
 981                      $what[$field] .= $alias;
 982                  } elseif ($field === 'thisid') {
 983                      $groupby = false;
 984                  }
 985              }
 986          }
 987  
 988          $fields = implode(', ', $what);
 989          $groupby = $groupby ? implode(', ', $groupby) : '';
 990  
 991          if ($groupby && !$sort) {
 992              $sort = implode(', ', $sortby);
 993          }
 994      } elseif ($custom_pg) {
 995          $groupby = trim($pgonly);
 996      }
 997  
 998      // Give control to search, if necessary.
 999      if ($q && !$issticky) {
1000          $s_filter = $searchall ? filterFrontPage('Section', 'searchable') : (empty($s) || $s == 'default' ? filterFrontPage() : '');
1001          $q = trim($q);
1002          $quoted = ($q[0] === '"') && ($q[strlen($q) - 1] === '"');
1003          $q = doSlash($quoted ? trim(trim($q, '"')) : $q);
1004  
1005          // Searchable article fields are limited to the columns of the
1006          // textpattern table and a matching fulltext index must exist.
1007          $cols = do_list_unique(get_pref('searchable_article_fields'));
1008  
1009          if (empty($cols) or $cols[0] == '') {
1010              $cols = array('Title', 'Body');
1011          }
1012  
1013          $search_terms = preg_replace('/\s+/', ' ', str_replace(array('\\', '%', '_', '\''), array('\\\\', '\\%', '\\_', '\\\''), $q));
1014          $score = ", MATCH (`".join("`, `", $cols)."`) AGAINST ('$q') AS score";
1015  
1016          if ($quoted || empty($m) || $m === 'exact') {
1017              for ($i = 0; $i < count($cols); $i++) {
1018                  $cols[$i] = "`$cols[$i]` LIKE '%$search_terms%'";
1019              }
1020          } else {
1021              $colJoin = ($m === 'any') ? "OR" : "AND";
1022              $search_terms = explode(' ', $search_terms);
1023              for ($i = 0; $i < count($cols); $i++) {
1024                  $like = array();
1025                  foreach ($search_terms as $search_term) {
1026                      $like[] = "`$cols[$i]` LIKE '%$search_term%'";
1027                  }
1028                  $cols[$i] = "(".join(" $colJoin ", $like).")";
1029              }
1030          }
1031  
1032          $cols = join(" OR ", $cols);
1033          $search = " AND ($cols) $s_filter";
1034          $fname = $searchform ? $searchform : (isset($thing) ? '' : 'search_results');
1035  
1036          if (!$sort) {
1037              $sort = 'score DESC';
1038          }
1039      } else {
1040          $search = $score = '';
1041          $fname = (!empty($listform) ? $listform : $form);
1042  
1043          if (!$sort) {
1044              $sort = "Posted DESC";
1045          }
1046      }
1047  
1048      $where = $theAtts['*'].$search;
1049      !empty($fields) or $fields = '*';
1050  
1051      // Do not paginate if we are on a custom list.
1052      if (!$iscustom && !$issticky) {
1053          if ($pageby === true || empty($thispage) && (!isset($pageby) || $pageby)) {
1054              $grand_total = getCount(array('textpattern', !empty($groupby) ? "DISTINCT $groupby" : '*'), $where);
1055              $total = $grand_total - $offset;
1056              $numPages = $pgby ? ceil($total / $pgby) : 1;
1057              $trace->log("[Found: $total articles, $numPages pages]");
1058  
1059              // Send paging info to txp:newer and txp:older.
1060              $thispage = array(
1061                  'pg'          => $pg,
1062                  'numPages'    => $numPages,
1063                  's'           => $s,
1064                  'c'           => $c,
1065                  'context'     => 'article',
1066                  'grand_total' => $grand_total,
1067                  'total'       => $total
1068              );
1069          }
1070  
1071          if ($pgonly) {
1072              return;
1073          }
1074      } elseif ($pgonly) {
1075          $total = getCount(array('textpattern', !empty($groupby) ? "DISTINCT $groupby" : '*'), $where);
1076          $total -= $offset;
1077  
1078          return $pgby ? ceil($total / $pgby) : $total;
1079      }
1080  
1081      // Preserve order of custom article ids unless 'sort' attribute is set.
1082      if (!empty($id) && empty($atts['sort']) && empty($groupby)) {
1083          $safe_sort = "FIELD(ID, ".$id."), ".$sort;
1084      } else {
1085          $safe_sort = $sort;
1086      }
1087  
1088      $fields !== '*' or $fields = null;
1089  
1090      if ($fields && !empty($groupby)) {
1091          $where .= " GROUP BY $groupby";
1092          $fields .= ', COUNT(*) AS count';
1093          $score = '';
1094      }
1095  
1096      $rs = safe_rows_start(
1097          ($fields ? $fields : "*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod").$score,
1098          'textpattern',
1099          "$where ORDER BY $safe_sort LIMIT ".intval($pgoffset).", ".intval($limit)
1100      );
1101  
1102      if ($rs && $last = numRows($rs)) {
1103          $count = 0;
1104          $articles = array();
1105          $chunk = false;
1106          $old_item = $txp_item;
1107          $txp_item['total'] = $last;
1108          unset($txp_item['breakby']);
1109          $groupby = !$breakby || is_numeric(strtr($breakby, ' ,', '00')) ?
1110              false :
1111              (preg_match('@<(?:'.TXP_PATTERN.'):@', $breakby) ? 1 : 2);
1112  
1113          while ($count++ <= $last) {
1114              global $thisarticle;
1115  
1116              if ($a = nextRow($rs)) {
1117                  populateArticleData($a);
1118                  $thisarticle['is_first'] = ($count == 1);
1119                  $thisarticle['is_last'] = ($count == $last);
1120                  $txp_item['count'] = isset($a['count']) ? $a['count'] : $count;
1121  
1122                  $newbreak = !$groupby ? $count :
1123                      ($groupby === 1 ?
1124                          parse($breakby, true, false) :
1125                          parse_form($breakby)
1126                      );
1127              } else {
1128                  $newbreak = null;
1129              }
1130  
1131              if (isset($txp_item['breakby']) && $newbreak !== $txp_item['breakby']) {
1132                  if ($breakform) {
1133                      $tmparticle = $thisarticle;
1134                      $thisarticle = $oldarticle;
1135                      $newform = parse_form($breakform);
1136                      $chunk = str_replace('<+>', $chunk, $newform);
1137                      $thisarticle = $tmparticle;
1138                  }
1139  
1140                  $chunk === false or $articles[] = $chunk;
1141                  $chunk = false;
1142              }
1143  
1144              if ($count <= $last) {
1145                  $item = false;
1146  
1147                  if ($allowoverride && !empty($a['override_form'])) {
1148                      $item = parse_form($a['override_form'], $txp_sections[$a['Section']]['skin']);
1149                  } elseif ($fname) {
1150                      $item = parse_form($fname);
1151                  }
1152  
1153                  if ($item !== false) {
1154                      $item = txp_sandbox(array(), $item, false);
1155                  } elseif (isset($thing)) {
1156                      $item = txp_sandbox(array(), $thing);
1157                  }
1158  
1159                  $item === false or $chunk .= $item;
1160              }
1161  
1162              $oldarticle = $thisarticle;
1163              $txp_item['breakby'] = $newbreak;
1164              unset($GLOBALS['thisarticle']);
1165          }
1166  
1167          if ($groupby) {
1168              $breakby = '';
1169          }
1170  
1171          $txp_item = $old_item;
1172      }
1173  
1174      return !empty($articles) ?
1175          doLabel($label, $labeltag).doWrap($articles, $wraptag, compact('break', 'breakby', 'breakclass', 'class')) :
1176          ($thing ? parse($thing, false) : '');
1177  }
1178  
1179  // -------------------------------------------------------------
1180  
1181  function doArticle($atts, $thing = null)
1182  {
1183      global $pretext, $thisarticle;
1184  
1185      if (isset($thing) && !isset($atts['form'])) {
1186          $atts['form'] = '';
1187      }
1188  
1189      $oldAtts = filterAtts();
1190      $atts = filterAtts($atts);
1191      extract($atts);
1192  
1193      // No output required, only setting atts.
1194      if ($pgonly) {
1195          return '';
1196      }
1197  
1198      if (empty($thisarticle) || $thisarticle['thisid'] != $pretext['id']) {
1199          $id = assert_int($pretext['id']);
1200          $thisarticle = null;
1201          $where = $atts['*'];
1202  
1203          $rs = safe_row(
1204              "*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod",
1205              'textpattern',
1206              "ID = $id AND $where LIMIT 1"
1207          );
1208  
1209          if ($rs) {
1210              populateArticleData($rs);
1211          }
1212      }
1213  
1214      if (!empty($thisarticle) && (in_list($thisarticle['status'], $status) || gps('txpreview'))) {
1215          extract($thisarticle);
1216          $thisarticle['is_first'] = $thisarticle['is_last'] = 1;
1217          $article = false;
1218  
1219          if ($allowoverride && $override_form) {
1220              $article = parse_form($override_form);
1221          } elseif ($form) {
1222              $article = parse_form($form);
1223          }
1224  
1225          if (isset($thing) && $article === false) {
1226              $article = parse($thing);
1227          }
1228  
1229          if ($article !== false && get_pref('use_comments') && get_pref('comments_auto_append')) {
1230              $article .= parse_form('comments_display');
1231          }
1232  
1233          unset($GLOBALS['thisarticle']);
1234      } else {
1235          // Restore atts to the previous article filter criteria.
1236          filterAtts($oldAtts ? $oldAtts : false);
1237      }
1238  
1239      return $article !== false ? $article : ($thing ? parse($thing, false) : '');
1240  }
1241  
1242  // -------------------------------------------------------------
1243  
1244  function article_custom($atts, $thing = null)
1245  {
1246      return parseArticles($atts, '1', $thing);
1247  }
1248  
1249  // -------------------------------------------------------------
1250  
1251  function parseArticles($atts, $iscustom = 0, $thing = null)
1252  {
1253      global $pretext, $is_article_list;
1254      $old_ial = $is_article_list;
1255      $is_article_list = empty($pretext['id']) || $iscustom;
1256      article_push();
1257      $r = ($is_article_list) ? doArticles($atts, $iscustom, $thing) : doArticle($atts, $thing);
1258      article_pop();
1259      $is_article_list = $old_ial;
1260  
1261      return $r;
1262  }
1263  
1264  // -------------------------------------------------------------
1265  
1266  function makeOut()
1267  {
1268      $array['status'] = '200';
1269  
1270      foreach (func_get_args() as $a) {
1271          $in = gps($a);
1272  
1273          if (is_scalar($in)) {
1274              $array[$a] = strval($in);
1275          } else {
1276              $array[$a] = '';
1277              $array['status'] = '404';
1278          }
1279      }
1280  
1281      return $array;
1282  }
1283  
1284  // -------------------------------------------------------------
1285  
1286  function validContext($context)
1287  {
1288      static $valid = null;
1289  
1290      if (empty($valid)) {
1291          foreach (array('article', 'image', 'file', 'link') as $type) {
1292              $valid[gTxt($type.'_context')] = $type;
1293              $valid[$type] = $type;
1294          }
1295      }
1296  
1297      return isset($valid[$context]) ? $valid[$context] : 'article';
1298  }
1299  
1300  /**
1301   * Chops a request string into URL-decoded path parts.
1302   *
1303   * @param   string $req Request string
1304   * @return  array
1305   * @package URL
1306   */
1307  
1308  function chopUrl($req, $min = 4)
1309  {
1310      $req = strtok($req, '?');
1311      $req = preg_replace('/index\.php$/i', '', $req);
1312      $r = array_map('urldecode', explode('/', strtolower($req)));
1313      $n = isset($min) ? max($min, count($r)) : count($r);
1314      $o = array('u0' => $req);
1315  
1316      for ($i = 1; $i < $n; $i++) {
1317          $o['u'.$i] = (isset($r[$i])) ? $r[$i] : null;
1318      }
1319  
1320      return $o;
1321  }

title

Description

title

Description

title

Description

title

title

Body