Gorilla.php
author Dan
Sun, 31 May 2009 00:38:09 -0400
changeset 1 f2ceea4fabe8
parent 0 cac93de16379
child 2 b6178b40aa09
permissions -rw-r--r--
Added support for replying and fixed a few security holes. TODO: search integration

<?php
/**!info**
{
  "Plugin Name"  : "Gorilla Paste",
  "Plugin URI"   : "http://enanocms.org/plugin/gorilla",
  "Description"  : "For The Toughest Pasting Jobs On Earth.&trade; The pastebin, Enano style. <a href=\"http://enanocms.org/plugin/geshi\" onclick=\"window.open(this.href); return false;\">GeSHi plugin</a> highly recommended.",
  "Author"       : "Dan Fuhry",
  "Version"      : "0.1.1",
  "Author URI"   : "http://enanocms.org/",
  "Version list" : ['0.1', '0.1.1']
}
**!*/

// Register namespace and ACLs
$plugins->attachHook('acl_rule_init', 'gorilla_setupcore($this, $session);');
// Add our special page
$plugins->attachHook('session_started', 'register_special_page(\'NewPaste\', \'gorilla_page_create\', true);');

// constants
define('PASTE_PRIVATE', 1);

function gorilla_setupcore(&$paths, &$session)
{
  // register our paste namespace
  $nssep = substr($paths->nslist['Special'], -1);
  $paths->create_namespace('Paste', 'Paste' . $nssep);
  
  // create our ACLs
  /**
   * @param string $acl_type An identifier for this field
   * @param int $default_perm Whether permission should be granted or not if it's not specified in the ACLs.
   * @param string $desc A human readable name for the permission type
   * @param array $deps The list of dependencies - this should be an array of ACL types
   * @param string $scope Which namespaces this field should apply to. This should be either a pipe-delimited list of namespace IDs or just "All".
   */
   
  $session->acl_extend_scope('read', 'Paste', $paths);
  $session->acl_extend_scope('post_comments', 'Paste', $paths);
  $session->acl_extend_scope('edit_comments', 'Paste', $paths);
  $session->acl_extend_scope('mod_comments', 'Paste', $paths);
  $session->acl_extend_scope('create_page', 'Paste', $paths);
  $session->acl_extend_scope('mod_misc', 'Paste', $paths);
  
  $session->register_acl_type('delete_paste_own', AUTH_ALLOW, 'gorilla_acl_delete_paste_own', array(), 'Paste');
  $session->register_acl_type('delete_paste_others', AUTH_DISALLOW, 'gorilla_acl_delete_paste_others', array(), 'Paste');
}

