# HG changeset patch # User Dan # Date 1243666333 14400 # Node ID cac93de16379573a0aa53e39bad9f07edaab1a05 First commit. Works great. TODO: add ability to reply to other pastes and provide some primitive threaded structure. diff -r 000000000000 -r cac93de16379 Gorilla.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Gorilla.php Sat May 30 02:52:13 2009 -0400 @@ -0,0 +1,749 @@ +GeSHi plugin highly recommended.", + "Author" : "Dan Fuhry", + "Version" : "0.1", + "Author URI" : "http://enanocms.org/" +} +**!*/ + +// 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; + } + + $output->header(); + + ?> + +
+ +
+ +
+ + ' . $lang->get('gorilla_msg_copying_from', array('paste_id' => $copy_from, 'paste_url' => makeUrlNS('Paste', $copy_from, false, true))) . '

'; + } + ?> + + +
+ +
+ + +
+ + get('gorilla_lbl_highlight'); ?> + + + + get('gorilla_msg_no_geshi'); ?> + + + +
+ + + + + +
+ get('gorilla_btn_advanced_options'); ?> +
+ + +

+ get('gorilla_lbl_title'); ?> + +

+ + +

+ get('gorilla_lbl_nick'); ?> + user_logged_in ) + { + echo '' . $lang->get('gorilla_msg_using_login_nick') . ''; + echo ''; + } + else if ( $session->user_logged_in ) + { + $rankinfo = $session->get_user_rank($session->user_id); + echo '' . $lang->get('gorilla_msg_using_logged_in_nick') . ' '; + echo '' . $session->username . ''; + echo ''; + } + else + { + echo ''; + } + ?> +

+ + +

+ get('gorilla_lbl_ttl'); ?> + + + + + + +

+ +
+
+ + + + user_logged_in ): ?> +
+ get('gorilla_msg_will_prompt_for_login'); ?> +
+ + + + + +
+ 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' + ); + + $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']) + ); + } + + if ( !$have_permission ) + { + return '1;
' . $lang->get('etc_access_denied') . '
'; + } + + // 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 ) 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 );"; + + 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=' . hmac_sha1($paste_id, sha1($info['text'])); + + $paste_url = makeUrlComplete('Paste', $paste_id, $params, true); + + return '0;' + . '
' . $lang->get('gorilla_msg_created') . '
' + . '
' . $lang->get('gorilla_msg_paste_url') . '
'; +} + +############################################################################### +## 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(' + '); + + if ( $this->exists ) + { + gorilla_display_paste($this->paste_data); + } + else + { + $output->header(); + $this->error_404(); + $output->footer(); + } + } + + public function error_404() + { + global $lang; + echo '

' . $lang->get('gorilla_msg_paste_not_found', array('create_link' => makeUrlNS('Special', 'NewPaste'))) . '

'; + } +} + +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'); + + 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; + } + } + + if ( $paste_flags & PASTE_PRIVATE || isset($_GET['delete']) ) + { + if ( @$_GET['hash'] !== hmac_sha1($paste_id, sha1($paste_text)) ) + { + die_friendly($lang->get('etc_access_denied_short'), '

' . $lang->get('gorilla_msg_wrong_hash') . '

'); + } + } + + $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 '

' . $lang->get('gorilla_msg_paste_deleted') . '

'; + } + else + { + $submit_url = makeUrlNS('Paste', $paste_id, 'delete&hash=' . hmac_sha1($paste_id, sha1($paste_text)), true); + ?> +
+

get('gorilla_msg_delete_confirm'); ?>

+

+ + +

+
+ footer(); + return true; + } + + if ( $paste_author > 1 ) + { + // logged-in user + $rank_info = $session->get_user_rank($paste_author); + $user_link = '' . htmlspecialchars($username) . ''; + } + else + { + // anonymous + $user_link = '' . htmlspecialchars($paste_author_name) . ''; + } + $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 '
'; + echo '
+ ' . $lang->get('gorilla_msg_other_formats', array('plain_link' => makeUrlNS('Paste', $paste_id, 'format=text', true), 'download_link' => makeUrlNS('Paste', $paste_id, 'format=download', true))) . ' + / + ' . $lang->get('gorilla_btn_new_paste') . ' + / + ' . $lang->get('gorilla_btn_copy_from_this') . ''; + + if ( $perms->get_permissions($perm) && $session->user_logged_in ) + { + echo ' / ' . $lang->get('gorilla_btn_delete') . ''; + } + if ( $perms->get_permissions('mod_misc') ) + { + echo ' / ' . $paste_author_ip . ''; + } + + echo '
'; + echo $pasteinfo; + echo '
'; + + 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 '

FIXME: WRITE REAL PLAINTEXT FORMATTER

'; + echo '
' . htmlspecialchars($text) . '
'; + } +} + +function gorilla_show_text_multi($text, $lang) +{ + $sections = preg_split('/^## .*$/m', $text); + $headingcount = preg_match_all('/^## (.+?)(?: \[([a-z_-]+)\])? *$/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 '

' . htmlspecialchars(trim($matches[1][$i])) . '

'; + gorilla_show_text(trim($sections[$i], "\r\n"), $clang); + } +} + +// 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, + PRIMARY KEY ( paste_id ) +) ENGINE=`MyISAM` CHARSET=`UTF8` COLLATE=`utf8_bin`; + +**!*/ + +/**!uninstall ** +DROP TABLE {{TABLE_PREFIX}}pastes; +**!*/ + +/**!language** + +{ + 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 paste #%paste_id%.', + 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', + 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. Create a new paste', + 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: 'raw / dl', + 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', + template_ns_string: 'paste', + + msg_paste_deleted: 'Paste deleted.', + msg_delete_confirm: 'Really delete this paste?', + btn_delete_confirm: 'Delete', + } + } + } +} + +**!*/