*/ class Post extends Model { public $belongs_to = "user"; # Array: $url_attrs # The translation array of the post URL setting to regular expressions. # Passed through the route_code filter. static $url_attrs = array('(year)' => '([0-9]{4})', '(month)' => '([0-9]{1,2})', '(day)' => '([0-9]{1,2})', '(hour)' => '([0-9]{1,2})', '(minute)' => '([0-9]{1,2})', '(second)' => '([0-9]{1,2})', '(id)' => '([0-9]+)', '(author)' => '([^\/]+)', '(clean)' => '([^\/]+)', '(url)' => '([^\/]+)', '(feather)' => '([^\/]+)', '(feathers)' => '([^\/]+)'); /** * Function: __construct * See Also: * */ public function __construct($post_id = null, $options = array()) { if (!isset($post_id) and empty($options)) return; if (isset($options["where"]) and !is_array($options["where"])) $options["where"] = array($options["where"]); elseif (!isset($options["where"])) $options["where"] = array(); $has_status = false; foreach ($options["where"] as $key => $val) if (is_int($key) and substr_count($val, "status") or $key == "status") $has_status = true; if (!XML_RPC) { $options["where"][] = self::feathers(); if (!$has_status) { $visitor = Visitor::current(); $private = (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_draft")) ? self::statuses(array("draft")) : self::statuses() ; if (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_own_draft")) { $private.= " OR (status = 'draft' AND user_id = :visitor_id)"; $options["params"][":visitor_id"] = $visitor->id; } $options["where"][] = $private; } } $options["left_join"][] = array("table" => "post_attributes", "where" => "post_id = posts.id"); $options["select"] = array_merge(array("posts.*", "post_attributes.name AS attribute_names", "post_attributes.value AS attribute_values"), oneof(@$options["select"], array())); $options["ignore_dupes"] = array("attribute_names", "attribute_values"); parent::grab($this, $post_id, $options); if ($this->no_results) return false; $this->attribute_values = (array) $this->attribute_values; $this->attribute_names = (array) $this->attribute_names; $this->attributes = ($this->attribute_names) ? array_combine($this->attribute_names, $this->attribute_values) : array() ; $this->filtered = (!isset($options["filter"]) or $options["filter"]) and !XML_RPC; $this->slug = $this->url; fallback($this->clean, $this->url); foreach ($this->attributes as $key => $val) if (!empty($key)) $this->$key = $val; Trigger::current()->filter($this, "post"); if ($this->filtered) $this->filter(); } /** * Function: find * See Also: * */ static function find($options = array(), $options_for_object = array(), $debug = false) { if (isset($options["where"]) and !is_array($options["where"])) $options["where"] = array($options["where"]); elseif (!isset($options["where"])) $options["where"] = array(); $has_status = false; foreach ($options["where"] as $key => $val) if ((is_int($key) and substr_count($val, "status")) or $key === "status") $has_status = true; if (!XML_RPC) { $options["where"][] = self::feathers(); if (!$has_status) { $visitor = Visitor::current(); $private = (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_draft")) ? self::statuses(array("draft")) : self::statuses() ; if (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_own_draft")) { $private.= " OR (status = 'draft' AND user_id = :visitor_id)"; $options["params"][":visitor_id"] = $visitor->id; } $options["where"][] = $private; } } $options["left_join"][] = array("table" => "post_attributes", "where" => "post_id = posts.id"); $options["select"] = array_merge(array("posts.*", "post_attributes.name AS attribute_names", "post_attributes.value AS attribute_values"), oneof(@$options["select"], array())); $options["ignore_dupes"] = array("attribute_names", "attribute_values"); fallback($options["order"], "pinned DESC, created_at DESC, id DESC"); return parent::search(get_class(), $options, $options_for_object); } /** * Function: add * Adds a post to the database. * * Most of the function arguments will fall back to various POST values. * * Calls the @add_post@ trigger with the inserted post and extra options. * * Note: The default parameter values are empty here so that the fallbacks work properly. * * Parameters: * $values - The data to insert. * $clean - The sanitized URL (or empty to default to "(feather).(new post's id)"). * $url - The unique URL (or empty to default to "(feather).(new post's id)"). * $feather - The feather to post as. * $user - to set as the post's author. * $pinned - Pin the post? * $status - Post status * $created_at - New @created_at@ timestamp for the post. * $updated_at - New @updated_at@ timestamp for the post, or @false@ to not updated it. * $trackbacks - URLs separated by " " to send trackbacks to. * $pingbacks - Send pingbacks? * $options - Options for the post. * * Returns: * The newly created . * * See Also: * */ static function add($values = array(), $clean = "", $url = "", $feather = null, $user = null, $pinned = null, $status = "", $created_at = null, $updated_at = null, $trackbacks = "", $pingbacks = true, $options = array()) { $user_id = ($user instanceof User) ? $user->id : $user ; $sql = SQL::current(); $visitor = Visitor::current(); $trigger = Trigger::current(); fallback($feather, oneof(@$_POST['feather'], "")); fallback($user_id, oneof(@$_POST['user_id'], Visitor::current()->id)); fallback($pinned, (int) !empty($_POST['pinned'])); fallback($status, (isset($_POST['draft'])) ? "draft" : oneof(@$_POST['status'], "public")); fallback($created_at, (!empty($_POST['created_at']) and (!isset($_POST['original_time']) or $_POST['created_at'] != $_POST['original_time'])) ? datetime($_POST['created_at']) : datetime()); fallback($updated_at, oneof(@$_POST['updated_at'], "0000-00-00 00:00:00")); fallback($trackbacks, oneof(@$_POST['trackbacks'], "")); fallback($options, oneof(@$_POST['option'], array())); if (isset($clean) and !isset($url)) $url = self::check_url($clean); if (isset($_POST['bookmarklet'])) { $trigger->filter($values, "bookmarklet_submit_values"); $trigger->filter($options, "bookmarklet_submit_options"); } $new_values = array("feather" => $feather, "user_id" => $user_id, "pinned" => $pinned, "status" => $status, "clean" => $clean, "url" => $url, "created_at" => $created_at, "updated_at" => $updated_at); $trigger->filter($new_values, "before_add_post"); $sql->insert("posts", $new_values); $id = $sql->latest("posts"); if (empty($clean) or empty($url)) $sql->update("posts", array("id" => $id), array("clean" => $feather.".".$id, "url" => $feather.".".$id)); # Insert the post attributes. foreach (array_merge($values, $options) as $name => $value) $sql->insert("post_attributes", array("post_id" => $id, "name" => $name, "value" => $value)); $post = new self($id, array("drafts" => true)); if ($trackbacks !== "") { $trackbacks = explode(",", $trackbacks); $trackbacks = array_map("trim", $trackbacks); $trackbacks = array_map("strip_tags", $trackbacks); $trackbacks = array_unique($trackbacks); $trackbacks = array_diff($trackbacks, array("")); foreach ($trackbacks as $url) trackback_send($post, $url); } if (Config::current()->send_pingbacks and $pingbacks) foreach ($values as $key => $value) send_pingbacks($value, $post); $post->redirect = isset($_POST['bookmarklet']) ? url("/admin/?action=bookmarklet&done") : $post->url() ; $trigger->call("add_post", $post, $options); return $post; } /** * Function: update * Updates a post with the given attributes. * * Most of the function arguments will fall back to various POST values. * * Parameters: * $values - An array of data to set for the post. * $user - to set as the post's author. * $pinned - Pin the post? * $status - Post status * $clean - A new clean URL for the post. * $url - A new URL for the post. * $created_at - New @created_at@ timestamp for the post. * $updated_at - New @updated_at@ timestamp for the post, or @false@ to not updated it. * $options - Options for the post. * * See Also: * */ public function update($values = null, $user = null, $pinned = null, $status = null, $clean = null, $url = null, $created_at = null, $updated_at = null, $options = null) { if ($this->no_results) return false; $trigger = Trigger::current(); $user_id = ($user instanceof User) ? $user->id : $user ; fallback($values, array_combine($this->attribute_names, $this->attribute_values)); fallback($user_id, oneof(@$_POST['user_id'], $this->user_id)); fallback($pinned, (int) !empty($_POST['pinned'])); fallback($status, (isset($_POST['draft'])) ? "draft" : oneof(@$_POST['status'], $this->status)); fallback($clean, $this->clean); fallback($url, oneof(@$_POST['slug'], $this->feather.".".$this->id)); fallback($created_at, (!empty($_POST['created_at'])) ? datetime($_POST['created_at']) : $this->created_at); fallback($updated_at, ($updated_at === false ? $this->updated_at : oneof($updated_at, @$_POST['updated_at'], datetime()))); fallback($options, oneof(@$_POST['option'], array())); if ($url != $this->url) # If they edited the slug, the clean URL should change too. $clean = $url; $old = clone $this; # Update all values of this post. foreach (array("user_id", "pinned", "status", "url", "created_at", "updated_at") as $attr) $this->$attr = $$attr; $new_values = array("pinned" => $pinned, "status" => $status, "clean" => $clean, "url" => $url, "created_at" => $created_at, "updated_at" => $updated_at); $trigger->filter($new_values, "before_update_post"); $sql = SQL::current(); $sql->update("posts", array("id" => $this->id), $new_values); # Insert the post attributes. foreach (array_merge($values, $options) as $name => $value) if ($sql->count("post_attributes", array("post_id" => $this->id, "name" => $name))) $sql->update("post_attributes", array("post_id" => $this->id, "name" => $name), array("value" => $this->$name = $value)); else $sql->insert("post_attributes", array("post_id" => $this->id, "name" => $name, "value" => $this->$name = $value)); $trigger->call("update_post", $this, $old, $options); } /** * Function: delete * See Also: * */ static function delete($id) { parent::destroy(get_class(), $id); SQL::current()->delete("post_attributes", array("post_id" => $id)); } /** * Function: deletable * Checks if the can delete the post. */ public function deletable($user = null) { if ($this->no_results) return false; fallback($user, Visitor::current()); if ($user->group->can("delete_post")) return true; return ($this->status == "draft" and $user->group->can("delete_draft")) or ($user->group->can("delete_own_post") and $this->user_id == $user->id) or (($user->group->can("delete_own_draft") and $this->status == "draft") and $this->user_id == $user->id); } /** * Function: editable * Checks if the can edit the post. */ public function editable($user = null) { if ($this->no_results) return false; fallback($user, Visitor::current()); if ($user->group->can("edit_post")) return true; return ($this->status == "draft" and $user->group->can("edit_draft")) or ($user->group->can("edit_own_post") and $this->user_id == $user->id) or (($user->group->can("edit_own_draft") and $this->status == "draft") and $this->user_id == $user->id); } /** * Function: any_editable * Checks if the can edit any posts. */ static function any_editable() { $visitor = Visitor::current(); $sql = SQL::current(); # Can they edit posts? if ($visitor->group->can("edit_post")) return true; # Can they edit drafts? if ($visitor->group->can("edit_draft") and $sql->count("posts", array("status" => "draft"))) return true; # Can they edit their own posts, and do they have any? if ($visitor->group->can("edit_own_post") and $sql->count("posts", array("user_id" => $visitor->id))) return true; # Can they edit their own drafts, and do they have any? if ($visitor->group->can("edit_own_draft") and $sql->count("posts", array("status" => "draft", "user_id" => $visitor->id))) return true; return false; } /** * Function: any_deletable * Checks if the can delete any posts. */ static function any_deletable() { $visitor = Visitor::current(); $sql = SQL::current(); # Can they delete posts? if ($visitor->group->can("delete_post")) return true; # Can they delete drafts? if ($visitor->group->can("delete_draft") and $sql->count("posts", array("status" => "draft"))) return true; # Can they delete their own posts, and do they have any? if ($visitor->group->can("delete_own_post") and $sql->count("posts", array("user_id" => $visitor->id))) return true; # Can they delete their own drafts, and do they have any? if ($visitor->group->can("delete_own_draft") and $sql->count("posts", array("status" => "draft", "user_id" => $visitor->id))) return true; return false; } /** * Function: exists * Checks if a post exists. * * Parameters: * $post_id - The post ID to check * * Returns: * true - if a post with that ID is in the database. */ static function exists($post_id) { return SQL::current()->count("posts", array("id" => $post_id)) == 1; } /** * Function: check_url * Checks if a given clean URL is already being used as another post's URL. * * Parameters: * $clean - The clean URL to check. * * Returns: * The unique version of the passed clean URL. If it's not used, it's the same as $clean. If it is, a number is appended. */ static function check_url($clean) { $count = SQL::current()->count("posts", array("clean" => $clean)); return (!$count or empty($clean)) ? $clean : $clean."-".($count + 1) ; } /** * Function: url * Returns a post's URL. */ public function url() { if ($this->no_results) return false; $config = Config::current(); $visitor = Visitor::current(); if (!$config->clean_urls) return $config->url."/?action=view&url=".urlencode($this->url); $login = (strpos($config->post_url, "(author)") !== false) ? $this->user->login : null ; $vals = array(when("Y", $this->created_at), when("m", $this->created_at), when("d", $this->created_at), when("H", $this->created_at), when("i", $this->created_at), when("s", $this->created_at), $this->id, urlencode($login), urlencode($this->clean), urlencode($this->url), urlencode($this->feather), urlencode(pluralize($this->feather))); Trigger::current()->filter($vals, "url_vals", $this); return $config->url."/".str_replace(array_keys(self::$url_attrs), $vals, $config->post_url); } /** * Function: title_from_excerpt * Generates an acceptable Title from the post's excerpt. * * Returns: * The post's excerpt. iltered -> first line -> ftags stripped -> truncated to 75 characters -> normalized. */ public function title_from_excerpt() { if ($this->no_results) return false; # Excerpts are likely to have some sort of markup module applied to them; # if the current instantiation is not filtered, make one that is. $post = ($this->filtered) ? $this : new Post($this->id) ; $excerpt = $post->excerpt(); Trigger::current()->filter($excerpt, "title_from_excerpt"); $split_lines = explode("\n", $excerpt); $first_line = $split_lines[0]; $stripped = strip_tags($first_line); # Strip all HTML $truncated = truncate($stripped, 75); # Truncate the excerpt to 75 characters $normalized = normalize($truncated); # Trim and normalize whitespace return $normalized; } /** * Function: title * Returns the given post's title, provided by its Feather. */ public function title() { if ($this->no_results) return false; # Excerpts are likely to have some sort of markup module applied to them; # if the current instantiation is not filtered, make one that is. $post = ($this->filtered) ? $this : new Post($this->id) ; $title = Feathers::$instances[$this->feather]->title($post); return Trigger::current()->filter($title, "title", $post); } /** * Function: excerpt * Returns the given post's excerpt, provided by its Feather. */ public function excerpt() { if ($this->no_results) return false; # Excerpts are likely to have some sort of markup module applied to them; # if the current instantiation is not filtered, make one that is. $post = ($this->filtered) ? $this : new Post($this->id) ; $excerpt = Feathers::$instances[$this->feather]->excerpt($post); return Trigger::current()->filter($excerpt, "excerpt", $post); } /** * Function: feed_content * Returns the given post's Feed content, provided by its Feather. */ public function feed_content() { if ($this->no_results) return false; # Excerpts are likely to have some sort of markup module applied to them; # if the current instantiation is not filtered, make one that is. $post = ($this->filtered) ? $this : new Post($this->id) ; $feed_content = Feathers::$instances[$this->feather]->feed_content($post); return Trigger::current()->filter($feed_content, "feed_content", $post); } /** * Function: next * Returns: * The next post (the post made after this one). */ public function next() { if ($this->no_results) return false; if (isset($this->next)) return $this->next; return $this->next = new self(null, array("where" => array("created_at >" => $this->created_at, $this->status == "draft" ? self::statuses(array("draft")) : self::statuses()), "order" => "created_at ASC, id ASC")); } /** * Function: prev * Returns: * The next post (the post made after this one). */ public function prev() { if ($this->no_results) return false; if (isset($this->prev)) return $this->prev; return $this->prev = new self(null, array("where" => array("created_at <" => $this->created_at, ($this->status == "draft" ? self::statuses(array("draft")) : self::statuses())), "order" => "created_at DESC, id DESC")); } /** * Function: theme_exists * Checks if the current post's feather theme file exists. */ public function theme_exists() { return !$this->no_results and Theme::current()->file_exists("feathers/".$this->feather); } /** * Function: filter * Filters the post attributes through filter_post and any Feather filters. */ private function filter() { $trigger = Trigger::current(); $class = camelize($this->feather); $trigger->filter($this, "filter_post"); if (isset(Feathers::$custom_filters[$class])) # Run through feather-specified filters, first. foreach (Feathers::$custom_filters[$class] as $custom_filter) { $varname = $custom_filter["field"]."_unfiltered"; if (!isset($this->$varname)) $this->$varname = @$this->$custom_filter["field"]; $this->$custom_filter["field"] = call_user_func_array(array(Feathers::$instances[$this->feather], $custom_filter["name"]), array($this->$custom_filter["field"], $this)); } if (isset(Feathers::$filters[$class])) # Now actually filter it. foreach (Feathers::$filters[$class] as $filter) { $varname = $filter["field"]."_unfiltered"; if (!isset($this->$varname)) $this->$varname = @$this->$filter["field"]; if (isset($this->$filter["field"]) and !empty($this->$filter["field"])) $trigger->filter($this->$filter["field"], $filter["name"], $this); } } /** * Function: trackback_url * Returns the posts trackback URL. */ public function trackback_url() { if ($this->no_results) return false; return Config::current()->chyrp_url."/includes/trackback.php?id=".$this->id; } /** * Function: from_url * Attempts to grab a post from its clean URL. */ static function from_url($attrs = null, $options = array()) { fallback($attrs, $_GET); $where = array(); $times = array("year", "month", "day", "hour", "minute", "second"); preg_match_all("/\(([^\)]+)\)/", Config::current()->post_url, $matches); $params = array(); foreach ($matches[1] as $attr) if (in_array($attr, $times)) $where[strtoupper($attr)."(created_at)"] = $attrs[$attr]; elseif ($attr == "author") { $user = new User(array("login" => $attrs['author'])); $where["user_id"] = $user->id; } elseif ($attr == "feathers") $where["feather"] = depluralize($attrs['feathers']); else { $tokens = array($where, $params, $attr); Trigger::current()->filter($tokens, "post_url_token"); list($where, $params, $attr) = $tokens; if ($attr !== null) { if (!isset($attrs[$attr])) continue; $where[$attr] = $attrs[$attr]; } } return new self(null, array_merge($options, array("where" => $where, "params" => $params))); } /** * Function: statuses * Returns a SQL query "chunk" for the "status" column permissions of the current user. * * Parameters: * $start - An array of additional statuses to allow; "registered_only" and "private" are added deterministically. */ static function statuses($start = array()) { $visitor = Visitor::current(); $statuses = array_merge(array("public"), $start); if (logged_in()) $statuses[] = "registered_only"; if ($visitor->group->can("view_private")) $statuses[] = "private"; return "(posts.status IN ('".implode("', '", $statuses)."') OR posts.status LIKE '%{".$visitor->group->id."}%') OR (posts.status LIKE '%{%' AND posts.user_id = ".$visitor->id.")"; } /** * Function: enabled_feathers * Returns a SQL query "chunk" for the "feather" column so that it matches enabled feathers. */ static function feathers() { return "posts.feather IN ('".implode("', '", Config::current()->enabled_feathers)."')"; } /** * Function: user * Returns a post's user. Example: $post->user->login * * !! DEPRECATED AFTER 2.0 !! */ public function user() { if ($this->no_results) return false; return new User($this->user_id); } /** * Function: groups * Lists the groups who can view the post if the post's status is specific to certain groups. */ public function groups() { if ($this->no_results) return false; preg_match_all("/\{([0-9]+)\}/", $this->status, $groups, PREG_PATTERN_ORDER); if (empty($groups[1])) return false; $names = array(); foreach ($groups[1] as $group_id) { $group = new Group($group_id); $names[] = $group->name; } return list_notate($names); } }