// Our paste creation page
function page_Special_NewPaste()
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  global $lang, $output;
  
  $have_geshi = isset($GLOBALS['geshi_supported_formats']);
  $perms = $session->fetch_page_acl('0', 'Paste');
  $have_permission = $perms->get_permissions('create_page');
  
  if ( $paths->getParam(0) === 'ajaxsubmit' )
  {
    header('Content-type: text/plain');
    echo gorilla_process_post($have_geshi, $have_permission, true);
    return true;
  }
  
  $private = false;
  $highlight = isset($_COOKIE['g_highlight']) ? $_COOKIE['g_highlight'] : 'plaintext';
  $text = '';
  $title = '';
  $ttl = 3600;
  $copy_from = false;
  
  if ( preg_match('/^Copy=([0-9]+)$/', $paths->getParam(0), $match) )
  {
    $paste_id = intval($match[1]);
    $q = $db->sql_query('SELECT paste_flags, paste_language, paste_text, paste_title, paste_ttl FROM ' . table_prefix . "pastes WHERE paste_id = $paste_id;");
    if ( !$q )
      $db->_die();
    
    list($flags, $highlight, $text, $title, $ttl) = $db->fetchrow_num();
    $db->free_result();
    $private = $flags & PASTE_PRIVATE ? true : false;
    $copy_from = $paste_id;
    
    if ( $flags & PASTE_PRIVATE )
    {
      if ( @$_GET['hash'] !== gorilla_sign($paste_id, $text) )
      {
        die_friendly($lang->get('etc_access_denied_short'), '<p>' . $lang->get('gorilla_msg_wrong_hash') . '</p>');
      }
    }
  }
  
  $output->header();
  
  ?>
  <script type="text/javascript">
  var gorilla_have_permission = <?php echo $have_permission ? 'true' : 'false'; ?>;
  function gorilla_create_submit()
  {
    if ( !window.gorilla_have_permission && user_level < USER_LEVEL_MEMBER )
    {
      load_component('login');
      
      ajaxLogonInit(function(k, response)
        {
          window.gorilla_have_permission = true;
          document.forms['gorilla_create'].submit();
        }, USER_LEVEL_MEMBER);
      return false;
    }
    
    try
    {
      load_component(['jquery', 'jquery-ui']);
      $('#gorilla_submit_result').empty().hide();
      
      var whitey = whiteOutElement(document.forms['gorilla_create']);
      
      var parent = parseInt($('#gorilla_parent').val());
      if ( isNaN(parent) )
        parent = 0;
      
      var json_packet = {
        highlight: $('#gorilla_highlight').val(),
        text: $('#gorilla_create_text').val(),
        is_private: $('#gorilla_private:checked').val() ? true : false,
        nick: $('#gorilla_nick').val(),
        title: $('#gorilla_title').val(),
        ttl: parseInt($('.gorilla_ttl:checked').val()),
        parent: parent,
        hash: $('#gorilla_hash').val();
      };
      json_packet = ajaxEscape(toJSONString(json_packet));
      ajaxPost(makeUrlNS('Special', 'NewPaste/ajaxsubmit'), 'r=' + json_packet, function(ajax)
        {
          if ( ajax.readyState == 4 && ajax.status == 200 )
          {
            var failed = parseInt((String(ajax.responseText)).substr(0, 1));
            if ( failed == 1 )
              whiteOutReportFailure(whitey);
            else
              whiteOutReportSuccess(whitey);
              
            var response = (String(ajax.responseText)).substr(2);
            
            setTimeout(function()
              {
                window.scroll(0, 0);
                $('#gorilla_submit_result').html(response).show('blind', 150);
              }, 1250);
          }
        });
      return false;
    }
    catch(e)
    {}
    
    return true;
  }
  addOnloadHook(function()
    {
      load_component('expander');
    });
  </script>
  <div id="gorilla_submit_result">
  <?php
    echo substr(gorilla_process_post($have_geshi, $have_permission), 2);
  ?>
  </div>
  
  <form action="<?php echo makeUrlNS('Special', 'NewPaste'); ?>" method="post" name="gorilla_create" onsubmit="return gorilla_create_submit();">
  
    <?php
    if ( $copy_from )
    {
      echo '<p style="float: left;">' . $lang->get('gorilla_msg_copying_from', array('paste_id' => $copy_from, 'paste_url' => makeUrlNS('Paste', $copy_from, false, true))) . '</p>';
    }
    ?>
  
    <!-- private -->
    <div style="float: right; margin: 10px 1%;">
      <label title="<?php echo $lang->get('gorilla_lbl_private_hint'); ?>">
        <input type="checkbox" name="is_private" id="gorilla_private" <?php if ( $private ) echo 'checked="checked"'; ?> />
        <img alt="<?php echo $lang->get('gorilla_lbl_private'); ?>" src="<?php echo cdnPath; ?>/images/lock16.png" />
      </label>
    </div>
    
    <!-- highlighting -->
    <div style="float: right; margin: 10px;">
    
    <?php echo $lang->get('gorilla_lbl_highlight'); ?>
    <?php if ( $have_geshi ): ?>
      <select name="highlight" id="gorilla_highlight">
        <?php
        // print out options for each GeSHi format
        global $geshi_supported_formats;
        $formats = array_merge(array('plaintext'), $geshi_supported_formats);
        foreach ( $formats as $format )
        {
          // $string = str_replace('-', '_', "geshi_lang_$format");
          // if ( ($_ = $lang->get($string)) !== $string )
          //   $string = $_;
          // else
            $string = $format;
            
          $sel = ( $format == $highlight ) ? ' selected="selected"' : '';
          echo '<option value="' . $format . '"' . $sel . '>' . $string . '</option>' . "\n          ";
        }
        ?>
      </select>
    <?php else: ?>
      <span style="color: #808080;"><?php echo $lang->get('gorilla_msg_no_geshi'); ?></span>
      <input type="hidden" name="highlight_type" id="gorilla_highlight" value="plaintext" />
    <?php endif; ?>
                   
    </div>
  
    <!-- text box -->
    
    <textarea id="gorilla_create_text" name="text" rows="30" cols="80" style="width: 98%; display: block; margin: 10px auto; clear: both;"><?php echo htmlspecialchars($text); ?></textarea>
    
    <fieldset enano:expand="closed" style="margin-bottom: 10px;">
      <legend><?php echo $lang->get('gorilla_btn_advanced_options'); ?></legend>
      <div>
      
      <!-- title -->
      <p>
        <?php echo $lang->get('gorilla_lbl_title'); ?>
        <input type="text" name="title" id="gorilla_title" size="40" value="<?php echo htmlspecialchars($title); ?>" />
      </p>
      
      <!-- nick -->
      <p>
        <?php echo $lang->get('gorilla_lbl_nick'); ?>
        <?php
        if ( !$have_permission && !$session->user_logged_in )
        {
          echo '<em>' . $lang->get('gorilla_msg_using_login_nick') . '</em>';
          echo '<input type="hidden" name="nick" id="gorilla_nick" value="" />';
        }
        else if ( $session->user_logged_in )
        {
          $rankinfo = $session->get_user_rank($session->user_id);
          echo '<em>' . $lang->get('gorilla_msg_using_logged_in_nick') . '</em> ';
          echo '<span style="' . $rankinfo['rank_style'] . '">' . $session->username . '</span>';
          echo '<input type="hidden" name="nick" id="gorilla_nick" value="" />';
        }
        else
        {
          echo '<input type="text" name="nick" id="gorilla_nick" value="' . $lang->get('gorilla_nick_anonymous') . '" />';
        }
        ?>
      </p>
      
      <!-- ttl -->
      <p>
        <?php echo $lang->get('gorilla_lbl_ttl'); ?>
        <em>
        <label><input<?php if ( !in_array($ttl, array(0, 86400, 2592000)) ) echo ' checked="checked"'; ?> class="gorilla_ttl" type="radio" name="ttl" value="3600" /> <?php echo $lang->get('gorilla_lbl_ttl_hour'); ?></label>
        <label><input<?php if ( $ttl == 86400                             ) echo ' checked="checked"'; ?> class="gorilla_ttl" type="radio" name="ttl" value="86400" /> <?php echo $lang->get('gorilla_lbl_ttl_day'); ?></label>
        <label><input<?php if ( $ttl == 2592000                           ) echo ' checked="checked"'; ?> class="gorilla_ttl" type="radio" name="ttl" value="2592000" /> <?php echo $lang->get('gorilla_lbl_ttl_month'); ?></label>
        <label><input<?php if ( $ttl == 0                                 ) echo ' checked="checked"'; ?> class="gorilla_ttl" type="radio" name="ttl" value="0" /> <?php echo $lang->get('gorilla_lbl_ttl_forever'); ?></label>
        </em>
      </p>
      
      <!-- reply -->
      <p>
        <?php echo $lang->get('gorilla_lbl_reply'); ?><input id="gorilla_parent" name="parent" size="8" value="<?php echo $copy_from ? strval($copy_from) : ''; ?>" />
      </p>
      
      </div>
    </fieldset>
    
    <!-- hash -->
    <?php if ( $private && $copy_from ): ?>
    <input type="hidden" name="hash" id="gorilla_hash" value="<?php echo gorilla_sign($paste_id, $text); ?>" />
    <?php else: ?>
    <input type="hidden" name="hash" id="gorilla_hash" value="" />
    <?php endif; ?>
  
    <!-- login notice -->
  
    <?php if ( !$have_permission && !$session->user_logged_in ): ?>
    <div class="info-box-mini">
      <?php echo $lang->get('gorilla_msg_will_prompt_for_login'); ?>
    </div>
    <?php endif; ?>
    
    <!-- submit -->
  
    <input type="submit" style="font-size: x-large;" value="<?php echo $lang->get('gorilla_btn_submit'); ?>" />
  </form>
  <?php
  
  $output->footer();
}

