PageRenderTime 59ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/post.php

https://github.com/HabariMag/habarimag-old
PHP | 1358 lines | 845 code | 153 blank | 360 comment | 141 complexity | c3a1f76a3f32f40a6db33ec0b70d342a MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * @package Habari
  4. *
  5. */
  6. /**
  7. *
  8. * Includes an instance of the PostInfo class; for holding inforecords about a Post
  9. * If the Post object describes an existing post; use the internal info object to
  10. * get, set, unset and test for existence (isset) of info records.
  11. * <code>
  12. * $this->info= new PostInfo( 1 ); // Info records of post with id = 1
  13. * $this->info->option1= "blah"; // set info record with name "option1" to value "blah"
  14. * $info_value= $this->info->option1; // get value of info record with name "option1" into variable $info_value
  15. * if ( isset ( $this->info->option1 ) ) // test for existence of "option1"
  16. * unset ( $this->info->option1 ); // delete "option1" info record
  17. * </code>
  18. *
  19. */
  20. class Post extends QueryRecord implements IsContent
  21. {
  22. // static variables to hold post status and post type values
  23. static $post_status_list = array();
  24. static $post_type_list_active = array();
  25. static $post_type_list_all = array();
  26. private $tags_object = null;
  27. private $comments_object = null;
  28. private $author_object = null;
  29. private $tokens = null;
  30. private $inforecords = null;
  31. protected $url_args;
  32. /**
  33. * returns an associative array of active post types
  34. * @param bool whether to force a refresh of the cached values
  35. * @return array An array of post type names => integer values
  36. */
  37. public static function list_active_post_types( $refresh = false )
  38. {
  39. if ( ( ! $refresh ) && ( ! empty( self::$post_type_list_active ) ) ) {
  40. return self::$post_type_list_active;
  41. }
  42. // clear out the previous cache
  43. self::$post_type_list_active = array( 'any' => 0 );
  44. $sql = 'SELECT * FROM {posttype} WHERE active = 1 ORDER BY id ASC';
  45. $results = DB::get_results( $sql );
  46. foreach ( $results as $result ) {
  47. self::$post_type_list_active[$result->name] = $result->id;
  48. }
  49. return self::$post_type_list_active;
  50. }
  51. /**
  52. * returns an associative array of all post types
  53. * @param bool whether to force a refresh of the cached values
  54. * @return array An array of post type names => (integer values, active values)
  55. */
  56. public static function list_all_post_types( $refresh = false )
  57. {
  58. if ( ( ! $refresh ) && ( ! empty( self::$post_type_list_all ) ) ) {
  59. return self::$post_type_list_all;
  60. }
  61. // clear out the previous cache
  62. self::$post_type_list_all = array( 'any' => 0 );
  63. $sql = 'SELECT * FROM {posttype} ORDER BY id ASC';
  64. $results = DB::get_results( $sql );
  65. foreach ( $results as $result ) {
  66. self::$post_type_list_all[$result->name] = array(
  67. 'id' => $result->id,
  68. 'active' => $result->active
  69. );
  70. }
  71. return self::$post_type_list_all;
  72. }
  73. /**
  74. * Activate an existing post type
  75. *
  76. * @param string The post type to activate
  77. */
  78. public static function activate_post_type( $type )
  79. {
  80. $all_post_types = Post::list_all_post_types( true ); // We force a refresh
  81. // Check if it exists
  82. if ( array_key_exists( $type, $all_post_types ) ) {
  83. if ( ! $all_post_types[$type]['active'] == 1 ) {
  84. // Activate it
  85. $sql = 'UPDATE {posttype} SET active = 1 WHERE id = ' . $all_post_types[$type]['id'];
  86. DB::query( $sql );
  87. }
  88. return true;
  89. }
  90. else {
  91. return false; // Doesn't exist
  92. }
  93. }
  94. /**
  95. * Deactivate a post type
  96. *
  97. * @param string The post type to deactivate
  98. */
  99. public static function deactivate_post_type( $type )
  100. {
  101. $active_post_types = Post::list_active_post_types( false ); // We force a refresh
  102. if ( array_key_exists( $type, $active_post_types ) ) {
  103. // $type is active so we'll deactivate it
  104. $sql = 'UPDATE {posttype} SET active = 0 WHERE id = ' . $active_post_types[$type];
  105. DB::query( $sql );
  106. return true;
  107. }
  108. return false;
  109. }
  110. /**
  111. * returns an associative array of post statuses
  112. * @param mixed $all true to list all statuses, not just external ones, Post to list external and any that match the Post status
  113. * @param boolean $refresh true to force a refresh of the cached values
  114. * @return array An array of post statuses names => interger values
  115. */
  116. public static function list_post_statuses( $all = true, $refresh = false )
  117. {
  118. $statuses = array();
  119. $statuses['any'] = 0;
  120. if ( $refresh || empty( self::$post_status_list ) ) {
  121. self::$post_status_list = array( 'any' => 0 );
  122. $sql = 'SELECT * FROM {poststatus} ORDER BY id ASC';
  123. $results = DB::get_results( $sql );
  124. self::$post_status_list = $results;
  125. }
  126. foreach ( self::$post_status_list as $status ) {
  127. if ( $all instanceof Post ) {
  128. if ( ! $status->internal || $status->id == $all->status ) {
  129. $statuses[$status->name] = $status->id;
  130. }
  131. }
  132. elseif ( $all ) {
  133. $statuses[$status->name] = $status->id;
  134. }
  135. elseif ( ! $status->internal ) {
  136. $statuses[$status->name] = $status->id;
  137. }
  138. }
  139. return $statuses;
  140. }
  141. /**
  142. * returns the integer value of the specified post status, or false
  143. * @param mixed a post status name or value
  144. * @return mixed an integer or boolean false
  145. */
  146. public static function status( $name )
  147. {
  148. $statuses = Post::list_post_statuses();
  149. if ( is_numeric( $name ) && ( false !== in_array( $name, $statuses ) ) ) {
  150. return $name;
  151. }
  152. if ( isset( $statuses[ MultiByte::strtolower( $name ) ] ) ) {
  153. return $statuses[MultiByte::strtolower( $name ) ];
  154. }
  155. return false;
  156. }
  157. /**
  158. * returns the friendly name of a post status, or null
  159. * @param mixed a post status value, or name
  160. * @return mixed a string of the status name, or null
  161. */
  162. public static function status_name( $status )
  163. {
  164. $statuses = array_flip( Post::list_post_statuses() );
  165. if ( is_numeric( $status ) && isset( $statuses[$status] ) ) {
  166. return $statuses[$status];
  167. }
  168. if ( false !== in_array( $status, $statuses ) ) {
  169. return $status;
  170. }
  171. return '';
  172. }
  173. /**
  174. * returns the integer value of the specified post type, or false
  175. * @param mixed a post type name or number
  176. * @return mixed an integer or boolean false
  177. */
  178. public static function type( $name )
  179. {
  180. $types = Post::list_active_post_types();
  181. if ( is_numeric( $name ) && ( false !== in_array( $name, $types ) ) ) {
  182. return $name;
  183. }
  184. if ( isset( $types[ MultiByte::strtolower( $name ) ] ) ) {
  185. return $types[ MultiByte::strtolower( $name ) ];
  186. }
  187. return false;
  188. }
  189. /**
  190. * returns the friendly name of a post type, or null
  191. * @param mixed a post type number, or name
  192. * @return mixed a string of the post type, or null
  193. */
  194. public static function type_name( $type )
  195. {
  196. $types = array_flip( Post::list_active_post_types() );
  197. if ( is_numeric( $type ) && isset( $types[$type] ) ) {
  198. return $types[$type];
  199. }
  200. if ( false !== in_array( $type, $types ) ) {
  201. return $type;
  202. }
  203. return '';
  204. }
  205. /**
  206. * inserts a new post type into the database, if it doesn't exist
  207. * @param string The name of the new post type
  208. * @param bool Whether the new post type is active or not
  209. * @return none
  210. */
  211. public static function add_new_type( $type, $active = true )
  212. {
  213. // refresh the cache from the DB, just to be sure
  214. $types = self::list_all_post_types( true );
  215. if ( ! array_key_exists( $type, $types ) ) {
  216. // Doesn't exist in DB.. add it and activate it.
  217. DB::query( 'INSERT INTO {posttype} (name, active) VALUES (?, ?)', array( $type, $active ) );
  218. }
  219. elseif ( $types[$type]['active'] == 0 ) {
  220. // Isn't active so we activate it
  221. self::activate_post_type( $type );
  222. }
  223. ACL::create_token( 'post_' . Utils::slugify( $type ), _t( 'Permissions to posts of type "%s"', array( $type ) ), _t( 'Content' ), true );
  224. // now force a refresh of the caches, so the new/activated type
  225. // is available for immediate use
  226. $types = self::list_active_post_types( true );
  227. $types = self::list_all_post_types( true );
  228. }
  229. /**
  230. * removes a post type from the database, if it exists and there are no posts
  231. * of the type
  232. * @param string The post type name
  233. * @return boolean
  234. * true if post type has been deleted
  235. * false if it has not been deleted (does not exist or there are posts using
  236. * this content type)
  237. */
  238. public static function delete_post_type( $type )
  239. {
  240. // refresh the cache from the DB, just to be sure
  241. $types = self::list_all_post_types( true );
  242. if ( array_key_exists( $type, $types ) ) {
  243. // Exists in DB.. check if there are content with this type.
  244. if ( ! DB::exists( '{posts}', array( 'content_type' => Post::type( $type ) ) ) ) {
  245. // Finally, remove from database and destroy tokens
  246. DB::delete( '{posttype}', array( 'name' => $type ) );
  247. ACL::destroy_token( 'post_' . Utils::slugify( $type ) );
  248. // now force a refresh of the caches, so the removed type is no longer
  249. // available for use
  250. $types = self::list_active_post_types( true );
  251. $types = self::list_all_post_types( true );
  252. return true;
  253. }
  254. }
  255. return false;
  256. }
  257. /**
  258. * inserts a new post status into the database, if it doesn't exist
  259. * @param string The name of the new post status
  260. * @param bool Whether this status is for internal use only. If true, this status will NOT be presented to the user
  261. * @return none
  262. */
  263. public static function add_new_status( $status, $internal = false )
  264. {
  265. // refresh the cache from the DB, just to be sure
  266. $statuses = self::list_post_statuses( true );
  267. if ( ! array_key_exists( $status, $statuses ) ) {
  268. // let's make sure we only insert an integer
  269. $internal = intval( $internal );
  270. DB::query( 'INSERT INTO {poststatus} (name, internal) VALUES (?, ?)', array( $status, $internal ) );
  271. // force a refresh of the cache, so the new status
  272. // is available for immediate use
  273. $statuses = self::list_post_statuses( true, true );
  274. }
  275. }
  276. public static function delete_post_status( $status )
  277. {
  278. $statuses = self::list_post_statuses( true, true );
  279. if ( array_key_exists( $status, $statuses ) ) {
  280. DB::query( 'DELETE FROM {poststatus} WHERE name = :name', array( ':name' => $status ) );
  281. // force a refresh of the cache, so the status is removed immediately
  282. $statuses = self::list_post_statuses( true, true );
  283. }
  284. }
  285. /**
  286. * Return the defined database columns for a Post.
  287. * @return array Array of columns in the Post table
  288. */
  289. public static function default_fields()
  290. {
  291. return array(
  292. 'id' => 0,
  293. 'slug' => '',
  294. 'title' => '',
  295. 'guid' => '',
  296. 'content' => '',
  297. 'cached_content' => '',
  298. 'user_id' => 0,
  299. 'status' => Post::status( 'draft' ),
  300. 'pubdate' => HabariDateTime::date_create(),
  301. 'updated' => HabariDateTime::date_create(),
  302. 'modified' => HabariDateTime::date_create(),
  303. 'content_type' => Post::type( 'entry' )
  304. );
  305. }
  306. /**
  307. * Constructor for the Post class.
  308. * @param array $paramarray an associative array of initial Post field values.
  309. */
  310. public function __construct( $paramarray = array() )
  311. {
  312. // Defaults
  313. $this->fields = array_merge(
  314. self::default_fields(),
  315. $this->fields
  316. );
  317. parent::__construct( $paramarray );
  318. if ( isset( $this->fields['tags'] ) ) {
  319. $this->tags_object = Terms::parse( $this->fields['tags'], 'Tag', Tags::vocabulary() );
  320. unset( $this->fields['tags'] );
  321. }
  322. $this->exclude_fields( 'id' );
  323. /* $this->fields['id'] could be null in case of a new post. If so, the info object is _not_ safe to use till after set_key has been called. Info records can be set immediately in any other case. */
  324. }
  325. /**
  326. * Return a single requested post.
  327. *
  328. * <code>
  329. * $post= Post::get( array( 'slug' => 'wooga' ) );
  330. * </code>
  331. *
  332. * @param array $paramarray An associative array of parameters, or a querystring
  333. * @return Post The first post that matched the given criteria
  334. */
  335. static function get( $paramarray = array() )
  336. {
  337. // Defaults
  338. $defaults = array (
  339. 'where' => array(
  340. array(
  341. 'status' => Post::status( 'published' ),
  342. ),
  343. ),
  344. 'fetch_fn' => 'get_row',
  345. );
  346. foreach ( $defaults['where'] as $index => $where ) {
  347. $defaults['where'][$index] = array_merge( $where, Utils::get_params( $paramarray ) );
  348. }
  349. // make sure we get at most one result
  350. $defaults['limit'] = 1;
  351. return Posts::get( $defaults );
  352. }
  353. /**
  354. * Create a post and save it.
  355. *
  356. * @param array $paramarray An associative array of post fields
  357. * @return Post The new Post object
  358. */
  359. static function create( $paramarray )
  360. {
  361. $post = new Post( $paramarray );
  362. $post->insert();
  363. return $post;
  364. }
  365. /**
  366. * Generate a new slug for the post.
  367. *
  368. * @return string The slug
  369. */
  370. private function setslug()
  371. {
  372. // determine the base value from:
  373. // - the new slug
  374. if ( isset( $this->newfields['slug'] ) && $this->newfields['slug'] != '' ) {
  375. $value = $this->newfields['slug'];
  376. }
  377. // - the new empty slug whilst in draft or progressing directly to published or scheduled from draft. Also allow changing of slug whilst in scheduled state
  378. elseif ( isset( $this->newfields['slug'] ) && $this->newfields['slug'] == '' ) {
  379. if ( $this->fields['status'] == Post::status( 'draft' ) || ( $this->fields['status'] != Post::status( 'draft' ) && $this->newfields['status'] != Post::status( 'draft' ) ) ) {
  380. if ( isset( $this->newfields['title'] ) && $this->newfields['title'] != '' ) {
  381. $value = $this->newfields['title'];
  382. }
  383. else {
  384. $value = $this->fields['title'];
  385. }
  386. }
  387. }
  388. // - the existing slug
  389. elseif ( $this->fields['slug'] != '' ) {
  390. $value = $this->fields['slug'];
  391. }
  392. // - the new post title
  393. elseif ( isset( $this->newfields['title'] ) && $this->newfields['title'] != '' ) {
  394. $value = $this->newfields['title'];
  395. }
  396. // - the existing post title
  397. elseif ( $this->fields['title'] != '' ) {
  398. $value = $this->fields['title'];
  399. }
  400. // - default
  401. else {
  402. $value = 'Post';
  403. }
  404. // make sure our slug is unique
  405. $slug = Plugins::filter( 'post_setslug', $value );
  406. $slug = Utils::slugify( $slug );
  407. $postfix = '';
  408. $postfixcount = 0;
  409. do {
  410. if ( ! $slugcount = DB::get_row( 'SELECT COUNT(slug) AS ct FROM {posts} WHERE slug = ?;', array( $slug . $postfix ) ) ) {
  411. Utils::debug( DB::get_errors() );
  412. exit;
  413. }
  414. if ( $slugcount->ct != 0 ) {
  415. $postfix = "-" . ( ++$postfixcount );
  416. }
  417. } while ( $slugcount->ct != 0 );
  418. return $this->newfields['slug'] = $slug . $postfix;
  419. }
  420. /**
  421. * Generate the GUID for the new post.
  422. */
  423. private function setguid()
  424. {
  425. if ( ! isset( $this->newfields['guid'] )
  426. || ( $this->newfields['guid'] == '' ) // GUID is empty
  427. || ( $this->newfields['guid'] == '//?p=' ) // GUID created by WP was erroneous (as is too common)
  428. ) {
  429. $result = 'tag:' . Site::get_url( 'hostname' ) . ',' . date( 'Y' ) . ':' . rawurlencode( $this->setslug() ) . '/' . time();
  430. $this->newfields['guid'] = $result;
  431. }
  432. return $this->newfields['guid'];
  433. }
  434. /**
  435. * function setstatus
  436. * @param mixed the status to set it to. String or integer.
  437. * @return integer the status of the post, or false if the new status isn't valid
  438. * Sets the status for a post, given a string or integer.
  439. */
  440. private function setstatus( $value )
  441. {
  442. $statuses = Post::list_post_statuses();
  443. $fieldname = isset( $this->fields['status'] ) ? 'newfields' : 'fields';
  444. if ( is_numeric( $value ) && in_array( $value, $statuses ) ) {
  445. return $this->{"$fieldname"}['status'] = $value;
  446. }
  447. elseif ( array_key_exists( $value, $statuses ) ) {
  448. return $this->{"$fieldname"}['status'] = Post::status( $value );
  449. }
  450. return false;
  451. }
  452. /**
  453. * Save the tags associated to this post into the terms and object_terms tables
  454. */
  455. private function save_tags()
  456. {
  457. return Tags::save_associations( $this->get_tags(), $this->id );
  458. }
  459. /**
  460. * function insert
  461. * Saves a new post to the posts table
  462. */
  463. public function insert()
  464. {
  465. $this->newfields['updated'] = HabariDateTime::date_create();
  466. $this->newfields['modified'] = $this->newfields['updated'];
  467. $this->setguid();
  468. $allow = true;
  469. $allow = Plugins::filter( 'post_insert_allow', $allow, $this );
  470. if ( ! $allow ) {
  471. return;
  472. }
  473. Plugins::act( 'post_insert_before', $this );
  474. // Invoke plugins for all fields, since they're all "changed" when inserted
  475. foreach ( $this->fields as $fieldname => $value ) {
  476. Plugins::act( 'post_update_' . $fieldname, $this, ( $this->id == 0 ) ? null : $value, $this->$fieldname );
  477. }
  478. // invoke plugins for status changes
  479. Plugins::act( 'post_status_' . self::status_name( $this->status ), $this, null );
  480. $result = parent::insertRecord( DB::table( 'posts' ) );
  481. $this->newfields['id'] = DB::last_insert_id(); // Make sure the id is set in the post object to match the row id
  482. $this->fields = array_merge( $this->fields, $this->newfields );
  483. $this->newfields = array();
  484. $this->info->commit( DB::last_insert_id() );
  485. $this->save_tags();
  486. $this->create_default_permissions();
  487. EventLog::log( sprintf( _t( 'New post %1$s (%2$s); Type: %3$s; Status: %4$s' ), $this->id, $this->slug, Post::type_name( $this->content_type ), $this->statusname ), 'info', 'content', 'habari' );
  488. Plugins::act( 'post_insert_after', $this );
  489. //scheduled post
  490. if ( $this->status == Post::status( 'scheduled' ) ) {
  491. Posts::update_scheduled_posts_cronjob();
  492. }
  493. return $result;
  494. }
  495. /**
  496. * function update
  497. * Updates an existing post in the posts table
  498. * @param bool $minor Indicates if this is a major or minor update
  499. */
  500. public function update( $minor = true )
  501. {
  502. $this->modified = HabariDateTime::date_create();
  503. if ( ! $minor && $this->status != Post::status( 'draft' ) ) {
  504. $this->updated = $this->modified;
  505. }
  506. if ( isset( $this->fields['guid'] ) ) {
  507. unset( $this->newfields['guid'] );
  508. }
  509. $allow = true;
  510. $allow = Plugins::filter( 'post_update_allow', $allow, $this );
  511. if ( ! $allow ) {
  512. return;
  513. }
  514. Plugins::act( 'post_update_before', $this );
  515. // Call setslug() only when post slug is changed
  516. if ( isset( $this->newfields['slug'] ) ) {
  517. if ( $this->fields['slug'] != $this->newfields['slug'] ) {
  518. $this->setslug();
  519. }
  520. }
  521. // invoke plugins for all fields which have been changed
  522. // For example, a plugin action "post_update_status" would be
  523. // triggered if the post has a new status value
  524. foreach ( $this->newfields as $fieldname => $value ) {
  525. Plugins::act( 'post_update_' . $fieldname, $this, $this->fields[$fieldname], $value );
  526. }
  527. // invoke plugins for status changes
  528. if ( isset( $this->newfields['status'] ) && $this->fields['status'] != $this->newfields['status'] ) {
  529. Plugins::act( 'post_status_' . self::status_name( $this->newfields['status'] ), $this, $this->fields['status'] );
  530. }
  531. $result = parent::updateRecord( DB::table( 'posts' ), array( 'id' => $this->id ) );
  532. //scheduled post
  533. if ( $this->fields['status'] == Post::status( 'scheduled' ) || $this->status == Post::status( 'scheduled' ) ) {
  534. Posts::update_scheduled_posts_cronjob();
  535. }
  536. $this->fields = array_merge( $this->fields, $this->newfields );
  537. $this->newfields = array();
  538. $this->save_tags();
  539. $this->info->commit();
  540. Plugins::act( 'post_update_after', $this );
  541. return $result;
  542. }
  543. /**
  544. * function delete
  545. * Deletes an existing post
  546. */
  547. public function delete()
  548. {
  549. $allow = true;
  550. $allow = Plugins::filter( 'post_delete_allow', $allow, $this );
  551. if ( ! $allow ) {
  552. return;
  553. }
  554. // invoke plugins
  555. Plugins::act( 'post_delete_before', $this );
  556. // delete all the tags associated with this post
  557. Tags::save_associations( new Terms(), $this->id );
  558. // Delete all comments associated with this post
  559. if ( $this->comments->count() > 0 ) {
  560. $this->comments->delete();
  561. }
  562. // Delete all info records associated with this post
  563. $this->info->delete_all();
  564. // Delete all post_tokens associated with this post
  565. $this->delete_tokens();
  566. $result = parent::deleteRecord( DB::table( 'posts' ), array( 'slug'=>$this->slug ) );
  567. EventLog::log( sprintf( _t( 'Post %1$s (%2$s) deleted.' ), $this->id, $this->slug ), 'info', 'content', 'habari' );
  568. //scheduled post
  569. if ( $this->status == Post::status( 'scheduled' ) ) {
  570. Posts::update_scheduled_posts_cronjob();
  571. }
  572. // invoke plugins on the after_post_delete action
  573. Plugins::act( 'post_delete_after', $this );
  574. return $result;
  575. }
  576. /**
  577. * function publish
  578. * Updates an existing post to published status
  579. * @return boolean True on success, false if not
  580. */
  581. public function publish()
  582. {
  583. if ( $this->status == Post::status( 'published' ) ) {
  584. return true;
  585. }
  586. $allow = true;
  587. $allow = Plugins::filter( 'post_publish_allow', $allow, $this );
  588. if ( ! $allow ) {
  589. return;
  590. }
  591. Plugins::act( 'post_publish_before', $this );
  592. if ( $this->status != Post::status( 'scheduled' ) ) {
  593. $this->pubdate = HabariDateTime::date_create();
  594. }
  595. if ( $this->status == Post::status( 'scheduled' ) ) {
  596. $msg = sprintf( _t( 'Scheduled Post %1$s (%2$s) published at %3$s.' ), $this->id, $this->slug, $this->pubdate->format() );
  597. }
  598. else {
  599. $msg = sprintf( _t( 'Post %1$s (%2$s) published.' ), $this->id, $this->slug );
  600. }
  601. $this->status = Post::status( 'published' );
  602. $result = $this->update( false );
  603. EventLog::log( $msg, 'info', 'content', 'habari' );
  604. // and call any final plugins
  605. Plugins::act( 'post_publish_after', $this );
  606. return $result;
  607. }
  608. /**
  609. * function __get
  610. * Overrides QueryRecord __get to implement custom object properties
  611. * @param string Name of property to return
  612. * @return mixed The requested field value
  613. */
  614. public function __get( $name )
  615. {
  616. // some properties are considered special and accidentally filtering them would be bad, so we exclude those
  617. $fieldnames = array_merge( array_keys( $this->fields ), array( 'permalink', 'tags', 'comments', 'comment_count', 'approved_comment_count', 'comment_feed_link', 'author', 'editlink', 'info' ) );
  618. $filter = false;
  619. if ( !in_array( $name, $fieldnames ) && strpos( $name, '_' ) !== false ) {
  620. $field_matches = implode('|', $fieldnames);
  621. if(preg_match( '/^(' . $field_matches . ')_(.+)$/', $name, $matches )) {
  622. list( $junk, $name, $filter )= $matches;
  623. }
  624. }
  625. switch ( $name ) {
  626. case 'statusname':
  627. $out = self::status_name( $this->status );
  628. break;
  629. case 'typename':
  630. $out = self::type_name( $this->content_type );
  631. break;
  632. case 'permalink':
  633. $out = $this->get_permalink();
  634. break;
  635. case 'editlink':
  636. $out = $this->get_editlink();
  637. break;
  638. case 'tags':
  639. $out = $this->get_tags();
  640. break;
  641. case 'comments':
  642. $out = $this->get_comments();
  643. break;
  644. case 'comment_count':
  645. $out = $this->get_comments()->count();
  646. break;
  647. case 'approved_comment_count':
  648. $out = Comments::count_by_id( $this->id );
  649. break;
  650. case 'comment_feed_link':
  651. $out = $this->get_comment_feed_link();
  652. break;
  653. case 'author':
  654. $out = $this->get_author();
  655. break;
  656. case 'info':
  657. $out = $this->get_info();
  658. break;
  659. default:
  660. $out = parent::__get( $name );
  661. break;
  662. }
  663. $out = Plugins::filter( "post_get", $out, $name, $this );
  664. $out = Plugins::filter( "post_{$name}", $out, $this );
  665. if ( $filter ) {
  666. $out = Plugins::filter( "post_{$name}_{$filter}", $out, $this );
  667. }
  668. return $out;
  669. }
  670. /**
  671. * function __set
  672. * Overrides QueryRecord __set to implement custom object properties
  673. * @param string Name of property to return
  674. * @return mixed The requested field value
  675. */
  676. public function __set( $name, $value )
  677. {
  678. switch ( $name ) {
  679. case 'pubdate':
  680. case 'updated':
  681. case 'modified':
  682. if ( !( $value instanceOf HabariDateTime ) ) {
  683. $value = HabariDateTime::date_create( $value );
  684. }
  685. break;
  686. case 'tags':
  687. if ( $value instanceof Terms ) {
  688. return $this->tags_object = $value;
  689. }
  690. elseif ( is_array( $value ) ) {
  691. return $this->tags_object = new Terms($value);
  692. }
  693. else {
  694. return $this->tags_object = Terms::parse( $value, 'Term', Tags::vocabulary() );
  695. }
  696. case 'status':
  697. return $this->setstatus( $value );
  698. }
  699. return parent::__set( $name, $value );
  700. }
  701. /**
  702. * Handle calls to this Post object that are implemented by plugins
  703. * @param string $name The name of the function called
  704. * @param array $args Arguments passed to the function call
  705. * @return mixed The value returned from any plugin filters, null if no value is returned
  706. */
  707. public function __call( $name, $args )
  708. {
  709. array_unshift( $args, 'post_call_' . $name, null, $this );
  710. return call_user_func_array( array( 'Plugins', 'filter' ), $args );
  711. }
  712. /**
  713. * Returns a form for editing this post
  714. * @param string $context The context the form is being created in, most often 'admin'
  715. * @return FormUI A form appropriate for creating and updating this post.
  716. */
  717. public function get_form( $context )
  718. {
  719. $form = new FormUI( 'create-content' );
  720. $form->class[] = 'create';
  721. $newpost = ( 0 === $this->id );
  722. // If the post has already been saved, add a link to its permalink
  723. if ( !$newpost ) {
  724. $post_links = $form->append( 'wrapper', 'post_links' );
  725. $permalink = ( $this->status != Post::status( 'published' ) ) ? $this->permalink . '?preview=1' : $this->permalink;
  726. $post_links->append( 'static', 'post_permalink', '<a href="'. $permalink .'" class="viewpost" >'.( $this->status != Post::status( 'published' ) ? _t( 'Preview Post' ) : _t( 'View Post' ) ).'</a>' );
  727. $post_links->class ='container';
  728. }
  729. // Create the Title field
  730. $form->append( 'text', 'title', 'null:null', _t( 'Title' ), 'admincontrol_text' );
  731. $form->title->class[] = 'important';
  732. $form->title->class[] = 'check-change';
  733. $form->title->tabindex = 1;
  734. $form->title->value = $this->title;
  735. // Create the silos
  736. if ( count( Plugins::get_by_interface( 'MediaSilo' ) ) ) {
  737. $form->append( 'silos', 'silos' );
  738. $form->silos->silos = Media::dir();
  739. }
  740. // Create the Content field
  741. $form->append( 'textarea', 'content', 'null:null', _t( 'Content' ), 'admincontrol_textarea' );
  742. $form->content->class[] = 'resizable';
  743. $form->content->class[] = 'check-change';
  744. $form->content->tabindex = 2;
  745. $form->content->value = $this->content;
  746. $form->content->raw = true;
  747. // Create the tags field
  748. $form->append( 'text', 'tags', 'null:null', _t( 'Tags, separated by, commas' ), 'admincontrol_text' );
  749. $form->tags->class = 'check-change';
  750. $form->tags->tabindex = 3;
  751. $form->tags->value = implode( ', ', (array)$this->get_tags() );
  752. // Create the splitter
  753. $publish_controls = $form->append( 'tabs', 'publish_controls' );
  754. // Create the publishing controls
  755. // pass "false" to list_post_statuses() so that we don't include internal post statuses
  756. $statuses = Post::list_post_statuses( $this );
  757. unset( $statuses[array_search( 'any', $statuses )] );
  758. $statuses = Plugins::filter( 'admin_publish_list_post_statuses', $statuses );
  759. $settings = $publish_controls->append( 'fieldset', 'settings', _t( 'Settings' ) );
  760. $settings->append( 'select', 'status', 'null:null', _t( 'Content State' ), array_flip( $statuses ), 'tabcontrol_select' );
  761. $settings->status->value = $this->status;
  762. // hide the minor edit checkbox if the post is new
  763. if ( $newpost ) {
  764. $settings->append( 'hidden', 'minor_edit', 'null:null' );
  765. $settings->minor_edit->value = false;
  766. }
  767. else {
  768. $settings->append( 'checkbox', 'minor_edit', 'null:null', _t( 'Minor Edit' ), 'tabcontrol_checkbox' );
  769. $settings->minor_edit->value = true;
  770. $form->append( 'hidden', 'modified', 'null:null' )->value = $this->modified;
  771. }
  772. $settings->append( 'checkbox', 'comments_enabled', 'null:null', _t( 'Comments Allowed' ), 'tabcontrol_checkbox' );
  773. $settings->comments_enabled->value = $this->info->comments_disabled ? false : true;
  774. $settings->append( 'text', 'pubdate', 'null:null', _t( 'Publication Time' ), 'tabcontrol_text' );
  775. $settings->pubdate->value = $this->pubdate->format( 'Y-m-d H:i:s' );
  776. $settings->append( 'hidden', 'updated', 'null:null' );
  777. $settings->updated->value = $this->updated->int;
  778. $settings->append( 'text', 'newslug', 'null:null', _t( 'Content Address' ), 'tabcontrol_text' );
  779. $settings->newslug->value = $this->slug;
  780. // Create the button area
  781. $buttons = $form->append( 'fieldset', 'buttons' );
  782. $buttons->template = 'admincontrol_buttons';
  783. $buttons->class[] = 'container';
  784. $buttons->class[] = 'buttons';
  785. $buttons->class[] = 'publish';
  786. // Create the Save button
  787. $require_any = array( 'own_posts' => 'create', 'post_any' => 'create', 'post_' . Post::type_name( $this->content_type ) => 'create' );
  788. if ( ( $newpost && User::identify()->can_any( $require_any ) ) || ( !$newpost && ACL::access_check( $this->get_access(), 'edit' ) ) ) {
  789. $buttons->append( 'submit', 'save', _t( 'Save' ), 'admincontrol_submit' );
  790. $buttons->save->tabindex = 4;
  791. }
  792. // Add required hidden controls
  793. $form->append( 'hidden', 'content_type', 'null:null' );
  794. $form->content_type->id = 'content_type';
  795. $form->content_type->value = $this->content_type;
  796. $form->append( 'hidden', 'post_id', 'null:null' );
  797. $form->post_id->id = 'id';
  798. $form->post_id->value = $this->id;
  799. $form->append( 'hidden', 'slug', 'null:null' );
  800. $form->slug->value = $this->slug;
  801. // Let plugins alter this form
  802. Plugins::act( 'form_publish', $form, $this, $context );
  803. // Return the form object
  804. return $form;
  805. }
  806. /**
  807. * Manage this post's comment form
  808. *
  809. * @param String context // What is $context for ?
  810. * @return FormUI The comment form for this post
  811. */
  812. public function comment_form( $context = 'public' )
  813. {
  814. // Handle comment submissions and default commenter id values
  815. $cookie = 'comment_' . Options::get( 'GUID' );
  816. $commenter_name = '';
  817. $commenter_email = '';
  818. $commenter_url = '';
  819. $commenter_content = '';
  820. $user = User::identify();
  821. if ( isset( $_SESSION['comment'] ) ) {
  822. $details = Session::get_set( 'comment' );
  823. $commenter_name = $details['name'];
  824. $commenter_email = $details['email'];
  825. $commenter_url = $details['url'];
  826. $commenter_content = $details['content'];
  827. }
  828. elseif ( $user->loggedin ) {
  829. $commenter_name = $user->displayname;
  830. $commenter_email = $user->email;
  831. $commenter_url = Site::get_url( 'habari' );
  832. }
  833. elseif ( isset( $_COOKIE[$cookie] ) ) {
  834. // limit to 3 elements so a # in the URL stays appended
  835. $commenter = explode( '#', $_COOKIE[ $cookie ], 3 );
  836. // make sure there are always at least 3 elements
  837. $commenter = array_pad( $commenter, 3, null );
  838. list( $commenter_name, $commenter_email, $commenter_url ) = $commenter;
  839. }
  840. // Now start the form.
  841. $form = new FormUI( 'comment-' . $context, 'comment' );
  842. $form->class[] = $context;
  843. $form->class[] = 'commentform';
  844. $form->set_option( 'form_action', URL::get( 'submit_feedback', array( 'id' => $this->id ) ) );
  845. // Create the Name field
  846. $form->append(
  847. 'text',
  848. 'cf_commenter',
  849. 'null:null',
  850. _t( 'Name <span class="required">*Required</span>' ),
  851. 'formcontrol_text'
  852. )->add_validator( 'validate_required', _t( 'The Name field value is required' ) )
  853. ->id = 'comment_name';
  854. $form->cf_commenter->tabindex = 1;
  855. $form->cf_commenter->value = $commenter_name;
  856. // Create the Email field
  857. $form->append(
  858. 'text',
  859. 'cf_email',
  860. 'null:null',
  861. _t( 'Email' ),
  862. 'formcontrol_text'
  863. )->add_validator( 'validate_email', _t( 'The Email field value must be a valid email address' ) )
  864. ->id = 'comment_email';
  865. $form->cf_email->tabindex = 2;
  866. if ( Options::get( 'comments_require_id' ) == 1 ) {
  867. $form->cf_email->add_validator( 'validate_required', _t( 'The Email field value must be a valid email address' ) );
  868. $form->cf_email->caption = _t( 'Email <span class="required">*Required</span>' );
  869. }
  870. $form->cf_email->value = $commenter_email;
  871. // Create the URL field
  872. $form->append(
  873. 'text',
  874. 'cf_url',
  875. 'null:null',
  876. _t( 'Website' ),
  877. 'formcontrol_text'
  878. )->add_validator( 'validate_url', _t( 'The Web Site field value must be a valid URL' ) )
  879. ->id = 'comment_url';
  880. $form->cf_url->tabindex = 3;
  881. $form->cf_url->value = $commenter_url;
  882. // Create the Comment field
  883. $form->append(
  884. 'text',
  885. 'cf_content',
  886. 'null:null',
  887. _t( 'Comment' ),
  888. 'formcontrol_textarea'
  889. )->add_validator( 'validate_required', _t( 'The Content field value is required' ) )
  890. ->id = 'comment_content';
  891. $form->cf_content->tabindex = 4;
  892. $form->cf_content->value = $commenter_content;
  893. // Create the Submit button
  894. $form->append( 'submit', 'cf_submit', _t( 'Submit' ), 'formcontrol_submit' );
  895. $form->cf_submit->tabindex = 5;
  896. // Add required hidden controls
  897. /*
  898. $form->append( 'hidden', 'content_type', 'null:null' );
  899. $form->content_type->value = $this->content_type;
  900. $form->append( 'hidden', 'post_id', 'null:null' );
  901. $form->post_id->id = 'id';
  902. $form->post_id->value = $this->id;
  903. $form->append( 'hidden', 'slug', 'null:null' );
  904. $form->slug->value = $this->slug;
  905. */
  906. // Let plugins alter this form
  907. Plugins::act( 'form_comment', $form, $this, $context );
  908. // Return the form object
  909. return $form;
  910. }
  911. /**
  912. * Returns a URL for the ->permalink property of this class.
  913. * @return string A URL to this post.
  914. * @todo separate permalink rule? (Not sure what this means - OW)
  915. */
  916. private function get_permalink()
  917. {
  918. $content_type = Post::type_name( $this->content_type );
  919. return URL::get(
  920. array(
  921. "display_{$content_type}",
  922. "display_post"
  923. ),
  924. $this,
  925. false
  926. );
  927. }
  928. /**
  929. * Returns a list of CSS classes for the post
  930. *
  931. * @param string|array $append Additional classes that should be added to the ones generated
  932. * @return string The resultant classes
  933. */
  934. public function css_class ( $append = array() ) {
  935. $classes = $append;
  936. $classes[] = 'post';
  937. $classes[] = 'post-' . $this->id;
  938. $classes[] = 'type-' . $this->typename;
  939. $classes[] = 'status-' . $this->statusname;
  940. foreach ( $this->tags as $tag ) {
  941. $classes[] = 'tag-' . $tag->term;
  942. }
  943. return implode( ' ', $classes );
  944. }
  945. /**
  946. * Returns a URL for the ->editlink property of this class.
  947. * @return string A url to edit this post in the admin.
  948. */
  949. private function get_editlink()
  950. {
  951. return URL::get( 'admin', 'page=publish&id=' . $this->id );
  952. }
  953. /**
  954. * function get_tags
  955. * Gets the tags for the post
  956. * @return Terms The tags object for this post
  957. */
  958. private function get_tags()
  959. {
  960. if ( $this->tags_object == null ) {
  961. $result = Tags::vocabulary()->get_associations( $this->id );
  962. if ( $result ) {
  963. $tags = new Terms($result);
  964. }
  965. else {
  966. $tags = new Terms();
  967. }
  968. $this->tags_object = $tags;
  969. }
  970. else {
  971. $tags = $this->tags_object;
  972. }
  973. return $tags;
  974. }
  975. /**
  976. * function get_comments
  977. * Gets the comments for the post
  978. * @return &array A reference to the comments array for this post
  979. */
  980. private function &get_comments()
  981. {
  982. if ( ! $this->comments_object ) {
  983. $this->comments_object = Comments::by_post_id( $this->id );
  984. }
  985. return $this->comments_object;
  986. }
  987. /**
  988. * private function get_comment_feed_link
  989. * Returns the permalink for this post's comments Atom feed
  990. * @return string The permalink of this post's comments Atom feed
  991. */
  992. private function get_comment_feed_link()
  993. {
  994. $content_type = Post::type_name( $this->content_type );
  995. return URL::get( array( "atom_feed_{$content_type}_comments" ), $this, false );
  996. }
  997. /**
  998. * function get_info
  999. * Gets the info object for this post, which contains data from the postinfo table
  1000. * related to this post.
  1001. * @return PostInfo object
  1002. */
  1003. private function get_info()
  1004. {
  1005. if ( ! isset( $this->inforecords ) ) {
  1006. // If this post isn't in the database yet...
  1007. if ( 0 == $this->id ) {
  1008. $this->inforecords = new PostInfo();
  1009. }
  1010. else {
  1011. $this->inforecords = new PostInfo( $this->id );
  1012. }
  1013. }
  1014. else {
  1015. $this->inforecords->set_key( $this->id );
  1016. }
  1017. return $this->inforecords;
  1018. }
  1019. /**
  1020. * private function get_author()
  1021. * returns a User object for the author of this post
  1022. * @return User a User object for the author of the current post
  1023. */
  1024. private function get_author()
  1025. {
  1026. if ( ! isset( $this->author_object ) ) {
  1027. // XXX for some reason, user_id is a string sometimes?
  1028. $this->author_object = User::get_by_id( $this->user_id );
  1029. }
  1030. return $this->author_object;
  1031. }
  1032. /**
  1033. * Returns a set of properties used by URL::get to create URLs
  1034. * @return array Properties of this post used to build a URL
  1035. */
  1036. public function get_url_args()
  1037. {
  1038. if ( !$this->url_args ) {
  1039. $arr = array( 'content_type_name' => Post::type_name( $this->content_type ) );
  1040. $author = URL::extract_args( $this->author, 'author_' );
  1041. $info = URL::extract_args( $this->info, 'info_' );
  1042. $this->url_args = array_merge( $author, $info, $arr, $this->to_array(), $this->pubdate->getdate() );
  1043. }
  1044. return $this->url_args;
  1045. }
  1046. /**
  1047. * Returns the ascending post, relative to this post, according to params
  1048. * @params The params by which to work out what is the ascending post
  1049. * @return Post The ascending post
  1050. */
  1051. public function ascend( $params = null )
  1052. {
  1053. return Posts::ascend( $this, $params );
  1054. }
  1055. /**
  1056. * Returns the descending post, relative to this post, according to params
  1057. * @params The params by which to work out what is the descending post
  1058. * @return Post The descending post
  1059. */
  1060. public function descend( $params = null )
  1061. {
  1062. return Posts::descend( $this, $params );
  1063. }
  1064. /**
  1065. * Return the content type of this object
  1066. *
  1067. * @return array An array of content types that this object represents, starting with the most specific
  1068. * @see IsContent
  1069. */
  1070. public function content_type()
  1071. {
  1072. return array( Post::type_name( $this->content_type ), 'Post' );
  1073. }
  1074. /**
  1075. * Adds the default tokens to this post when it's saved
  1076. */
  1077. public function create_default_tokens()
  1078. {
  1079. $tokens = array();
  1080. $tokens = Plugins::filter( 'post_tokens', $tokens, $this );
  1081. $this->add_tokens( $this->content_type() );
  1082. }
  1083. /**
  1084. * Checks if this post has one or more tokens
  1085. *
  1086. * @param mixed $tokens A single token string or an array of tokens
  1087. * @return mixed false if no tokens match, an array of matching token ids if any match
  1088. */
  1089. public function has_tokens( $tokens )
  1090. {
  1091. $this->get_tokens();
  1092. $tokens = Utils::single_array( $tokens );
  1093. $tokens = array_map( array( 'ACL', 'token_id' ), $tokens );
  1094. $tokens = array_intersect( $tokens, $this->tokens );
  1095. if ( count( $tokens ) == 0 ) {
  1096. return false;
  1097. }
  1098. return $tokens;
  1099. }
  1100. /**
  1101. * Add a token to a post
  1102. * @param mixed $token The name of the permission to add, or an array of permissions to add
  1103. */
  1104. public function add_tokens( $tokens )
  1105. {
  1106. $this->get_tokens();
  1107. $tokens = Utils::single_array( $tokens );
  1108. $tokens = array_map( array( 'ACL', 'token_id' ), $tokens );
  1109. $add_tokens = array_diff( $tokens, $this->tokens );
  1110. $add_tokens = array_unique( $add_tokens );
  1111. foreach ( $add_tokens as $token_id ) {
  1112. DB::insert( '{post_tokens}', array( 'post_id' => $this->id, 'token_id' => $token_id ) );
  1113. }
  1114. $this->tokens = array_merge( $this->tokens, $add_tokens );
  1115. $this->tokens = array_unique( $this->tokens );
  1116. }
  1117. /**
  1118. * Deletes all tokens from a post
  1119. */
  1120. public function delete_tokens()
  1121. {
  1122. DB::delete( '{post_tokens}', array( 'post_id' => $this->id ) );
  1123. $this->tokens = array();
  1124. }
  1125. /**
  1126. * Deletes tokens from a post
  1127. * @param mixed $token The name of the permission to remove, or an array of permissions to remove
  1128. */
  1129. public function remove_tokens( $tokens )
  1130. {
  1131. $this->get_tokens();
  1132. $tokens = Utils::single_array( $tokens );
  1133. $tokens = array_map( array( 'ACL', 'token_id' ), $tokens );
  1134. $remove_tokens = array_intersect( $tokens, $this->tokens );
  1135. foreach ( $remove_tokens as $token_id ) {
  1136. DB::delete( '{post_tokens}', array( 'post_id' => $this->id, 'token_id' => $token_id ) );
  1137. }
  1138. $this->tokens = array_diff( $this->tokens, $remove_tokens );
  1139. }
  1140. /**
  1141. * Applies a new set of specific tokens to a post
  1142. * @param mixed $tokens A string token, or an array of tokens to apply to this post
  1143. */
  1144. public function set_tokens( $tokens )
  1145. {
  1146. $tokens = Utils::single_array( $tokens );
  1147. $new_tokens = array_map( array( 'ACL', 'token_id' ), $tokens );
  1148. $new_tokens = array_unique( $new_tokens );
  1149. DB::delete( '{post_tokens}', array( 'post_id' => $this->id ) );
  1150. foreach ( $new_tokens as $token_id ) {
  1151. DB::insert( '{post_tokens}', array( 'post_id' => $this->id, 'token_id' => $token_id ) );
  1152. }
  1153. $this->tokens = $new_tokens;
  1154. }
  1155. /**
  1156. * Returns an array of token ids that are associated with this post
  1157. * Also initializes the internal token array for use by other token operations
  1158. *
  1159. * @return array An array of token ids
  1160. */
  1161. public function get_tokens()
  1162. {
  1163. if ( empty( $this->tokens ) ) {
  1164. $this->tokens = DB::get_column( 'SELECT token_id FROM {post_tokens} WHERE post_id = ?', array( $this->id ) );
  1165. }
  1166. return $this->tokens;
  1167. }
  1168. /**
  1169. * Returns an access Bitmask for the given user on this post
  1170. * @param User $user The user mask to fetch
  1171. * @return Bitmask
  1172. */
  1173. public function get_access( $user = null )
  1174. {
  1175. if ( ! $user instanceof User ) {
  1176. $user = User::identify();
  1177. }
  1178. if ( $user->can( 'super_user' ) ) {
  1179. return ACL::get_bitmask( 'full' );
  1180. }
  1181. // Collect a list of applicable tokens
  1182. $tokens = array(
  1183. 'post_any',
  1184. 'post_' . Post::type_name( $this->content_type ),
  1185. );
  1186. if ( $user->id == $this->user_id ) {
  1187. $tokens[] = 'own_posts';
  1188. }
  1189. $tokens = array_merge( $tokens, $this->get_tokens() );
  1190. // collect all possible token accesses on this post
  1191. $token_accesses = array();
  1192. foreach ( $tokens as $token ) {
  1193. $access = ACL::get_user_token_access( $user, $token );
  1194. if ( $access instanceof Bitmask ) {
  1195. $token_accesses[] = ACL::get_user_token_access( $user, $token )->value;
  1196. }
  1197. }
  1198. // now that we have all the accesses, loop through them to build the access to the particular post
  1199. if ( in_array( 0, $token_accesses ) ) {
  1200. return ACL::get_bitmask( 0 );
  1201. }
  1202. return ACL::get_bitmask( Utils::array_or( $token_accesses ) );
  1203. }
  1204. }
  1205. ?>