/** * Write an entry to the event log. * * @param string $message The message * @param string $severity The severity * @param string $type The type * @param string $module The module * @param mixed $data The data * @return object LogEntry The inserted LogEntry object */ public static function log($message, $severity = 'info', $type = 'default', $module = null, $data = null) { $module = self::get_module($module); $log = new LogEntry(array('message' => $message, 'severity' => $severity, 'module' => $module, 'type' => $type, 'data' => $data, 'ip' => Utils::get_ip())); $user = User::identify(); if ($user->loggedin) { $log->user_id = $user->id; } $log->insert(); return $log; }
/** * Get a 10-digit hex code that identifies the user submitting the comment * @param A post id to which the comment will be submitted * @param The IP address of the commenter * @return A 10-digit hex code **/ public function get_code($post_id, $ip = '') { if ( $ip == '' ) { $ip = Utils::get_ip(); } $code = substr( md5( $post_id . Options::get( 'GUID' ) . 'more salt' . $ip ), 0, 10 ); $code = Plugins::filter( 'comment_code', $code, $post_id, $ip ); return $code; }
/** * Commit $_SESSION data to the database for this user. * * @param string $session_id The PHP-generated session id * @param string $data Data from session stored as a string */ public static function write( $session_id, $data ) { $remote_address = Utils::get_ip(); // not always set, even by real browsers $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; // default to writing the data only if it's changed if ( self::$initial_data !== $data ) { $dowrite = true; } else { $dowrite = false; } // but let a plugin make the final decision. we may want to ignore search spiders, for instance $dowrite = Plugins::filter( 'session_write', $dowrite, $session_id, $data ); if ( $dowrite ) { // DB::update() checks if the record key exists, and inserts if not $record = array( 'ip' => self::get_subnet( $remote_address ), 'expires' => HabariDateTime::date_create()->int + self::$lifetime, 'ua' => $user_agent, 'data' => $data, ); DB::update( DB::table( 'sessions' ), $record, array( 'token' => $session_id ) ); } }
/** * Receive a Pingback via XMLRPC * @param array $params An array of XMLRPC parameters from the remote call * @return string The success state of the pingback */ public function xmlrpc_pingback__ping( $params ) { try { list( $source_uri, $target_uri )= $params; // This should really be done by an Habari core function $target_parse = InputFilter::parse_url( $target_uri ); $target_stub = $target_parse['path']; $base_url = Site::get_path( 'base', true ); if ( '/' != $base_url) { $target_stub = str_replace( $base_url, '', $target_stub ); } $target_stub = trim( $target_stub, '/' ); if ( strpos( $target_stub, '?' ) !== false ) { list( $target_stub, $query_string )= explode( '?', $target_stub ); } // Can this be used as a target? $target_slug = URL::parse( $target_stub )->named_arg_values['slug']; if ( $target_slug === false ) { throw new XMLRPCException( 33 ); } // Does the target exist? $target_post = Post::get( array( 'slug' => $target_slug ) ); if ( $target_post === false ) { throw new XMLRPCException( 32 ); } // Is comment allowed? if ( $target_post->info->comments_disabled ) { throw new XMLRPCException( 33 ); } // Is this Pingback already registered? if ( Comments::get( array( 'post_id' => $target_post->id, 'url' => $source_uri, 'type' => Comment::PINGBACK ) )->count() > 0 ) { throw new XMLRPCException( 48 ); } // Retrieve source contents try { $rr = new RemoteRequest( $source_uri ); $rr->execute(); if ( ! $rr->executed() ) { throw new XMLRPCException( 16 ); } $source_contents = $rr->get_response_body(); $headers = $rr->get_response_headers(); } catch ( XMLRPCException $e ) { // catch our special type of exception and re-throw it throw $e; } catch ( Exception $e ) { throw new XMLRPCException( -32300 ); } // Encoding is converted into internal encoding. // First, detect the source string's encoding $habari_encoding = strtoupper( MultiByte::hab_encoding() ); $source_encoding = 'Windows-1252'; // Is the charset in the headers? if ( isset( $headers['Content-Type'] ) && strpos( $headers['Content-Type'], 'charset' ) !== false ) { // This regex should be changed to meet the HTTP spec at some point if ( preg_match("/charset[\x09\x0A\x0C\x0D\x20]*=[\x09\x0A\x0C\x0D\x20]*('?)([A-Za-z0-9\-\_]+)\1/i", $headers['Content-Type'], $matches ) ) { $source_encoding = strtoupper( $matches[2] ); } } // Can we tell the charset from the stream itself? else if ( ( $enc = MultiByte::detect_bom_encoding( $source_contents ) ) !== false ) { $source_encoding = $enc; } // Is the charset in a meta tag? else if ( preg_match( "/<meta[^>]+charset[\x09\x0A\x0C\x0D\x20]*=[\x09\x0A\x0C\x0D\x20]*([\"']?)([A-Za-z0-9\-\_]+)\1/i", $source_contents, $matches ) ) { $source_encoding = strtoupper( $matches[2] ); if (in_array($source_encoding, array("UTF-16", "UTF-16BE", "UTF-16LE"))) { $source_encoding = "UTF-8"; } } // Then, convert the string $ret = MultiByte::convert_encoding( $source_contents, $habari_encoding, $source_encoding ); if ( $ret !== false ) { $source_contents = $ret; } // Find the page's title preg_match( '/<title>(.*)<\/title>/is', $source_contents, $matches ); $source_title = $matches[1]; // Find the reciprocal links and their context preg_match( '/<body[^>]*>(.+)<\/body>/is', $source_contents, $matches ); $source_contents_filtered = preg_replace( '/\s{2,}/is', ' ', strip_tags( $matches[1], '<a>' ) ); // Get rid of all the non-recriprocal links $ht = new HTMLTokenizer( trim( $source_contents_filtered ) ); $set = $ht->parse(); $all_links = $set->slice( 'a', array() ); $keep_links = $set->slice( 'a', array( 'href' => $target_uri ) ); $bad_links = array_diff( $all_links, $keep_links ); foreach( $bad_links as $link ) { $link->tokenize_replace( '' ); $set->replace_slice( $link ); } $source_contents_filtered = (string)$set; // Get the excerpt if ( !preg_match( '%.{0,100}?<a[^>]*?href\\s*=\\s*("|\'|)' . $target_uri . '\\1[^>]*?'.'>(.+?)</a>.{0,100}%s', $source_contents_filtered, $source_excerpt ) ) { throw new XMLRPCException( 17 ); } /** Sanitize Data */ $source_excerpt = '…' . InputFilter::filter( $source_excerpt[0] ) . '…'; $source_title = InputFilter::filter($source_title); $source_uri = InputFilter::filter($source_uri); /* Sanitize the URL */ if (!empty($source_uri)) { $parsed = InputFilter::parse_url( $source_uri ); if ( $parsed['is_relative'] ) { // guess if they meant to use an absolute link $parsed = InputFilter::parse_url( 'http://' . $source_uri ); if ( ! $parsed['is_error'] ) { $source_uri = InputFilter::glue_url( $parsed ); } else { // disallow relative URLs $source_uri = ''; } } if ( $parsed['is_pseudo'] || ( $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https' ) ) { // allow only http(s) URLs $source_uri = ''; } else { // reconstruct the URL from the error-tolerant parsing // http:moeffju.net/blog/ -> http://moeffju.net/blog/ $source_uri = InputFilter::glue_url( $parsed ); } } // Add a new pingback comment $pingback = new Comment( array( 'post_id' => $target_post->id, 'name' => $source_title, 'email' => '', 'url' => $source_uri, 'ip' => Utils::get_ip(), 'content' => $source_excerpt, 'status' => Comment::STATUS_UNAPPROVED, 'date' => HabariDateTime::date_create(), 'type' => Comment::PINGBACK, ) ); $pingback->insert(); // Respond to the Pingback return 'The pingback has been registered'; } catch ( XMLRPCException $e ) { $e->output_fault_xml(); } }
/** * Add a comment to the site * * @param mixed $post A Post object instance or Post object id * @param string $name The commenter's name * @param string $email The commenter's email address * @param string $url The commenter's website URL * @param string $content The comment content * @param array $extra An associative array of extra values that should be considered */ function add_comment($post, $name = null, $email = null, $url = null, $content = null, $extra = null) { if (is_numeric($post)) { $post = Post::get(array('id' => $post)); } if (!$post instanceof Post) { // Not sure what you're trying to pull here, but that's no good header('HTTP/1.1 403 Forbidden', true, 403); die; } // let's do some basic sanity checking on the submission if (Options::get('comments_require_id') == true && (empty($name) || empty($email))) { Session::error(_t('Both name and e-mail address must be provided.')); } if (empty($content)) { Session::error(_t('You did not provide any content for your comment!')); } if (Session::has_errors()) { // save whatever was provided in session data Session::add_to_set('comment', $name, 'name'); Session::add_to_set('comment', $email, 'email'); Session::add_to_set('comment', $url, 'url'); Session::add_to_set('comment', $content, 'content'); // now send them back to the form Utils::redirect($post->permalink . '#respond'); } if ($post->info->comments_disabled) { // comments are disabled, so let's just send // them back to the post's permalink Session::error(_t('Comments on this post are disabled!')); Utils::redirect($post->permalink); } /* Sanitize data */ foreach (array('name', 'url', 'email', 'content') as $k) { ${$k} = InputFilter::filter(${$k}); } // there should never be any HTML in the name, so do some extra filtering on it $name = strip_tags(html_entity_decode($name, ENT_QUOTES, 'UTF-8')); /* Sanitize the URL */ if (!empty($url)) { $parsed = InputFilter::parse_url($url); if ($parsed['is_relative']) { // guess if they meant to use an absolute link $parsed = InputFilter::parse_url('http://' . $url); if (!$parsed['is_error']) { $url = InputFilter::glue_url($parsed); } else { // disallow relative URLs $url = ''; } } if ($parsed['is_pseudo'] || $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https') { // allow only http(s) URLs $url = ''; } else { // reconstruct the URL from the error-tolerant parsing // http:moeffju.net/blog/ -> http://moeffju.net/blog/ $url = InputFilter::glue_url($parsed); } } if (preg_match('/^\\p{Z}*$/u', $content)) { Session::error(_t('Comment contains only whitespace/empty comment')); Utils::redirect($post->permalink); } /* Create comment object*/ $comment = new Comment(array('post_id' => $post->id, 'name' => $name, 'email' => $email, 'url' => $url, 'ip' => sprintf("%u", ip2long(Utils::get_ip())), 'content' => $content, 'status' => Comment::STATUS_UNAPPROVED, 'date' => HabariDateTime::date_create(), 'type' => Comment::COMMENT)); // Should this really be here or in a default filter? // In any case, we should let plugins modify the status after we set it here. $user = User::identify(); if ($user->loggedin && $comment->email == $user->email) { $comment->status = Comment::STATUS_APPROVED; } // Users need to have permission to add comments if (!$user->can('comment')) { Session::error(_t('You do not have permission to create comments.')); Utils::redirect($post->permalink); } // Allow themes to work with comment hooks Themes::create(); // Allow plugins to change comment data and add commentinfo based on plugin-added form fields Plugins::act('comment_accepted', $comment, $this->handler_vars, $extra); $spam_rating = 0; $spam_rating = Plugins::filter('spam_filter', $spam_rating, $comment, $this->handler_vars, $extra); if ($spam_rating >= Options::get('spam_percentage', 100)) { $comment->status = Comment::STATUS_SPAM; } $comment->insert(); $anchor = ''; // If the comment was saved if ($comment->id && $comment->status != Comment::STATUS_SPAM) { $anchor = '#comment-' . $comment->id; // store in the user's session that this comment is pending moderation if ($comment->status == Comment::STATUS_UNAPPROVED) { Session::notice(_t('Your comment is pending moderation.'), 'comment_' . $comment->id); } // if no cookie exists, we should set one // but only if the user provided some details $cookie_name = 'comment_' . Options::get('GUID'); // build the string we store for the cookie $cookie_content = implode('#', array($comment->name, $comment->email, $comment->url)); // if the user is not logged in and there is no cookie OR the cookie differs from the current set if (User::identify()->loggedin == false && (!isset($_COOKIE[$cookie_name]) || $_COOKIE[$cookie_name] != $cookie_content)) { // update the cookie setcookie($cookie_name, $cookie_content, time() + HabariDateTime::YEAR, Site::get_path('base', true)); } } // Return the commenter to the original page. Utils::redirect($post->permalink . $anchor); }
/** * Commit $_SESSION data to the database for this user. */ public static function write() { if (!isset(self::$session_id)) { self::create(); } $remote_address = Utils::get_ip(); // not always set, even by real browsers $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $dowrite = self::changed(); if (isset($_SESSION)) { // get the data from the ArrayObject $data = $_SESSION; } else { $dowrite = false; $data = array(); } // but let a plugin make the final decision. we may want to ignore search spiders, for instance $dowrite = Plugins::filter('session_write', $dowrite, self::$session_id, $data); if ($dowrite) { // DB::update() checks if the record key exists, and inserts if not $record = array('ip' => self::get_subnet($remote_address), 'expires' => DateTime::create()->int + self::$lifetime, 'ua' => MultiByte::substr($user_agent, 0, 255), 'data' => serialize($data)); DB::update(DB::table('sessions'), $record, array('token' => self::$session_id)); } }
/** * Receive a Pingback via XMLRPC * @param array $params An array of XMLRPC parameters from the remote call * @return string The success state of the pingback */ public function xmlrpc_pingback__ping($params) { try { list($source_uri, $target_uri) = $params; // This should really be done by an Habari core function $target_parse = InputFilter::parse_url($target_uri); $target_stub = $target_parse['path']; $base_url = Site::get_path('base', true); if ('/' != $base_url) { $target_stub = str_replace($base_url, '', $target_stub); } $target_stub = trim($target_stub, '/'); if (strpos($target_stub, '?') !== false) { list($target_stub, $query_string) = explode('?', $target_stub); } // Can this be used as a target? $target_slug = URL::parse($target_stub)->named_arg_values['slug']; if ($target_slug === false) { throw new XMLRPCException(33); } // Does the target exist? $target_post = Post::get(array('slug' => $target_slug)); if ($target_post === false) { throw new XMLRPCException(32); } // Is comment allowed? if ($target_post->info->comments_disabled) { throw new XMLRPCException(33); } // Is this Pingback already registered? if (Comments::get(array('post_id' => $target_post->id, 'url' => $source_uri, 'type' => Comment::PINGBACK))->count() > 0) { throw new XMLRPCException(48); } // Retrieve source contents try { $rr = new RemoteRequest($source_uri); $rr->execute(); if (!$rr->executed()) { throw new XMLRPCException(16); } $source_contents = $rr->get_response_body(); } catch (XMLRPCException $e) { // catch our special type of exception and re-throw it throw $e; } catch (Exception $e) { throw new XMLRPCException(-32300); } // encoding is converted into internal encoding. // @todo check BOM at beginning of file before checking for a charset attribute $habari_encoding = MultiByte::hab_encoding(); if (preg_match("/<meta[^>]+charset=([A-Za-z0-9\\-\\_]+)/i", $source_contents, $matches) && strtolower($habari_encoding) != strtolower($matches[1])) { $ret = MultiByte::convert_encoding($source_contents, $habari_encoding, $matches[1]); if ($ret !== false) { $source_contents = $ret; } } // Find the page's title preg_match('/<title>(.*)<\\/title>/is', $source_contents, $matches); $source_title = $matches[1]; // Find the reciprocal links and their context preg_match('/<body[^>]*>(.+)<\\/body>/is', $source_contents, $matches); $source_contents_filtered = preg_replace('/\\s{2,}/is', ' ', strip_tags($matches[1], '<a>')); if (!preg_match('%.{0,100}?<a[^>]*?href\\s*=\\s*("|\'|)' . $target_uri . '\\1[^>]*?' . '>(.+?)</a>.{0,100}%s', $source_contents_filtered, $source_excerpt)) { throw new XMLRPCException(17); } /** Sanitize Data */ $source_excerpt = '…' . InputFilter::filter($source_excerpt[0]) . '…'; $source_title = InputFilter::filter($source_title); $source_uri = InputFilter::filter($source_uri); /* Sanitize the URL */ if (!empty($source_uri)) { $parsed = InputFilter::parse_url($source_uri); if ($parsed['is_relative']) { // guess if they meant to use an absolute link $parsed = InputFilter::parse_url('http://' . $source_uri); if (!$parsed['is_error']) { $source_uri = InputFilter::glue_url($parsed); } else { // disallow relative URLs $source_uri = ''; } } if ($parsed['is_pseudo'] || $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https') { // allow only http(s) URLs $source_uri = ''; } else { // reconstruct the URL from the error-tolerant parsing // http:moeffju.net/blog/ -> http://moeffju.net/blog/ $source_uri = InputFilter::glue_url($parsed); } } // Add a new pingback comment $pingback = new Comment(array('post_id' => $target_post->id, 'name' => $source_title, 'email' => '', 'url' => $source_uri, 'ip' => sprintf("%u", ip2long(Utils::get_ip())), 'content' => $source_excerpt, 'status' => Comment::STATUS_UNAPPROVED, 'date' => HabariDateTime::date_create(), 'type' => Comment::PINGBACK)); $pingback->insert(); // Respond to the Pingback return 'The pingback has been registered'; } catch (XMLRPCException $e) { $e->output_fault_xml(); } }
/** * Implements custom object properties * @param string $name Name of property to return * @return mixed The requested field value */ public function __get($name) { static $moderated = null; switch ($name) { case 'count': return count($this); case 'moderated': if (empty($moderated)) { $moderated_statuses = Plugins::filter('moderated_statuses', array('approved')); $moderated_statuses = array_map(function ($value) { return Comment::status($value); }, $moderated_statuses); $moderated = array(); foreach ($this as $comment) { if (in_array($comment->status, $moderated_statuses)) { $moderated[$comment->id] = $comment; } if (isset($_COOKIE['comment_' . Options::get('GUID')])) { list($commenter, $email, $url) = explode('#', $_COOKIE['comment_' . Options::get('GUID')]); if ($comment->ip == Utils::get_ip() && $comment->name == $commenter && $comment->email == $email && $comment->url == $url) { $moderated[$comment->id] = $comment; } } } $moderated = new Comments($moderated); } return $moderated; default: if ($index = array_search($name, Comment::list_comment_statuses())) { return $this->only('status', $index); } if ($index = array_search($name, Comment::list_comment_types())) { return $this->only('type', $index); } // Dumb check for plurals $pluralize = function ($s) { return $s . 's'; }; if ($index = array_search($name, array_map($pluralize, Comment::list_comment_statuses()))) { return $this->only('status', $index); } if ($index = array_search($name, array_map($pluralize, Comment::list_comment_types()))) { return $this->only('type', $index); } } trigger_error('Property "@name" does not exist.', E_NOTICE); return null; }
/** * private function sort_comments * sorts all the comments in this set into several container buckets * so that you can then call $comments->trackbacks() to receive an * array of all trackbacks, for example **/ private function sort_comments() { $type_sort = array(Comment::COMMENT => 'comments', Comment::PINGBACK => 'pingbacks', Comment::TRACKBACK => 'trackbacks'); foreach ($this as $c) { // first, divvy up approved and unapproved comments switch ($c->status) { case Comment::STATUS_APPROVED: $this->sort['approved'][] = $c; $this->sort['moderated'][] = $c; break; case Comment::STATUS_UNAPPROVED: if (isset($_COOKIE['comment_' . Options::get('GUID')])) { list($name, $email, $url) = explode('#', $_COOKIE['comment_' . Options::get('GUID')]); } else { $name = ''; $email = ''; $url = ''; } if ($c->ip == sprintf("%u", ip2long(Utils::get_ip())) && $c->name == $name && $c->email == $email && $c->url == $url) { $this->sort['moderated'][] = $c; } $this->sort['unapproved'][] = $c; break; case Comment::STATUS_SPAM: $this->sort['spam'][] = $c; break; } // now sort by comment type $this->sort[$type_sort[$c->type]][] = $c; } }
/** * Verify a 10-digit hex code that identifies the user submitting the comment * @param A post id to which the comment has been submitted * @param The IP address of the commenter * @return True if the code is valid, false if not **/ public function verify_code($suspect_code, $post_id, $ip = '') { if ($ip == '') { $ip = sprintf("%u", ip2long(Utils::get_ip())); } $code = substr(md5($post_id . Options::get('GUID') . 'more salt' . $ip), 0, 10); $code = Plugins::filter('comment_code', $code, $post_id, $ip); return $suspect_code == $code; }
/** * Add a comment to the site * * @param mixed $post A Post object instance or Post object id * @param string $name The commenter's name * @param string $email The commenter's email address * @param string $url The commenter's website URL * @param string $content The comment content * @param array $extra An associative array of extra values that should be considered */ function add_comment($post, $name = null, $email = null, $url = null, $content = null, $extra = null) { if (is_numeric($post)) { $post = Post::get(array('id' => $post)); } if (!$post instanceof Post) { // Not sure what you're trying to pull here, but that's no good header('HTTP/1.1 403 Forbidden', true, 403); die; } /* Sanitize data */ foreach (array('name', 'url', 'email', 'content') as $k) { ${$k} = InputFilter::filter(${$k}); } // there should never be any HTML in the name, so do some extra filtering on it $name = strip_tags(html_entity_decode($name, ENT_QUOTES, 'UTF-8')); /* Sanitize the URL */ if (!empty($url)) { $parsed = InputFilter::parse_url($url); if ($parsed['is_relative']) { // guess if they meant to use an absolute link $parsed = InputFilter::parse_url('http://' . $url); if (!$parsed['is_error']) { $url = InputFilter::glue_url($parsed); } else { // disallow relative URLs $url = ''; } } if ($parsed['is_pseudo'] || $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https') { // allow only http(s) URLs $url = ''; } else { // reconstruct the URL from the error-tolerant parsing // http:moeffju.net/blog/ -> http://moeffju.net/blog/ $url = InputFilter::glue_url($parsed); } } /* Create comment object*/ $comment = new Comment(array('post_id' => $post->id, 'name' => $name, 'email' => $email, 'url' => $url, 'ip' => Utils::get_ip(), 'content' => $content, 'status' => Comment::status('approved'), 'date' => DateTime::create(), 'type' => Comment::type('comment'))); // Should this really be here or in a default filter? // In any case, we should let plugins modify the status after we set it here. $user = User::identify(); if ($user->loggedin && $comment->email == $user->email) { $comment->status = 'approved'; } // Allow themes to work with comment hooks Themes::create(); // Allow plugins to change comment data and add commentinfo based on plugin-added form fields Plugins::act('comment_accepted', $comment, $this->handler_vars, $extra); $spam_rating = 0; $spam_rating = Plugins::filter('spam_filter', $spam_rating, $comment, $this->handler_vars, $extra); if ($spam_rating >= Options::get('spam_percentage', 100)) { $comment->status = 'spam'; } $comment->insert(); $anchor = ''; // If the comment was saved if ($comment->id && $comment->status != 'spam') { $anchor = '#comment-' . $comment->id; // store in the user's session that this comment is pending moderation if ($comment->status == 'unapproved') { Session::notice(_t('Your comment is pending moderation.'), 'comment_' . $comment->id); } // if no cookie exists, we should set one // but only if the user provided some details $cookie_name = 'comment_' . Options::get('public-GUID'); // build the string we store for the cookie $cookie_content = implode('#', array($comment->name, $comment->email, $comment->url)); // if the user is not logged in and there is no cookie OR the cookie differs from the current set if (User::identify()->loggedin == false && (!isset($_COOKIE[$cookie_name]) || $_COOKIE[$cookie_name] != $cookie_content)) { // update the cookie setcookie($cookie_name, $cookie_content, time() + DateTime::YEAR, Site::get_path('base', true)); } } // Return the commenter to the original page. Utils::redirect($post->permalink . $anchor); }