// actual processing for submitted pastes
function gorilla_process_post($have_geshi, $have_permission, $is_ajax = false)
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  global $lang;
  
  $fields = array(
      'highlight' => 'string',
      'text' => 'string',
      'is_private' => 'boolean',
      'nick' => 'string',
      'title' => 'string',
      'ttl' => 'integer',
      'parent' => 'integer',
      'hash' => 'string'
    );
  
  $info = array();
  
  if ( $is_ajax )
  {
    try
    {
      $request = enano_json_decode(@$_POST['r']);
    }
    catch ( Exception $e )
    {
      return '1;No JSON request given';
    }
    foreach ( $fields as $field => $type )
    {
      if ( !isset($request[$field]) )
        return "1;Field \"$field\" not provided";
      if ( ($ftype = gettype($request[$field])) !== $type )
        return "1;Field \"$field\": expected $type, got $ftype";
      
      $info[$field] = $request[$field];
    }
  }
  else
  {
    foreach ( $fields as $field => $type )
    {
      if ( !isset($_POST[$field]) && $field != 'is_private' )
        return '';
    }
    $info = array(
        'highlight' => $_POST['highlight'],
        'text' => $_POST['text'],
        'is_private' => isset($_POST['is_private']),
        'nick' => $_POST['nick'],
        'title' => $_POST['title'],
        'ttl' => intval($_POST['ttl']),
        'parent' => intval($_POST['parent']),
        'hash' => $_POST['hash']
      );
  }
  
  if ( $info['parent'] )
  {
    // make sure we have the right hash
    $q = $db->sql_query('SELECT paste_text FROM ' . table_prefix . "pastes WHERE paste_id = {$info['parent']};");
    if ( !$q )
      $db->_die();
    
    if ( $db->numrows() > 0 )
    {
      list($old_text) = $db->fetchrow_num();
      if ( $info['hash'] !== gorilla_sign($info['parent'], $old_text) )
      {
        $info['parent'] = 0;
      }
    }
    else
    {
      $info['parent'] = 0;
    }
  }
  
  if ( !$have_permission )
  {
    return '1;<div class="error-box-mini">' . $lang->get('etc_access_denied') . '</div>';
  }
  
  // validate highlight scheme
  global $geshi_supported_formats;
  if ( is_array($geshi_supported_formats) )
  {
    if ( !in_array($info['highlight'], $geshi_supported_formats) )
      $info['highlight'] = 'plaintext';
  }
  else
  {
    $info['highlight'] = 'plaintext';
  }
  
  setcookie('g_highlight', $info['highlight'], time() + 365 * 24 * 60 * 60);
  
  $info_db = $info;
  foreach ( $info_db as &$item )
  {
    if ( is_string($item) )
      $item = "'" . $db->escape($item) . "'";
    else if ( is_bool($item) )
      $item = $item ? '1' : '0';
    else if ( is_int($item) )
      $item = strval($item);
    else
      $item = "''";
  }
  
  $now = time();
  $flags = 0;
  if ( $info['is_private'] )
    $flags |= PASTE_PRIVATE;
  
  $sql = 'INSERT INTO ' . table_prefix . "pastes( paste_title, paste_text, paste_author, paste_author_name, paste_author_ip, paste_language, paste_timestamp, paste_ttl, paste_flags, paste_parent ) VALUES\n"
  . "  ( {$info_db['title']}, {$info_db['text']}, $session->user_id, {$info_db['nick']}, '{$_SERVER['REMOTE_ADDR']}', {$info_db['highlight']}, $now, {$info_db['ttl']}, $flags, {$info_db['parent']} );";
       
  if ( !$db->sql_query($sql) )
    ( $is_ajax ) ? $db->die_json() : $db->_die();
  
  // avoid insert_id
  $q = $db->sql_query('SELECT paste_id FROM ' . table_prefix . "pastes WHERE paste_timestamp = $now ORDER BY paste_id DESC LIMIT 1;");
  if ( !$q )
    ( $is_ajax ) ? $db->die_json() : $db->_die();
  list($paste_id) = $db->fetchrow_num();
  $db->free_result();
  
  $params = false;
  if ( $flags & PASTE_PRIVATE )
    $params = 'hash=' . gorilla_sign($paste_id, $info['text']);
  
  $paste_url = makeUrlComplete('Paste', $paste_id, $params, true);
  
  return '0;'
           . '<div class="info-box-mini">' . $lang->get('gorilla_msg_created') . '</div>'
           . '<div style="font-size: larger; text-align: center; margin: 30px 0;">' . $lang->get('gorilla_msg_paste_url') . '<br /><input onfocus="this.select()" readonly="readonly" type="text" size="50" style="font-size: larger; text-align: center;" value="' . $paste_url . '" /></div>';
}

