|
- <?php
- /**
- * Class: Post
- * The Post model.
- *
- * See Also:
- * <Model>
- */
- 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:
- * <Model::grab>
- */
- 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:
- * <Model::search>
- */
- 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 - <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 <Post>.
- *
- * See Also:
- * <update>
- */
- 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 - <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:
- * <add>
- */
- 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:
- * <Model::destroy>
- */
- static function delete($id) {
- parent::destroy(get_class(), $id);
- SQL::current()->delete("post_attributes", array("post_id" => $id));
- }
-
- /**
- * Function: deletable
- * Checks if the <User> 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 <User> 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 <Visitor> 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 <Visitor> 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);
- }
- }
|