KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Post.php 31KB

9 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. <?php
  2. /**
  3. * Class: Post
  4. * The Post model.
  5. *
  6. * See Also:
  7. * <Model>
  8. */
  9. class Post extends Model {
  10. public $belongs_to = "user";
  11. # Array: $url_attrs
  12. # The translation array of the post URL setting to regular expressions.
  13. # Passed through the route_code filter.
  14. static $url_attrs = array('(year)' => '([0-9]{4})',
  15. '(month)' => '([0-9]{1,2})',
  16. '(day)' => '([0-9]{1,2})',
  17. '(hour)' => '([0-9]{1,2})',
  18. '(minute)' => '([0-9]{1,2})',
  19. '(second)' => '([0-9]{1,2})',
  20. '(id)' => '([0-9]+)',
  21. '(author)' => '([^\/]+)',
  22. '(clean)' => '([^\/]+)',
  23. '(url)' => '([^\/]+)',
  24. '(feather)' => '([^\/]+)',
  25. '(feathers)' => '([^\/]+)');
  26. /**
  27. * Function: __construct
  28. * See Also:
  29. * <Model::grab>
  30. */
  31. public function __construct($post_id = null, $options = array()) {
  32. if (!isset($post_id) and empty($options)) return;
  33. if (isset($options["where"]) and !is_array($options["where"]))
  34. $options["where"] = array($options["where"]);
  35. elseif (!isset($options["where"]))
  36. $options["where"] = array();
  37. $has_status = false;
  38. foreach ($options["where"] as $key => $val)
  39. if (is_int($key) and substr_count($val, "status") or $key == "status")
  40. $has_status = true;
  41. if (!XML_RPC) {
  42. $options["where"][] = self::feathers();
  43. if (!$has_status) {
  44. $visitor = Visitor::current();
  45. $private = (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_draft")) ?
  46. self::statuses(array("draft")) :
  47. self::statuses() ;
  48. if (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_own_draft")) {
  49. $private.= " OR (status = 'draft' AND user_id = :visitor_id)";
  50. $options["params"][":visitor_id"] = $visitor->id;
  51. }
  52. $options["where"][] = $private;
  53. }
  54. }
  55. $options["left_join"][] = array("table" => "post_attributes",
  56. "where" => "post_id = posts.id");
  57. $options["select"] = array_merge(array("posts.*",
  58. "post_attributes.name AS attribute_names",
  59. "post_attributes.value AS attribute_values"),
  60. oneof(@$options["select"], array()));
  61. $options["ignore_dupes"] = array("attribute_names", "attribute_values");
  62. parent::grab($this, $post_id, $options);
  63. if ($this->no_results)
  64. return false;
  65. $this->attribute_values = (array) $this->attribute_values;
  66. $this->attribute_names = (array) $this->attribute_names;
  67. $this->attributes = ($this->attribute_names) ?
  68. array_combine($this->attribute_names, $this->attribute_values) :
  69. array() ;
  70. $this->filtered = (!isset($options["filter"]) or $options["filter"]) and !XML_RPC;
  71. $this->slug = $this->url;
  72. fallback($this->clean, $this->url);
  73. foreach ($this->attributes as $key => $val)
  74. if (!empty($key))
  75. $this->$key = $val;
  76. Trigger::current()->filter($this, "post");
  77. if ($this->filtered)
  78. $this->filter();
  79. }
  80. /**
  81. * Function: find
  82. * See Also:
  83. * <Model::search>
  84. */
  85. static function find($options = array(), $options_for_object = array(), $debug = false) {
  86. if (isset($options["where"]) and !is_array($options["where"]))
  87. $options["where"] = array($options["where"]);
  88. elseif (!isset($options["where"]))
  89. $options["where"] = array();
  90. $has_status = false;
  91. foreach ($options["where"] as $key => $val)
  92. if ((is_int($key) and substr_count($val, "status")) or $key === "status")
  93. $has_status = true;
  94. if (!XML_RPC) {
  95. $options["where"][] = self::feathers();
  96. if (!$has_status) {
  97. $visitor = Visitor::current();
  98. $private = (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_draft")) ?
  99. self::statuses(array("draft")) :
  100. self::statuses() ;
  101. if (isset($options["drafts"]) and $options["drafts"] and $visitor->group->can("view_own_draft")) {
  102. $private.= " OR (status = 'draft' AND user_id = :visitor_id)";
  103. $options["params"][":visitor_id"] = $visitor->id;
  104. }
  105. $options["where"][] = $private;
  106. }
  107. }
  108. $options["left_join"][] = array("table" => "post_attributes",
  109. "where" => "post_id = posts.id");
  110. $options["select"] = array_merge(array("posts.*",
  111. "post_attributes.name AS attribute_names",
  112. "post_attributes.value AS attribute_values"),
  113. oneof(@$options["select"], array()));
  114. $options["ignore_dupes"] = array("attribute_names", "attribute_values");
  115. fallback($options["order"], "pinned DESC, created_at DESC, id DESC");
  116. return parent::search(get_class(), $options, $options_for_object);
  117. }
  118. /**
  119. * Function: add
  120. * Adds a post to the database.
  121. *
  122. * Most of the function arguments will fall back to various POST values.
  123. *
  124. * Calls the @add_post@ trigger with the inserted post and extra options.
  125. *
  126. * Note: The default parameter values are empty here so that the fallbacks work properly.
  127. *
  128. * Parameters:
  129. * $values - The data to insert.
  130. * $clean - The sanitized URL (or empty to default to "(feather).(new post's id)").
  131. * $url - The unique URL (or empty to default to "(feather).(new post's id)").
  132. * $feather - The feather to post as.
  133. * $user - <User> to set as the post's author.
  134. * $pinned - Pin the post?
  135. * $status - Post status
  136. * $created_at - New @created_at@ timestamp for the post.
  137. * $updated_at - New @updated_at@ timestamp for the post, or @false@ to not updated it.
  138. * $trackbacks - URLs separated by " " to send trackbacks to.
  139. * $pingbacks - Send pingbacks?
  140. * $options - Options for the post.
  141. *
  142. * Returns:
  143. * The newly created <Post>.
  144. *
  145. * See Also:
  146. * <update>
  147. */
  148. static function add($values = array(),
  149. $clean = "",
  150. $url = "",
  151. $feather = null,
  152. $user = null,
  153. $pinned = null,
  154. $status = "",
  155. $created_at = null,
  156. $updated_at = null,
  157. $trackbacks = "",
  158. $pingbacks = true,
  159. $options = array()) {
  160. $user_id = ($user instanceof User) ? $user->id : $user ;
  161. $sql = SQL::current();
  162. $visitor = Visitor::current();
  163. $trigger = Trigger::current();
  164. fallback($feather, oneof(@$_POST['feather'], ""));
  165. fallback($user_id, oneof(@$_POST['user_id'], Visitor::current()->id));
  166. fallback($pinned, (int) !empty($_POST['pinned']));
  167. fallback($status, (isset($_POST['draft'])) ? "draft" : oneof(@$_POST['status'], "public"));
  168. fallback($created_at, (!empty($_POST['created_at']) and
  169. (!isset($_POST['original_time']) or $_POST['created_at'] != $_POST['original_time'])) ?
  170. datetime($_POST['created_at']) :
  171. datetime());
  172. fallback($updated_at, oneof(@$_POST['updated_at'], "0000-00-00 00:00:00"));
  173. fallback($trackbacks, oneof(@$_POST['trackbacks'], ""));
  174. fallback($options, oneof(@$_POST['option'], array()));
  175. if (isset($clean) and !isset($url))
  176. $url = self::check_url($clean);
  177. if (isset($_POST['bookmarklet'])) {
  178. $trigger->filter($values, "bookmarklet_submit_values");
  179. $trigger->filter($options, "bookmarklet_submit_options");
  180. }
  181. $new_values = array("feather" => $feather,
  182. "user_id" => $user_id,
  183. "pinned" => $pinned,
  184. "status" => $status,
  185. "clean" => $clean,
  186. "url" => $url,
  187. "created_at" => $created_at,
  188. "updated_at" => $updated_at);
  189. $trigger->filter($new_values, "before_add_post");
  190. $sql->insert("posts", $new_values);
  191. $id = $sql->latest("posts");
  192. if (empty($clean) or empty($url))
  193. $sql->update("posts",
  194. array("id" => $id),
  195. array("clean" => $feather.".".$id,
  196. "url" => $feather.".".$id));
  197. # Insert the post attributes.
  198. foreach (array_merge($values, $options) as $name => $value)
  199. $sql->insert("post_attributes",
  200. array("post_id" => $id,
  201. "name" => $name,
  202. "value" => $value));
  203. $post = new self($id, array("drafts" => true));
  204. if ($trackbacks !== "") {
  205. $trackbacks = explode(",", $trackbacks);
  206. $trackbacks = array_map("trim", $trackbacks);
  207. $trackbacks = array_map("strip_tags", $trackbacks);
  208. $trackbacks = array_unique($trackbacks);
  209. $trackbacks = array_diff($trackbacks, array(""));
  210. foreach ($trackbacks as $url)
  211. trackback_send($post, $url);
  212. }
  213. if (Config::current()->send_pingbacks and $pingbacks)
  214. foreach ($values as $key => $value)
  215. send_pingbacks($value, $post);
  216. $post->redirect = isset($_POST['bookmarklet']) ? url("/admin/?action=bookmarklet&done") : $post->url() ;
  217. $trigger->call("add_post", $post, $options);
  218. return $post;
  219. }
  220. /**
  221. * Function: update
  222. * Updates a post with the given attributes.
  223. *
  224. * Most of the function arguments will fall back to various POST values.
  225. *
  226. * Parameters:
  227. * $values - An array of data to set for the post.
  228. * $user - <User> to set as the post's author.
  229. * $pinned - Pin the post?
  230. * $status - Post status
  231. * $clean - A new clean URL for the post.
  232. * $url - A new URL for the post.
  233. * $created_at - New @created_at@ timestamp for the post.
  234. * $updated_at - New @updated_at@ timestamp for the post, or @false@ to not updated it.
  235. * $options - Options for the post.
  236. *
  237. * See Also:
  238. * <add>
  239. */
  240. public function update($values = null,
  241. $user = null,
  242. $pinned = null,
  243. $status = null,
  244. $clean = null,
  245. $url = null,
  246. $created_at = null,
  247. $updated_at = null,
  248. $options = null) {
  249. if ($this->no_results)
  250. return false;
  251. $trigger = Trigger::current();
  252. $user_id = ($user instanceof User) ? $user->id : $user ;
  253. fallback($values, array_combine($this->attribute_names, $this->attribute_values));
  254. fallback($user_id, oneof(@$_POST['user_id'], $this->user_id));
  255. fallback($pinned, (int) !empty($_POST['pinned']));
  256. fallback($status, (isset($_POST['draft'])) ? "draft" : oneof(@$_POST['status'], $this->status));
  257. fallback($clean, $this->clean);
  258. fallback($url, oneof(@$_POST['slug'], $this->feather.".".$this->id));
  259. fallback($created_at, (!empty($_POST['created_at'])) ? datetime($_POST['created_at']) : $this->created_at);
  260. fallback($updated_at, ($updated_at === false ?
  261. $this->updated_at :
  262. oneof($updated_at, @$_POST['updated_at'], datetime())));
  263. fallback($options, oneof(@$_POST['option'], array()));
  264. if ($url != $this->url) # If they edited the slug, the clean URL should change too.
  265. $clean = $url;
  266. $old = clone $this;
  267. # Update all values of this post.
  268. foreach (array("user_id", "pinned", "status", "url", "created_at", "updated_at") as $attr)
  269. $this->$attr = $$attr;
  270. $new_values = array("pinned" => $pinned,
  271. "status" => $status,
  272. "clean" => $clean,
  273. "url" => $url,
  274. "created_at" => $created_at,
  275. "updated_at" => $updated_at);
  276. $trigger->filter($new_values, "before_update_post");
  277. $sql = SQL::current();
  278. $sql->update("posts",
  279. array("id" => $this->id),
  280. $new_values);
  281. # Insert the post attributes.
  282. foreach (array_merge($values, $options) as $name => $value)
  283. if ($sql->count("post_attributes", array("post_id" => $this->id, "name" => $name)))
  284. $sql->update("post_attributes",
  285. array("post_id" => $this->id,
  286. "name" => $name),
  287. array("value" => $this->$name = $value));
  288. else
  289. $sql->insert("post_attributes",
  290. array("post_id" => $this->id,
  291. "name" => $name,
  292. "value" => $this->$name = $value));
  293. $trigger->call("update_post", $this, $old, $options);
  294. }
  295. /**
  296. * Function: delete
  297. * See Also:
  298. * <Model::destroy>
  299. */
  300. static function delete($id) {
  301. parent::destroy(get_class(), $id);
  302. SQL::current()->delete("post_attributes", array("post_id" => $id));
  303. }
  304. /**
  305. * Function: deletable
  306. * Checks if the <User> can delete the post.
  307. */
  308. public function deletable($user = null) {
  309. if ($this->no_results)
  310. return false;
  311. fallback($user, Visitor::current());
  312. if ($user->group->can("delete_post"))
  313. return true;
  314. return ($this->status == "draft" and $user->group->can("delete_draft")) or
  315. ($user->group->can("delete_own_post") and $this->user_id == $user->id) or
  316. (($user->group->can("delete_own_draft") and $this->status == "draft") and $this->user_id == $user->id);
  317. }
  318. /**
  319. * Function: editable
  320. * Checks if the <User> can edit the post.
  321. */
  322. public function editable($user = null) {
  323. if ($this->no_results)
  324. return false;
  325. fallback($user, Visitor::current());
  326. if ($user->group->can("edit_post"))
  327. return true;
  328. return ($this->status == "draft" and $user->group->can("edit_draft")) or
  329. ($user->group->can("edit_own_post") and $this->user_id == $user->id) or
  330. (($user->group->can("edit_own_draft") and $this->status == "draft") and $this->user_id == $user->id);
  331. }
  332. /**
  333. * Function: any_editable
  334. * Checks if the <Visitor> can edit any posts.
  335. */
  336. static function any_editable() {
  337. $visitor = Visitor::current();
  338. $sql = SQL::current();
  339. # Can they edit posts?
  340. if ($visitor->group->can("edit_post"))
  341. return true;
  342. # Can they edit drafts?
  343. if ($visitor->group->can("edit_draft") and
  344. $sql->count("posts", array("status" => "draft")))
  345. return true;
  346. # Can they edit their own posts, and do they have any?
  347. if ($visitor->group->can("edit_own_post") and
  348. $sql->count("posts", array("user_id" => $visitor->id)))
  349. return true;
  350. # Can they edit their own drafts, and do they have any?
  351. if ($visitor->group->can("edit_own_draft") and
  352. $sql->count("posts", array("status" => "draft", "user_id" => $visitor->id)))
  353. return true;
  354. return false;
  355. }
  356. /**
  357. * Function: any_deletable
  358. * Checks if the <Visitor> can delete any posts.
  359. */
  360. static function any_deletable() {
  361. $visitor = Visitor::current();
  362. $sql = SQL::current();
  363. # Can they delete posts?
  364. if ($visitor->group->can("delete_post"))
  365. return true;
  366. # Can they delete drafts?
  367. if ($visitor->group->can("delete_draft") and
  368. $sql->count("posts", array("status" => "draft")))
  369. return true;
  370. # Can they delete their own posts, and do they have any?
  371. if ($visitor->group->can("delete_own_post") and
  372. $sql->count("posts", array("user_id" => $visitor->id)))
  373. return true;
  374. # Can they delete their own drafts, and do they have any?
  375. if ($visitor->group->can("delete_own_draft") and
  376. $sql->count("posts", array("status" => "draft", "user_id" => $visitor->id)))
  377. return true;
  378. return false;
  379. }
  380. /**
  381. * Function: exists
  382. * Checks if a post exists.
  383. *
  384. * Parameters:
  385. * $post_id - The post ID to check
  386. *
  387. * Returns:
  388. * true - if a post with that ID is in the database.
  389. */
  390. static function exists($post_id) {
  391. return SQL::current()->count("posts", array("id" => $post_id)) == 1;
  392. }
  393. /**
  394. * Function: check_url
  395. * Checks if a given clean URL is already being used as another post's URL.
  396. *
  397. * Parameters:
  398. * $clean - The clean URL to check.
  399. *
  400. * Returns:
  401. * 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.
  402. */
  403. static function check_url($clean) {
  404. $count = SQL::current()->count("posts", array("clean" => $clean));
  405. return (!$count or empty($clean)) ? $clean : $clean."-".($count + 1) ;
  406. }
  407. /**
  408. * Function: url
  409. * Returns a post's URL.
  410. */
  411. public function url() {
  412. if ($this->no_results)
  413. return false;
  414. $config = Config::current();
  415. $visitor = Visitor::current();
  416. if (!$config->clean_urls)
  417. return $config->url."/?action=view&amp;url=".urlencode($this->url);
  418. $login = (strpos($config->post_url, "(author)") !== false) ? $this->user->login : null ;
  419. $vals = array(when("Y", $this->created_at),
  420. when("m", $this->created_at),
  421. when("d", $this->created_at),
  422. when("H", $this->created_at),
  423. when("i", $this->created_at),
  424. when("s", $this->created_at),
  425. $this->id,
  426. urlencode($login),
  427. urlencode($this->clean),
  428. urlencode($this->url),
  429. urlencode($this->feather),
  430. urlencode(pluralize($this->feather)));
  431. Trigger::current()->filter($vals, "url_vals", $this);
  432. return $config->url."/".str_replace(array_keys(self::$url_attrs), $vals, $config->post_url);
  433. }
  434. /**
  435. * Function: title_from_excerpt
  436. * Generates an acceptable Title from the post's excerpt.
  437. *
  438. * Returns:
  439. * The post's excerpt. iltered -> first line -> ftags stripped -> truncated to 75 characters -> normalized.
  440. */
  441. public function title_from_excerpt() {
  442. if ($this->no_results)
  443. return false;
  444. # Excerpts are likely to have some sort of markup module applied to them;
  445. # if the current instantiation is not filtered, make one that is.
  446. $post = ($this->filtered) ? $this : new Post($this->id) ;
  447. $excerpt = $post->excerpt();
  448. Trigger::current()->filter($excerpt, "title_from_excerpt");
  449. $split_lines = explode("\n", $excerpt);
  450. $first_line = $split_lines[0];
  451. $stripped = strip_tags($first_line); # Strip all HTML
  452. $truncated = truncate($stripped, 75); # Truncate the excerpt to 75 characters
  453. $normalized = normalize($truncated); # Trim and normalize whitespace
  454. return $normalized;
  455. }
  456. /**
  457. * Function: title
  458. * Returns the given post's title, provided by its Feather.
  459. */
  460. public function title() {
  461. if ($this->no_results)
  462. return false;
  463. # Excerpts are likely to have some sort of markup module applied to them;
  464. # if the current instantiation is not filtered, make one that is.
  465. $post = ($this->filtered) ? $this : new Post($this->id) ;
  466. $title = Feathers::$instances[$this->feather]->title($post);
  467. return Trigger::current()->filter($title, "title", $post);
  468. }
  469. /**
  470. * Function: excerpt
  471. * Returns the given post's excerpt, provided by its Feather.
  472. */
  473. public function excerpt() {
  474. if ($this->no_results)
  475. return false;
  476. # Excerpts are likely to have some sort of markup module applied to them;
  477. # if the current instantiation is not filtered, make one that is.
  478. $post = ($this->filtered) ? $this : new Post($this->id) ;
  479. $excerpt = Feathers::$instances[$this->feather]->excerpt($post);
  480. return Trigger::current()->filter($excerpt, "excerpt", $post);
  481. }
  482. /**
  483. * Function: feed_content
  484. * Returns the given post's Feed content, provided by its Feather.
  485. */
  486. public function feed_content() {
  487. if ($this->no_results)
  488. return false;
  489. # Excerpts are likely to have some sort of markup module applied to them;
  490. # if the current instantiation is not filtered, make one that is.
  491. $post = ($this->filtered) ? $this : new Post($this->id) ;
  492. $feed_content = Feathers::$instances[$this->feather]->feed_content($post);
  493. return Trigger::current()->filter($feed_content, "feed_content", $post);
  494. }
  495. /**
  496. * Function: next
  497. * Returns:
  498. * The next post (the post made after this one).
  499. */
  500. public function next() {
  501. if ($this->no_results)
  502. return false;
  503. if (isset($this->next))
  504. return $this->next;
  505. return $this->next = new self(null, array("where" => array("created_at >" => $this->created_at,
  506. $this->status == "draft" ?
  507. self::statuses(array("draft")) :
  508. self::statuses()),
  509. "order" => "created_at ASC, id ASC"));
  510. }
  511. /**
  512. * Function: prev
  513. * Returns:
  514. * The next post (the post made after this one).
  515. */
  516. public function prev() {
  517. if ($this->no_results)
  518. return false;
  519. if (isset($this->prev))
  520. return $this->prev;
  521. return $this->prev = new self(null, array("where" => array("created_at <" => $this->created_at,
  522. ($this->status == "draft" ?
  523. self::statuses(array("draft")) :
  524. self::statuses())),
  525. "order" => "created_at DESC, id DESC"));
  526. }
  527. /**
  528. * Function: theme_exists
  529. * Checks if the current post's feather theme file exists.
  530. */
  531. public function theme_exists() {
  532. return !$this->no_results and Theme::current()->file_exists("feathers/".$this->feather);
  533. }
  534. /**
  535. * Function: filter
  536. * Filters the post attributes through filter_post and any Feather filters.
  537. */
  538. private function filter() {
  539. $trigger = Trigger::current();
  540. $class = camelize($this->feather);
  541. $trigger->filter($this, "filter_post");
  542. if (isset(Feathers::$custom_filters[$class])) # Run through feather-specified filters, first.
  543. foreach (Feathers::$custom_filters[$class] as $custom_filter) {
  544. $varname = $custom_filter["field"]."_unfiltered";
  545. if (!isset($this->$varname))
  546. $this->$varname = @$this->$custom_filter["field"];
  547. $this->$custom_filter["field"] = call_user_func_array(array(Feathers::$instances[$this->feather], $custom_filter["name"]),
  548. array($this->$custom_filter["field"], $this));
  549. }
  550. if (isset(Feathers::$filters[$class])) # Now actually filter it.
  551. foreach (Feathers::$filters[$class] as $filter) {
  552. $varname = $filter["field"]."_unfiltered";
  553. if (!isset($this->$varname))
  554. $this->$varname = @$this->$filter["field"];
  555. if (isset($this->$filter["field"]) and !empty($this->$filter["field"]))
  556. $trigger->filter($this->$filter["field"], $filter["name"], $this);
  557. }
  558. }
  559. /**
  560. * Function: trackback_url
  561. * Returns the posts trackback URL.
  562. */
  563. public function trackback_url() {
  564. if ($this->no_results) return
  565. false;
  566. return Config::current()->chyrp_url."/includes/trackback.php?id=".$this->id;
  567. }
  568. /**
  569. * Function: from_url
  570. * Attempts to grab a post from its clean URL.
  571. */
  572. static function from_url($attrs = null, $options = array()) {
  573. fallback($attrs, $_GET);
  574. $where = array();
  575. $times = array("year", "month", "day", "hour", "minute", "second");
  576. preg_match_all("/\(([^\)]+)\)/", Config::current()->post_url, $matches);
  577. $params = array();
  578. foreach ($matches[1] as $attr)
  579. if (in_array($attr, $times))
  580. $where[strtoupper($attr)."(created_at)"] = $attrs[$attr];
  581. elseif ($attr == "author") {
  582. $user = new User(array("login" => $attrs['author']));
  583. $where["user_id"] = $user->id;
  584. } elseif ($attr == "feathers")
  585. $where["feather"] = depluralize($attrs['feathers']);
  586. else {
  587. $tokens = array($where, $params, $attr);
  588. Trigger::current()->filter($tokens, "post_url_token");
  589. list($where, $params, $attr) = $tokens;
  590. if ($attr !== null) {
  591. if (!isset($attrs[$attr]))
  592. continue;
  593. $where[$attr] = $attrs[$attr];
  594. }
  595. }
  596. return new self(null, array_merge($options, array("where" => $where, "params" => $params)));
  597. }
  598. /**
  599. * Function: statuses
  600. * Returns a SQL query "chunk" for the "status" column permissions of the current user.
  601. *
  602. * Parameters:
  603. * $start - An array of additional statuses to allow; "registered_only" and "private" are added deterministically.
  604. */
  605. static function statuses($start = array()) {
  606. $visitor = Visitor::current();
  607. $statuses = array_merge(array("public"), $start);
  608. if (logged_in())
  609. $statuses[] = "registered_only";
  610. if ($visitor->group->can("view_private"))
  611. $statuses[] = "private";
  612. return "(posts.status IN ('".implode("', '", $statuses)."') OR posts.status LIKE '%{".$visitor->group->id."}%') OR (posts.status LIKE '%{%' AND posts.user_id = ".$visitor->id.")";
  613. }
  614. /**
  615. * Function: enabled_feathers
  616. * Returns a SQL query "chunk" for the "feather" column so that it matches enabled feathers.
  617. */
  618. static function feathers() {
  619. return "posts.feather IN ('".implode("', '", Config::current()->enabled_feathers)."')";
  620. }
  621. /**
  622. * Function: user
  623. * Returns a post's user. Example: $post->user->login
  624. *
  625. * !! DEPRECATED AFTER 2.0 !!
  626. */
  627. public function user() {
  628. if ($this->no_results)
  629. return false;
  630. return new User($this->user_id);
  631. }
  632. /**
  633. * Function: groups
  634. * Lists the groups who can view the post if the post's status is specific to certain groups.
  635. */
  636. public function groups() {
  637. if ($this->no_results)
  638. return false;
  639. preg_match_all("/\{([0-9]+)\}/", $this->status, $groups, PREG_PATTERN_ORDER);
  640. if (empty($groups[1]))
  641. return false;
  642. $names = array();
  643. foreach ($groups[1] as $group_id) {
  644. $group = new Group($group_id);
  645. $names[] = $group->name;
  646. }
  647. return list_notate($names);
  648. }
  649. }