###############################################################################
## PASTE DISPLAY
###############################################################################

class Namespace_Paste extends Namespace_Default
{
  protected $paste_data = false;
  
  public function __construct($page_id, $namespace, $revid = 0)
  {
    $this->page_id = $page_id;
    $this->namespace = $namespace;
    $this->revision_id = 0;
    
    $this->build_cdata();
  }
  
  public function build_cdata()
  {
    global $db, $session, $paths, $template, $plugins; // Common objects
    global $lang;
  
    $this->exists = false;
    if ( ctype_digit($this->page_id) )
    {
      $q = $db->sql_query('SELECT p.*, u.username FROM ' . table_prefix . "pastes AS p\n"
                        . "  LEFT JOIN " . table_prefix . "users AS u\n"
                        . "    ON ( u.user_id = p.paste_author )\n"
                        . "  WHERE p.paste_id = $this->page_id;");
      if ( $db->numrows() > 0 )
      {
        $this->exists = true;
        $this->paste_data = $db->fetchrow();
      }
      $db->free_result();
    }
    if ( $this->exists )
    {
      $this->cdata = array(
        'name' => empty($this->paste_data['paste_title']) ? $lang->get('gorilla_untitled_paste') : $this->paste_data['paste_title'],
        'urlname' => $this->page_id,
        'namespace' => $this->namespace,
        'special' => 0,
        'visible' => 0,
        'comments_on' => 1,
        'protected' => 0,
        'delvotes' => 0,
        'delvote_ips' => '',
        'wiki_mode' => 2,
        'page_exists' => true,
        'page_format' => getConfig('default_page_format', 'wikitext')
      );
    }
    else
    {
      $this->cdata = array(
        'name' => $lang->get('gorilla_title_404'),
        'urlname' => $this->page_id,
        'namespace' => $this->namespace,
        'special' => 0,
        'visible' => 0,
        'comments_on' => 0,
        'protected' => 0,
        'delvotes' => 0,
        'delvote_ips' => '',
        'wiki_mode' => 2,
        'page_exists' => false,
        'page_format' => getConfig('default_page_format', 'wikitext')
      );
    }
    $this->cdata = Namespace_Default::bake_cdata($this->cdata);
  }
  
  public function send()
  {
    global $db, $session, $paths, $template, $plugins; // Common objects
    global $output, $lang;
    
    $plugins->attachHook('page_type_string_set', '$this->namespace_string = $lang->get(\'gorilla_template_ns_string\');');
    $template->add_header('<style type="text/css">
      .geshi_highlighted a {
        background-image: none !important;
        padding-right: 0 !important;
      }
      </style>
      ');
    
    if ( $this->exists )
    {
      gorilla_display_paste($this->paste_data);
    }
    else
    {
      $output->header();
      $this->error_404();
      $output->footer();
    }
  }
  
  public function error_404()
  {
    global $lang;
    echo '<p>' . $lang->get('gorilla_msg_paste_not_found', array('create_link' => makeUrlNS('Special', 'NewPaste'))) . '</p>';
  }
}

function gorilla_display_paste($data)
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  global $lang;
  global $output;
  
  extract($data);
  $perms = $session->fetch_page_acl($paste_id, 'Paste');
  
  $localhash = false;
  if ( $paste_flags & PASTE_PRIVATE )
  {
    $localhash = gorilla_sign($paste_id, $paste_text);
  }
  
  if ( $paste_flags & PASTE_PRIVATE || isset($_GET['delete']) )
  {
    if ( @$_GET['hash'] !== $localhash )
    {
      // allow viewing regardless if mod or admin
      if ( !($session->user_level >= USER_LEVEL_MOD && !isset($_GET['delete'])) )
      {
        die_friendly($lang->get('etc_access_denied_short'), '<p>' . $lang->get('gorilla_msg_wrong_hash') . '</p>');
      }
    }
  }
  
  if ( isset($_GET['format']) )
  {
    switch($_GET['format'])
    {
      case 'text':
      case 'plain':
        header('Content-type: text/plain');
        echo $paste_text;
        return true;
        break;
      case 'download':
        header('Content-type: text/plain');
        header('Content-disposition: attachment; filename="paste' . $paste_id . '.txt"');
        header('Content-length: ' . strlen($paste_text));
        echo $paste_text;
        return true;
        break;
    }
  }
  
  $output->header();
  
  $perm = $paste_author == $session->user_id ? 'delete_paste_own' : 'delete_paste_others';
  if ( isset($_GET['delete']) && !isset($_POST['cancel']) )
  {
    if ( isset($_POST['delete_confirm']) )
    {
      $q = $db->sql_query('DELETE FROM ' . table_prefix . "pastes WHERE paste_id = $paste_id;");
      if ( !$q )
        $db->_die();
      
      echo '<p>' . $lang->get('gorilla_msg_paste_deleted') . '</p>';
    }
    else
    {
      $submit_url = makeUrlNS('Paste', $paste_id, 'delete&hash=' . gorilla_sign($paste_id, $paste_text), true);
      ?>
      <form action="<?php echo $submit_url; ?>" method="post">
        <p><?php echo $lang->get('gorilla_msg_delete_confirm'); ?></p>
        <p>
          <input type="submit" value="<?php echo $lang->get('gorilla_btn_delete_confirm'); ?>" name="delete_confirm" style="font-weight: bold;" />
          <input type="submit" value="<?php echo $lang->get('etc_cancel'); ?>" name="cancel" />
        </p>
      </form>
      <?php
    }
    $output->footer();
    return true;
  }
  
  if ( $paste_author > 1 )
  {
    // logged-in user
    $rank_info = $session->get_user_rank($paste_author);
    $user_link = '<a href="' . makeUrlNS('User', $username, false, true) . '" style="' . $rank_info['rank_style'] . '">' . htmlspecialchars($username) . '</a>';
  }
  else
  {
    // anonymous
    $user_link = '<b>' . htmlspecialchars($paste_author_name) . '</b>';
  }
  $date = enano_date('D, j M Y H:i:s', $paste_timestamp);
  $pasteinfo = $lang->get('gorilla_msg_paste_info', array('user_link' => $user_link, 'date' => $date));
  
  echo '<div class="mdg-infobox" style="margin: 10px 0;">';
  echo '<div style="float: right;">
          ' . $lang->get('gorilla_msg_other_formats', array('plain_link' => makeUrlNS('Paste', $paste_id, 'format=text' .  ( $localhash ? "&hash=$localhash" : '' ), true), 'download_link' => makeUrlNS('Paste', $paste_id, 'format=download' .  ( $localhash ? "&hash=$localhash" : '' ), true))) . '
          /
          <a title="' . $lang->get('gorilla_tip_new_paste') . '" href="' . makeUrlNS('Special', 'NewPaste') . '">' . $lang->get('gorilla_btn_new_paste') . '</a>
          /
          <a title="' . $lang->get('gorilla_tip_copy_from_this') . '" href="' . makeUrlNS('Special', 'NewPaste/Copy=' . $paste_id, ( $paste_flags & PASTE_PRIVATE ? 'hash=' . gorilla_sign($paste_id, $paste_text) : false ), true) . '">' . $lang->get('gorilla_btn_copy_from_this') . '</a>';
          
  if ( $paste_parent )
  {
    // pull flags of parent
    $q = $db->sql_query('SELECT paste_text, paste_flags FROM ' . table_prefix . "pastes WHERE paste_id = $paste_parent;");
    if ( !$q )
      $db->_die();
    
    if ( $db->numrows() > 0 )
    {
      list($parent_text, $parent_flags) = $db->fetchrow_num();
      $parenthash = false;
      if ( $parent_flags & PASTE_PRIVATE )
      {
        $parenthash = gorilla_sign($paste_parent, $parent_text);
      }
      
      echo ' / ' . $lang->get('gorilla_msg_reply_to', array(
          'parent_link' => makeUrlNS('Paste', $paste_parent, ( $parenthash ? "hash=$parenthash" : '' ), true),
          'diff_link' => makeUrlNS('Paste', $paste_id, 'diff_parent' .  ( $localhash ? "&hash=$localhash" : '' ), true),
          'parent_id' => $paste_parent
        ));
    }
    $db->free_result($q);
  }
          
  if ( $perms->get_permissions($perm) && $session->user_logged_in )
  {
    echo ' / <a title="' . $lang->get('gorilla_tip_delete') . '" href="' . makeUrlNS('Paste', $paste_id, 'delete&hash=' . gorilla_sign($paste_id, $paste_text), true) . '">' . $lang->get('gorilla_btn_delete') . '</a>';
  }
  if ( $perms->get_permissions('mod_misc') )
  {
    echo ' / <span title="' . $lang->get('gorilla_tip_paste_ip') . '">' . $paste_author_ip . '</span>';
  }
  
  echo '</div>';
  echo $pasteinfo;
  echo '</div>';
  
  if ( isset($_GET['diff_parent']) && isset($parent_text) )
  {
    echo '<p>' . $lang->get('gorilla_btn_view_normal', array('orig_url' => makeUrlNS('Paste', $paste_id, ( $localhash ? "hash=$localhash" : '' ), true))) . '</p>';
    // convert to unix newlines to avoid confusing the diff engine (seen on Chromium on Linux)
    echo RenderMan::diff(str_replace("\r\n", "\n", $parent_text), str_replace("\r\n", "\n", $paste_text));
    $output->footer();
    return;
  }
  
  if ( preg_match('/^## /m', $paste_text) )
  {
    gorilla_show_text_multi($paste_text, $paste_language);
  }
  else
  {
    gorilla_show_text($paste_text, $paste_language);
  }
  
  $output->footer();
}

function gorilla_show_text($text, $lang)
{
  $have_geshi = isset($GLOBALS['geshi_supported_formats']);
  
  if ( $have_geshi )
  {
    if ( $lang == 'plaintext' )
      $lang = 'text';
    
    if ( !defined('GESHI_ROOT') )
    define('GESHI_ROOT', ENANO_ROOT . '/plugins/geshi/');
  
    require_once ( GESHI_ROOT . 'base.php' );
    
    $geshi = new GeSHi($text, $lang, null);
    $geshi->set_header_type(GESHI_HEADER_DIV);
    $geshi->enable_line_numbers(GESHI_FANCY_LINE_NUMBERS, 2);
    $geshi->set_overall_class('geshi_highlighted');
    $parsed = $geshi->parse_code();
    
    echo $parsed;
  }
  else
  {
    echo '<h3>FIXME: WRITE REAL PLAINTEXT FORMATTER</h3>';
    echo '<pre>' . htmlspecialchars($text) . '</pre>';
  }
}

function gorilla_show_text_multi($text, $lang)
{
  $sections = preg_split('/^## .*$/m', $text);
  $headingcount = preg_match_all('/^## (.+?)(?: \[([a-z_-]+)\])? *\r?$/m', $text, $matches);
  
  // if we have one heading less than the number of sections, print the first section
  while ( count($sections) > $headingcount )
  {
    gorilla_show_text(trim($sections[0], "\r\n"), $lang);
    
    unset($sections[0]);
    $sections = array_values($sections);
  }
  
  foreach ( $matches[0] as $i => $_ )
  {
    $clang = !empty($matches[2][$i]) ? $matches[2][$i] : $lang;
    echo '<h2>' . htmlspecialchars(trim($matches[1][$i])) . '</h2>';
    gorilla_show_text(trim($sections[$i], "\r\n"), $clang);
  }
}

function gorilla_sign($id, $text)
{
  return hmac_sha1($id, sha1($text));
}

// make sure pastes are pruned on a regular basis
function gorilla_prune_expired()
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  
  $now = time();
  $q = $db->sql_query('DELETE FROM ' . table_prefix . "pastes WHERE paste_timestamp + paste_ttl < $now AND paste_ttl > 0;");
}

register_cron_task('gorilla_prune_expired', 1);

/**!install dbms="mysql"; **
CREATE TABLE {{TABLE_PREFIX}}pastes(
  paste_id int(18) NOT NULL auto_increment,
  paste_title text DEFAULT NULL,
  paste_text text NOT NULL DEFAULT '',
  paste_author int(12) NOT NULL DEFAULT 1,
  paste_author_name varchar(255) NOT NULL DEFAULT 'Anonymous',
  paste_author_ip varchar(39) NOT NULL,
  paste_language varchar(32) NOT NULL DEFAULT 'plaintext',
  paste_timestamp int(12) NOT NULL DEFAULT 0,
  paste_ttl int(12) NOT NULL DEFAULT 86400,
  paste_flags int(8) NOT NULL DEFAULT 0,
  paste_parent int(18) NOT NULL DEFAULT 0,
  PRIMARY KEY ( paste_id )
) ENGINE=`MyISAM` CHARSET=`UTF8` COLLATE=`utf8_bin`;

**!*/

/**!upgrade from="0.1"; to="0.1.1"; dbms="mysql"; **
ALTER TABLE {{TABLE_PREFIX}}pastes ADD COLUMN paste_parent int(18) NOT NULL DEFAULT 0;
**!*/

/**!uninstall **
DROP TABLE {{TABLE_PREFIX}}pastes;
**!*/

/**!language**
<code>
{
  eng: {
    categories: ['meta', 'gorilla'],
    strings: {
      meta: {
        gorilla: 'Gorilla',
      },
      gorilla: {
        acl_delete_paste_own: 'Delete own pastes',
        acl_delete_paste_others: 'Delete others\' pastes',
        
        page_create: 'Create paste',
        msg_copying_from: 'Copying from <a href="%paste_url%">paste #%paste_id%</a>.',
        lbl_highlight: 'Language:',
        msg_no_geshi: 'Not supported',
        btn_advanced_options: 'Advanced options',
        lbl_private: 'Private paste',
        lbl_private_hint: 'Don\'t list this paste or allow it to be included in searches',
        lbl_title: 'Title:',
        lbl_nick: 'Nickname:',
        nick_anonymous: 'Anonymous',
        msg_using_login_nick: 'Using username provided during login',
        msg_using_logged_in_nick: 'Logged in; using nickname:',
        lbl_ttl: 'Keep it for:',
        lbl_ttl_hour: '1 hour',
        lbl_ttl_day: '1 day',
        lbl_ttl_month: '1 month',
        lbl_ttl_forever: 'forever',
        lbl_reply: 'Reply to paste: #',
        msg_will_prompt_for_login: 'You are not logged in. You will be asked to log in when you click the submit button below.',
        btn_submit: 'Paste it!',
        
        msg_created: 'Paste created.',
        msg_paste_url: 'Share this paste using the following URL:',
        
        untitled_paste: 'Untitled paste',
        title_404: 'Paste not found',
        msg_paste_not_found: 'This paste cannot be found or has been deleted. <a href="%create_link%">Create a new paste</a>',
        msg_wrong_hash: 'Either you are trying to view a private paste which requires a hash in the URL or you were linked to this page from an outside source and the CSRF protection kicked in.',
        
        msg_paste_info: 'By %user_link%, pasted on %date%',
        msg_other_formats: '<a title="View as plain text" href="%plain_link%" onclick="window.open(this.href, \'gorillaplaintext\', \'address=no,status=no,toolbar=no,menus=no,scroll=yes,width=640,height=480\'); return false;">raw</a> / <a title="Download paste" href="%download_link%">dl</a>',
        btn_new_paste: 'new',
        tip_new_paste: 'Create a new paste',
        btn_copy_from_this: 'cp',
        tip_copy_from_this: 'Create a new paste, copying this one into the form',
        btn_delete: 'rm',
        tip_delete: 'Delete this paste',
        tip_paste_ip: 'IP address of paste author',
        msg_reply_to: 'parent: <a href="%parent_link%">#%parent_id%</a> (<a href="%diff_link%">diff</a>)',
        btn_view_normal: '<b>Difference from parent</b> (<a href="%orig_url%">back to paste</a>)',
        template_ns_string: 'paste',
        
        msg_paste_deleted: 'Paste deleted.',
        msg_delete_confirm: 'Really delete this paste?',
        btn_delete_confirm: 'Delete',
      }
    }
  }
}
</code>
**!*/