PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/theme.php

https://github.com/HabariMag/habarimag-old
PHP | 1337 lines | 839 code | 148 blank | 350 comment | 136 complexity | d9129a50cff703caec8367ab70066af1 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * @package Habari
  4. *
  5. */
  6. /**
  7. * Habari Theme Class
  8. *
  9. * The Theme class is the behind-the-scenes representation of
  10. * of a set of UI files that compose the visual theme of the blog
  11. *
  12. */
  13. class Theme extends Pluggable
  14. {
  15. public $name = null;
  16. public $version = null;
  17. public $template_engine = null;
  18. public $theme_dir = null;
  19. public $config_vars = array();
  20. private $var_stack = array( array() );
  21. private $current_var_stack = 0;
  22. /**
  23. * We build the Post filters by analyzing the handler_var
  24. * data which is assigned to the handler ( by the Controller and
  25. * also, optionally, by the Theme )
  26. */
  27. public $valid_filters = array(
  28. 'content_type',
  29. 'not:content_type',
  30. 'slug',
  31. 'not:slug',
  32. 'user_id',
  33. 'vocabulary',
  34. 'status',
  35. 'page',
  36. 'tag',
  37. 'not:tag',
  38. 'month',
  39. 'year',
  40. 'day',
  41. 'criteria',
  42. 'limit',
  43. 'nolimit',
  44. 'offset',
  45. 'fetch_fn',
  46. 'id',
  47. 'info',
  48. 'has:info',
  49. 'all:info',
  50. 'any:info',
  51. 'not:info',
  52. 'not:all:info',
  53. 'not:any:info',
  54. );
  55. /**
  56. * Constructor for theme
  57. *
  58. * If no parameter is supplied, then the constructor
  59. * Loads the active theme from the database.
  60. *
  61. * If no theme option is set, a fatal error is thrown
  62. *
  63. * @param name ( optional ) override the default theme lookup
  64. * @param template_engine ( optional ) specify a template engine
  65. * @param theme_dir ( optional ) specify a theme directory
  66. */
  67. public function __construct( $themedata )
  68. {
  69. $this->name = $themedata->name;
  70. $this->version = $themedata->version;
  71. $this->theme_dir = $themedata->theme_dir;
  72. // Set up the corresponding engine to handle the templating
  73. $this->template_engine = new $themedata->template_engine();
  74. $this->template_engine->set_template_dir( $themedata->theme_dir );
  75. $this->plugin_id = $this->plugin_id();
  76. $this->load();
  77. }
  78. /**
  79. * Loads a theme's metadata from an XML file in theme's
  80. * directory.
  81. *
  82. */
  83. public function info()
  84. {
  85. $xml_file = $this->theme_dir . '/theme.xml';
  86. if(!file_exists($xml_file)) {
  87. return new SimpleXMLElement('<?xml version="1.0" encoding="utf-8" ?>
  88. <pluggable type="theme">
  89. <name>Unknown Theme</name>
  90. <version>1.0</version>
  91. </pluggable>
  92. ');
  93. }
  94. if ( $xml_content = file_get_contents( $xml_file ) ) {
  95. $theme_data = new SimpleXMLElement( $xml_content );
  96. return $theme_data;
  97. }
  98. }
  99. /**
  100. * Assign the default variables that would be used in every template
  101. */
  102. public function add_template_vars()
  103. {
  104. if ( !$this->template_engine->assigned( 'user' ) ) {
  105. $this->assign( 'user', User::identify() );
  106. }
  107. if ( !$this->template_engine->assigned( 'loggedin' ) ) {
  108. $this->assign( 'loggedin', User::identify()->loggedin );
  109. }
  110. if ( !$this->template_engine->assigned( 'page' ) ) {
  111. $this->assign( 'page', isset( $this->page ) ? $this->page : 1 );
  112. }
  113. $handler = Controller::get_handler();
  114. if ( isset( $handler ) ) {
  115. Plugins::act( 'add_template_vars', $this, $handler->handler_vars );
  116. }
  117. }
  118. /**
  119. * Find the first template that matches from the list provided and display it
  120. * @param array $template_list The list of templates to search for
  121. */
  122. public function display_fallback( $template_list, $display_function = 'display' )
  123. {
  124. foreach ( (array)$template_list as $template ) {
  125. if ( $this->template_exists( $template ) ) {
  126. $this->assign( '_template_list', $template_list );
  127. $this->assign( '_template', $template );
  128. return $this->$display_function( $template );
  129. }
  130. }
  131. return false;
  132. }
  133. /**
  134. * Determine if a template exists in the current theme
  135. *
  136. * @param string $template_name The name of the template to detect
  137. * @return boolean True if template exists
  138. */
  139. public function template_exists( $template_name )
  140. {
  141. return $this->template_engine->template_exists( $template_name );
  142. }
  143. /**
  144. * Grabs post data and inserts that data into the internal
  145. * handler_vars array, which eventually gets extracted into
  146. * the theme's ( and thereby the template_engine's ) local
  147. * symbol table for use in the theme's templates
  148. *
  149. * This is the default, generic function to grab posts. To
  150. * "filter" the posts retrieved, simply pass any filters to
  151. * the handler_vars variables associated with the post retrieval.
  152. * For instance, to filter by tag, ensure that handler_vars['tag']
  153. * contains the tag to filter by. Simple as that.
  154. */
  155. public function act_display( $paramarray = array( 'user_filters'=> array() ) )
  156. {
  157. Utils::check_request_method( array( 'GET', 'HEAD', 'POST' ) );
  158. // Get any full-query parameters
  159. $possible = array( 'user_filters', 'fallback', 'posts', 'post', 'content_type' );
  160. foreach ( $possible as $varname ) {
  161. if ( isset( $paramarray[$varname] ) ) {
  162. $$varname = $paramarray[$varname];
  163. }
  164. }
  165. $where_filters = array();
  166. $where_filters = Controller::get_handler()->handler_vars->filter_keys( $this->valid_filters );
  167. $where_filters['vocabulary'] = array();
  168. if ( array_key_exists( 'tag', $where_filters ) ) {
  169. $tags = Tags::parse_url_tags( $where_filters['tag'] );
  170. $not_tag = $tags['exclude_tag'];
  171. $all_tag = $tags['include_tag'];
  172. if ( count( $not_tag ) > 0 ) {
  173. $where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':not:term' => $not_tag ) );
  174. }
  175. if ( count( $all_tag ) > 0 ) {
  176. $where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':all:term' => $all_tag ) );
  177. }
  178. $where_filters['tag_slug'] = Utils::slugify( $where_filters['tag'] );
  179. unset( $where_filters['tag'] );
  180. }
  181. if ( !isset( $_GET['preview'] ) ) {
  182. $where_filters['status'] = Post::status( 'published' );
  183. }
  184. if ( !isset( $posts ) ) {
  185. $user_filters = Plugins::filter( 'template_user_filters', $user_filters );
  186. $user_filters = array_intersect_key( $user_filters, array_flip( $this->valid_filters ) );
  187. // Work around the tags parameters to Posts::get() being subsumed by the vocabulary parameter
  188. if( isset( $user_filters['not:tag'] ) ) {
  189. $user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':not:term' => $user_filters['not:tag'] );
  190. unset( $user_filters['not:tag'] );
  191. }
  192. if( isset( $user_filters['tag'] ) ) {
  193. $user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':term_display' => $user_filters['tag'] );
  194. unset( $user_filters['tag'] );
  195. }
  196. $where_filters = $where_filters->merge( $user_filters );
  197. $where_filters = Plugins::filter( 'template_where_filters', $where_filters );
  198. $posts = Posts::get( $where_filters );
  199. }
  200. $this->assign( 'posts', $posts );
  201. if ( $posts !== false && count( $posts ) > 0 ) {
  202. if ( count( $posts ) == 1 ) {
  203. $post = $posts instanceof Post ? $posts : reset( $posts );
  204. Stack::add( 'body_class', Post::type_name( $post->content_type ) . '-' . $post->id );
  205. }
  206. else {
  207. $post = reset( $posts );
  208. Stack::add( 'body_class', 'multiple' );
  209. }
  210. $this->assign( 'post', $post );
  211. $type = Post::type_name( $post->content_type );
  212. }
  213. elseif ( ( $posts === false ) ||
  214. ( isset( $where_filters['page'] ) && $where_filters['page'] > 1 && count( $posts ) == 0 ) ) {
  215. if ( $this->template_exists( '404' ) ) {
  216. $fallback = array( '404' );
  217. // Replace template variables with the 404 rewrite rule
  218. $this->request->{URL::get_matched_rule()->name} = false;
  219. $this->request->{URL::set_404()->name} = true;
  220. $this->matched_rule = URL::get_matched_rule();
  221. // 404 status header sent in act_display_404, but we're past
  222. // that, so send it now.
  223. header( 'HTTP/1.1 404 Not Found', true, 404 );
  224. }
  225. else {
  226. $this->display( 'header' );
  227. echo '<h2>';
  228. _e( "Whoops! 404. The page you were trying to access is not really there. Please try again." );
  229. echo '</h2>';
  230. header( 'HTTP/1.1 404 Not Found', true, 404 );
  231. $this->display( 'footer' );
  232. die;
  233. }
  234. }
  235. $extract = $where_filters->filter_keys( 'page', 'type', 'id', 'slug', 'posttag', 'year', 'month', 'day', 'tag', 'tag_slug' );
  236. foreach ( $extract as $key => $value ) {
  237. $$key = $value;
  238. }
  239. $this->assign( 'page', isset( $page )? $page:1 );
  240. if ( !isset( $fallback ) ) {
  241. // Default fallbacks based on the number of posts
  242. $fallback = array( '{$type}.{$id}', '{$type}.{$slug}', '{$type}.tag.{$posttag}' );
  243. if ( count( $posts ) > 1 ) {
  244. $fallback[] = '{$type}.multiple';
  245. $fallback[] = 'multiple';
  246. }
  247. else {
  248. $fallback[] = '{$type}.single';
  249. $fallback[] = 'single';
  250. }
  251. }
  252. $searches = array( '{$id}','{$slug}','{$year}','{$month}','{$day}','{$type}','{$tag}', );
  253. $replacements = array(
  254. ( isset( $post ) && $post instanceof Post ) ? $post->id : '-',
  255. ( isset( $post ) && $post instanceof Post ) ? $post->slug : '-',
  256. isset( $year ) ? $year : '-',
  257. isset( $month ) ? $month : '-',
  258. isset( $day ) ? $day : '-',
  259. isset( $type ) ? $type : '-',
  260. isset( $tag_slug ) ? $tag_slug : '-',
  261. );
  262. $fallback[] = 'home';
  263. $fallback = Plugins::filter( 'template_fallback', $fallback, $posts, isset( $post ) ? $post : null );
  264. $fallback = array_values( array_unique( MultiByte::str_replace( $searches, $replacements, $fallback ) ) );
  265. for ( $z = 0; $z < count( $fallback ); $z++ ) {
  266. if ( ( MultiByte::strpos( $fallback[$z], '{$posttag}' ) !== false ) && ( isset( $post ) ) && ( $post instanceof Post ) ) {
  267. $replacements = array();
  268. if ( $alltags = $post->tags ) {
  269. foreach ( $alltags as $current_tag ) {
  270. $replacements[] = MultiByte::str_replace( '{$posttag}', $current_tag->term, $fallback[$z] );
  271. }
  272. array_splice( $fallback, $z, 1, $replacements );
  273. }
  274. else {
  275. break;
  276. }
  277. }
  278. }
  279. return $this->display_fallback( $fallback );
  280. }
  281. /**
  282. * Helper function: Displays the home page
  283. * @param array $user_filters Additional arguments used to get the page content
  284. */
  285. public function act_display_home( $user_filters = array() )
  286. {
  287. $paramarray['fallback'] = array(
  288. 'home',
  289. 'multiple',
  290. );
  291. // Makes sure home displays only entries
  292. $default_filters = array(
  293. 'content_type' => Post::type( 'entry' ),
  294. );
  295. $paramarray['user_filters'] = array_merge( $default_filters, $user_filters );
  296. return $this->act_display( $paramarray );
  297. }
  298. /**
  299. * Helper function: Displays multiple entries
  300. * @param array $user_filters Additional arguments used to get the page content
  301. */
  302. public function act_display_entries( $user_filters = array() )
  303. {
  304. $paramarray['fallback'] = array(
  305. '{$type}.multiple',
  306. 'multiple',
  307. );
  308. // Makes sure home displays only entries
  309. $default_filters = array(
  310. 'content_type' => Post::type( 'entry' ),
  311. );
  312. $paramarray['user_filters'] = array_merge( $default_filters, $user_filters );
  313. return $this->act_display( $paramarray );
  314. }
  315. /**
  316. * Helper function: Display a post
  317. * @param array $user_filters Additional arguments used to get the page content
  318. */
  319. public function act_display_post( $user_filters = array() )
  320. {
  321. $paramarray['fallback'] = array(
  322. '{$type}.{$id}',
  323. '{$type}.{$slug}',
  324. '{$type}.tag.{$posttag}',
  325. '{$type}.single',
  326. '{$type}.multiple',
  327. 'single',
  328. 'multiple',
  329. );
  330. // Does the same as a Post::get()
  331. $default_filters = array(
  332. 'fetch_fn' => 'get_row',
  333. 'limit' => 1,
  334. );
  335. // Remove the page from filters.
  336. $page_key = array_search( 'page', $this->valid_filters );
  337. unset( $this->valid_filters[$page_key] );
  338. $paramarray['user_filters'] = array_merge( $default_filters, $user_filters );
  339. return $this->act_display( $paramarray );
  340. }
  341. /**
  342. * Helper function: Display the posts for a tag
  343. * @param array $user_filters Additional arguments used to get the page content
  344. */
  345. public function act_display_tag( $user_filters = array() )
  346. {
  347. $paramarray['fallback'] = array(
  348. 'tag.{$tag}',
  349. 'tag',
  350. 'multiple',
  351. );
  352. // Makes sure home displays only entries
  353. $default_filters = array(
  354. 'content_type' => Post::type( 'entry' ),
  355. );
  356. $this->assign( 'tag', Controller::get_var( 'tag' ) );
  357. // Assign tag objects to the theme
  358. $tags = Tags::parse_url_tags( Controller::get_var( 'tag' ), true );
  359. $this->assign( 'include_tag', $tags['include_tag'] );
  360. $this->assign( 'exclude_tag', $tags['exclude_tag'] );
  361. $paramarray['user_filters'] = array_merge( $default_filters, $user_filters );
  362. return $this->act_display( $paramarray );
  363. }
  364. /**
  365. * Helper function: Display the posts for a specific date
  366. * @param array $user_filters Additional arguments used to get the page content
  367. */
  368. public function act_display_date( $user_filters = array() )
  369. {
  370. $handler_vars = Controller::get_handler()->handler_vars;
  371. $y = isset( $handler_vars['year'] );
  372. $m = isset( $handler_vars['month'] );
  373. $d = isset( $handler_vars['day'] );
  374. if ( $y && $m && $d ) {
  375. $paramarray['fallback'][] = 'year.{$year}.month.{$month}.day.{$day}';
  376. }
  377. if ( $y && $m && $d ) {
  378. $paramarray['fallback'][] = 'year.month.day';
  379. }
  380. if ( $m && $d ) {
  381. $paramarray['fallback'][] = 'month.{$month}.day.{$day}';
  382. }
  383. if ( $y && $m ) {
  384. $paramarray['fallback'][] = 'year.{$year}.month.{$month}';
  385. }
  386. if ( $y && $d ) {
  387. $paramarray['fallback'][] = 'year.{$year}.day.{$day}';
  388. }
  389. if ( $m && $d ) {
  390. $paramarray['fallback'][] = 'month.day';
  391. }
  392. if ( $y && $d ) {
  393. $paramarray['fallback'][] = 'year.day';
  394. }
  395. if ( $y && $m ) {
  396. $paramarray['fallback'][] = 'year.month';
  397. }
  398. if ( $m ) {
  399. $paramarray['fallback'][] = 'month.{$month}';
  400. }
  401. if ( $d ) {
  402. $paramarray['fallback'][] = 'day.{$day}';
  403. }
  404. if ( $y ) {
  405. $paramarray['fallback'][] = 'year.{$year}';
  406. }
  407. if ( $y ) {
  408. $paramarray['fallback'][] = 'year';
  409. }
  410. if ( $m ) {
  411. $paramarray['fallback'][] = 'month';
  412. }
  413. if ( $d ) {
  414. $paramarray['fallback'][] = 'day';
  415. }
  416. $paramarray['fallback'][] = 'date';
  417. $paramarray['fallback'][] = 'multiple';
  418. $paramarray['fallback'][] = 'home';
  419. $paramarray['user_filters'] = $user_filters;
  420. if ( !isset( $paramarray['user_filters']['content_type'] ) ) {
  421. $paramarray['user_filters']['content_type'] = Post::type( 'entry' );
  422. }
  423. $this->assign( 'year', $y ? (int)Controller::get_var( 'year' ) : null );
  424. $this->assign( 'month', $m ? (int)Controller::get_var( 'month' ) : null );
  425. $this->assign( 'day', $d ? (int)Controller::get_var( 'day' ) : null );
  426. return $this->act_display( $paramarray );
  427. }
  428. /**
  429. * Helper function: Display the posts for a specific criteria
  430. * @param array $user_filters Additional arguments used to get the page content
  431. */
  432. public function act_search( $user_filters = array() )
  433. {
  434. $paramarray['fallback'] = array(
  435. 'search',
  436. 'multiple',
  437. );
  438. $paramarray['user_filters'] = $user_filters;
  439. $this->assign( 'criteria', Controller::get_var( 'criteria' ) );
  440. return $this->act_display( $paramarray );
  441. }
  442. /**
  443. * Helper function: Display a 404 template
  444. *
  445. * @param array $user_filters Additional arguments user to get the page content
  446. */
  447. public function act_display_404( $user_filters = array() )
  448. {
  449. $paramarray['fallback'] = array(
  450. '404',
  451. );
  452. header( 'HTTP/1.1 404 Not Found' );
  453. $paramarray['user_filters'] = $user_filters;
  454. return $this->act_display( $paramarray );
  455. }
  456. /**
  457. * Helper function: Avoids having to call $theme->template_engine->display( 'template_name' );
  458. * @param string $template_name The name of the template to display
  459. */
  460. public function display( $template_name )
  461. {
  462. $this->add_template_vars();
  463. $this->play_var_stack();
  464. $this->template_engine->assign( 'theme', $this );
  465. $this->template_engine->display( $template_name );
  466. }
  467. /**
  468. * Helper function: Avoids having to call $theme->template_engine->fetch( 'template_name' );
  469. *
  470. * @param string $template_name The name of the template to display
  471. * @param boolean $unstack If true, end the current template variable buffer upon returning
  472. * @return string The content of the template
  473. */
  474. public function fetch( $template_name, $unstack = false )
  475. {
  476. $this->play_var_stack();
  477. $this->add_template_vars();
  478. $this->template_engine->assign( 'theme', $this );
  479. $return = $this->fetch_unassigned( $template_name );
  480. if ( $unstack ) {
  481. $this->end_buffer();
  482. }
  483. return $return;
  484. }
  485. /**
  486. * Play back the full stack of template variables to assign them into the template
  487. */
  488. protected function play_var_stack()
  489. {
  490. $this->template_engine->clear();
  491. for ( $z = 0; $z <= $this->current_var_stack; $z++ ) {
  492. foreach ( $this->var_stack[$z] as $key => $value ) {
  493. $this->template_engine->assign( $key, $value );
  494. }
  495. }
  496. }
  497. /**
  498. * Calls the template engine's fetch() method without pre-assigning template variables.
  499. * Assumes that the template variables have already been set.
  500. *
  501. * @param string $template_name The name of the template to display
  502. * @return string The content of the template
  503. */
  504. public function fetch_unassigned( $template_name )
  505. {
  506. return $this->template_engine->fetch( $template_name );
  507. }
  508. /**
  509. * Helper function: Avoids having to call $theme->template_engine->key= 'value';
  510. */
  511. public function assign( $key, $value )
  512. {
  513. $this->var_stack[$this->current_var_stack][$key] = $value;
  514. }
  515. /**
  516. * Aggregates and echos the additional header code by combining Plugins and Stack calls.
  517. */
  518. public function theme_header( $theme )
  519. {
  520. // create a stack of the atom tags before the first action so they can be unset if desired
  521. Stack::add( 'template_atom', array( 'alternate', 'application/atom+xml', 'Atom 1.0', implode( '', $this->feed_alternate_return() ) ), 'atom' );
  522. Stack::add( 'template_atom', array( 'edit', 'application/atom+xml', 'Atom Publishing Protocol', URL::get( 'atompub_servicedocument' ) ), 'app' );
  523. Stack::add( 'template_atom', array( 'EditURI', 'application/rsd+xml', 'RSD', URL::get( 'rsd' ) ), 'rsd' );
  524. Plugins::act( 'template_header', $theme );
  525. $atom = Stack::get( 'template_atom', '<link rel="%1$s" type="%2$s" title="%3$s" href="%4$s">' );
  526. $styles = Stack::get( 'template_stylesheet', array( 'Stack', 'styles' ) );
  527. $scripts = Stack::get( 'template_header_javascript', array( 'Stack', 'scripts' ) );
  528. $output = implode( "\n", array( $atom, $styles, $scripts ) );
  529. Plugins::act( 'template_header_after', $theme );
  530. return $output;
  531. }
  532. /**
  533. * Aggregates and echos the additional footer code by combining Plugins and Stack calls.
  534. */
  535. public function theme_footer( $theme )
  536. {
  537. Plugins::act( 'template_footer', $theme );
  538. $output = Stack::get( 'template_footer_stylesheet', array( 'Stack', 'styles' ) );
  539. $output .= Stack::get( 'template_footer_javascript', array( 'Stack', 'scripts' ) );
  540. return $output;
  541. }
  542. /**
  543. * Display an object using a template designed for the type of object it is
  544. * The $object is assigned into the theme using the $content template variable
  545. *
  546. * @param Theme $theme The theme used to display the object
  547. * @param object $object An object to display
  548. * @param string $context The context in which the object will be displayed
  549. * @return
  550. */
  551. public function theme_content( $theme, $object, $context = null )
  552. {
  553. $fallback = array();
  554. $content_types = array();
  555. if ( $object instanceof IsContent ) {
  556. $content_types = Utils::single_array( $object->content_type() );
  557. }
  558. if ( is_object( $object ) ) {
  559. $content_types[] = strtolower( get_class( $object ) );
  560. }
  561. $content_types[] = 'content';
  562. $content_types = array_flip( $content_types );
  563. if ( isset( $context ) ) {
  564. foreach ( $content_types as $type => $type_id ) {
  565. $content_type = $context . $object->content_type();
  566. $fallback[] = strtolower( $context . '.' . $type );
  567. }
  568. }
  569. foreach ( $content_types as $type => $type_id ) {
  570. $fallback[] = strtolower( $type );
  571. }
  572. if ( isset( $context ) ) {
  573. $fallback[] = strtolower( $context );
  574. }
  575. $fallback = array_unique( $fallback );
  576. $this->content = $object;
  577. return $this->display_fallback( $fallback, 'fetch' );
  578. }
  579. /**
  580. * Returns the appropriate alternate feed based on the currently matched rewrite rule.
  581. *
  582. * @param mixed $return Incoming return value from other plugins
  583. * @param Theme $theme The current theme object
  584. * @return string Link to the appropriate alternate Atom feed
  585. */
  586. public function theme_feed_alternate( $theme )
  587. {
  588. $matched_rule = URL::get_matched_rule();
  589. if ( is_object( $matched_rule ) ) {
  590. // This is not a 404
  591. $rulename = $matched_rule->name;
  592. }
  593. else {
  594. // If this is a 404 and no rewrite rule matched the request
  595. $rulename = '';
  596. }
  597. switch ( $rulename ) {
  598. case 'display_entry':
  599. case 'display_page':
  600. return URL::get( 'atom_entry', array( 'slug' => Controller::get_var( 'slug' ) ) );
  601. break;
  602. case 'display_entries_by_tag':
  603. return URL::get( 'atom_feed_tag', array( 'tag' => Controller::get_var( 'tag' ) ) );
  604. break;
  605. case 'display_home':
  606. default:
  607. return URL::get( 'atom_feed', array( 'index' => '1' ) );
  608. }
  609. return '';
  610. }
  611. /**
  612. * Returns the feedback URL to which comments should be submitted for the indicated Post
  613. *
  614. * @param Theme $theme The current theme
  615. * @param Post $post The post object to get the feedback URL for
  616. * @return string The URL to the feedback entrypoint for this comment
  617. */
  618. public function theme_comment_form_action( $theme, $post )
  619. {
  620. return URL::get( 'submit_feedback', array( 'id' => $post->id ) );
  621. }
  622. /**
  623. * Build a collection of paginated URLs to be used for pagination.
  624. *
  625. * @param string The RewriteRule name used to build the links.
  626. * @param array Various settings used by the method and the RewriteRule.
  627. * @return string Collection of paginated URLs built by the RewriteRule.
  628. */
  629. public static function theme_page_selector( $theme, $rr_name = null, $settings = array() )
  630. {
  631. // We can't detect proper pagination if $theme->posts isn't a Posts object,
  632. // so if it's not, bail.
  633. if(!$theme->posts instanceof Posts) {
  634. return '';
  635. }
  636. $current = $theme->page;
  637. $items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ?
  638. $theme->posts->get_param_cache['limit'] :
  639. Options::get( 'pagination' );
  640. $total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page );
  641. // Make sure the current page is valid
  642. if ( $current > $total ) {
  643. $current = $total;
  644. }
  645. else if ( $current < 1 ) {
  646. $current = 1;
  647. }
  648. // Number of pages to display on each side of the current page.
  649. $leftSide = isset( $settings['leftSide'] ) ? $settings['leftSide'] : 1;
  650. $rightSide = isset( $settings['rightSide'] ) ? $settings['rightSide'] : 1;
  651. // Add the page '1'.
  652. $pages[] = 1;
  653. // Add the pages to display on each side of the current page, based on $leftSide and $rightSide.
  654. for ( $i = max( $current - $leftSide, 2 ); $i < $total && $i <= $current + $rightSide; $i++ ) {
  655. $pages[] = $i;
  656. }
  657. // Add the last page if there is more than one page.
  658. if ( $total > 1 ) {
  659. $pages[] = (int) $total;
  660. }
  661. // Sort the array by natural order.
  662. natsort( $pages );
  663. // This variable is used to know the last page processed by the foreach().
  664. $prevpage = 0;
  665. // Create the output variable.
  666. $out = '';
  667. if ( 1 === count( $pages ) && isset( $settings['hideIfSinglePage'] ) && $settings['hideIfSinglePage'] === true ) {
  668. return '';
  669. }
  670. foreach ( $pages as $page ) {
  671. $settings['page'] = $page;
  672. // Add ... if the gap between the previous page is higher than 1.
  673. if ( ( $page - $prevpage ) > 1 ) {
  674. $out .= '&nbsp;<span class="sep">&hellip;</span>';
  675. }
  676. // Wrap the current page number with square brackets.
  677. $caption = ( $page == $current ) ? $current : $page;
  678. // Build the URL using the supplied $settings and the found RewriteRules arguments.
  679. $url = URL::get( $rr_name, $settings, false );
  680. // Build the HTML link.
  681. $out .= '&nbsp;<a href="' . $url . '" ' . ( ( $page == $current ) ? 'class="current-page"' : '' ) . '>' . $caption . '</a>';
  682. $prevpage = $page;
  683. }
  684. return $out;
  685. }
  686. /**
  687. *Provides a link to the previous page
  688. *
  689. * @param string $text text to display for link
  690. */
  691. public function theme_prev_page_link( $theme, $text = null )
  692. {
  693. $settings = array();
  694. // If there's no previous page, skip and return null
  695. $settings['page'] = (int) ( $theme->page - 1 );
  696. if ( $settings['page'] < 1 ) {
  697. return null;
  698. }
  699. // If no text was supplied, use default text
  700. if ( $text == '' ) {
  701. $text = '&larr; ' . _t( 'Previous' );
  702. }
  703. return '<a class="prev-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>';
  704. }
  705. /**
  706. *Provides a link to the next page
  707. *
  708. * @param string $text text to display for link
  709. */
  710. public function theme_next_page_link( $theme, $text = null )
  711. {
  712. $settings = array();
  713. // If there's no next page, skip and return null
  714. $settings['page'] = (int) ( $theme->page + 1 );
  715. $items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ?
  716. $theme->posts->get_param_cache['limit'] :
  717. Options::get( 'pagination' );
  718. $total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page );
  719. if ( $settings['page'] > $total ) {
  720. return null;
  721. }
  722. // If no text was supplied, use default text
  723. if ( $text == '' ) {
  724. $text = _t( 'Next' ) . ' &rarr;';
  725. }
  726. return '<a class="next-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>';
  727. }
  728. /**
  729. * Returns a full qualified URL of the specified post based on the comments count, and links to the post.
  730. *
  731. * Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires".
  732. *
  733. * Since we use sprintf() in the final concatenation, you must format passed strings accordingly.
  734. *
  735. * @param Theme $theme The current theme object
  736. * @param Post $post Post object used to build the comments link
  737. * @param string $zero String to return when there are no comments
  738. * @param string $one String to return when there is one comment
  739. * @param string $many String to return when there are more than one comment
  740. * @param string $fragment Fragment (bookmark) portion of the URL to append to the link
  741. * @param string $title Fragment (bookmark) portion of the URL to append to the link
  742. * @return string Linked string to display for comment count
  743. * @see Theme::theme_comments_count()
  744. */
  745. public function theme_comments_link( $theme, $post, $zero = '', $one = '', $many = '', $fragment = 'comments' )
  746. {
  747. $count = $theme->comments_count_return( $post, $zero, $one, $many );
  748. return '<a href="' . $post->permalink . '#' . $fragment . '" title="' . _t( 'Read Comments' ) . '">' . end( $count ) . '</a>';
  749. }
  750. /**
  751. * Returns a full qualified URL of the specified post based on the comments count.
  752. *
  753. * Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires".
  754. *
  755. * Since we use sprintf() in the final concatenation, you must format passed strings accordingly.
  756. *
  757. * @param Theme $theme The current theme object
  758. * @param Post $post Post object used to build the comments link
  759. * @param string $zero String to return when there are no comments
  760. * @param string $one String to return when there is one comment
  761. * @param string $many String to return when there are more than one comment
  762. * @return string String to display for comment count
  763. */
  764. public function theme_comments_count( $theme, $post, $zero = '', $one = '', $many = '' )
  765. {
  766. $count = $post->comments->approved->count;
  767. if ( $count == 0 ) {
  768. $text = empty( $zero ) ? _t( 'No Comments' ) : $zero;
  769. return sprintf( $text, $count );
  770. }
  771. else {
  772. if ( empty( $one ) && empty( $many ) ) {
  773. $text = _n( '%s Comment', '%s Comments', $count );
  774. }
  775. else {
  776. if ( empty( $one ) ) {
  777. $one = $many;
  778. }
  779. if ( empty( $many ) ) {
  780. $many = $one;
  781. }
  782. $text = $count == 1 ? $one : $many;
  783. }
  784. return sprintf( $text, $count );
  785. }
  786. }
  787. /**
  788. * Returns the count of queries executed
  789. *
  790. * @return integer The query count
  791. */
  792. public function theme_query_count()
  793. {
  794. return count( DB::get_profiles() );
  795. }
  796. /**
  797. * Returns total query execution time in seconds
  798. *
  799. * @return float Query execution time in seconds, with fractions.
  800. */
  801. public function theme_query_time()
  802. {
  803. return array_sum( array_map( create_function( '$a', 'return $a->total_time;' ), DB::get_profiles() ) );
  804. }
  805. /**
  806. * Returns a humane commenter's link for a comment if a URL is supplied, or just display the comment author's name
  807. *
  808. * @param Theme $theme The current theme
  809. * @param Comment $comment The comment object
  810. * @return string A link to the comment author or the comment author's name with no link
  811. */
  812. public function theme_comment_author_link( $theme, $comment )
  813. {
  814. $url = $comment->url;
  815. if ( $url != '' ) {
  816. $parsed_url = InputFilter::parse_url( $url );
  817. if ( $parsed_url['host'] == '' ) {
  818. $url = '';
  819. }
  820. else {
  821. $url = InputFilter::glue_url( $parsed_url );
  822. }
  823. }
  824. if ( $url != '' ) {
  825. return '<a href="'.$url.'">' . $comment->name . '</a>';
  826. }
  827. else {
  828. return $comment->name;
  829. }
  830. }
  831. /**
  832. * Detects if a variable is assigned to the template engine for use in
  833. * constructing the template's output.
  834. *
  835. * @param key name of variable
  836. * @returns boolean true if name is set, false if not set
  837. */
  838. public function __isset( $key )
  839. {
  840. return isset( $this->var_stack[$this->current_var_stack][$key] );
  841. }
  842. /**
  843. * Set a template variable, a property alias for assign()
  844. *
  845. * @param string $key The template variable to set
  846. * @param mixed $value The value of the variable
  847. */
  848. public function __set( $key, $value )
  849. {
  850. $this->assign( $key, $value );
  851. }
  852. /**
  853. * Get a template variable value
  854. *
  855. * @param string $key The template variable name to get
  856. * @return mixed The value of the variable
  857. */
  858. public function __get( $key )
  859. {
  860. if ( isset( $this->var_stack[$this->current_var_stack][$key] ) ) {
  861. return $this->var_stack[$this->current_var_stack][$key];
  862. }
  863. return '';
  864. }
  865. /**
  866. * Remove a template variable value
  867. *
  868. * @param string $key The template variable name to unset
  869. */
  870. public function __unset( $key )
  871. {
  872. unset( $this->var_stack[$this->current_var_stack][$key] );
  873. }
  874. /**
  875. * Start a new template variable buffer
  876. */
  877. public function start_buffer()
  878. {
  879. $this->current_var_stack++;
  880. $this->var_stack[$this->current_var_stack] = $this->var_stack[$this->current_var_stack - 1];
  881. }
  882. /**
  883. * End the current template variable buffer
  884. */
  885. public function end_buffer()
  886. {
  887. unset( $this->var_stack[$this->current_var_stack] );
  888. $this->current_var_stack--;
  889. }
  890. /**
  891. * Handle methods called on this class or its descendants that are not defined by this class.
  892. * Allow plugins to provide additional theme actions, like a custom act_display_*()
  893. *
  894. * @param string $function The method that was called.
  895. * @param array $params An array of parameters passed to the method
  896. **/
  897. public function __call( $function, $params )
  898. {
  899. if ( strpos( $function, 'act_' ) === 0 ) {
  900. // The first parameter is an array, get it
  901. if ( count( $params ) > 0 ) {
  902. list( $user_filters )= $params;
  903. }
  904. else {
  905. $user_filters = array();
  906. }
  907. $action = substr( $function, 4 );
  908. Plugins::act( 'theme_action', $action, $this, $user_filters );
  909. }
  910. else {
  911. $purposed = 'output';
  912. if ( preg_match( '/^(.*)_(return|end)$/', $function, $matches ) ) {
  913. $purposed = $matches[2];
  914. $function = $matches[1];
  915. }
  916. array_unshift( $params, $function, $this );
  917. $result = call_user_func_array( array( 'Plugins', 'theme' ), $params );
  918. switch ( $purposed ) {
  919. case 'return':
  920. return $result;
  921. case 'end':
  922. echo end( $result );
  923. return end( $result );
  924. default:
  925. $output = implode( '', ( array ) $result );
  926. echo $output;
  927. return $output;
  928. }
  929. }
  930. }
  931. /**
  932. * Retrieve the block objects for the current scope and specified area
  933. * Incomplete!
  934. *
  935. * @param string $area The area to which blocks will be output
  936. * @param string $scope The scope to which blocks will be output
  937. * @param Theme $theme The theme that is outputting these blocks
  938. * @return array An array of Block instances to render
  939. * @todo Finish this function to pull data from a block_instances table
  940. */
  941. public function get_blocks( $area, $scope, $theme )
  942. {
  943. $blocks = DB::get_results( 'SELECT b.* FROM {blocks} b INNER JOIN {blocks_areas} ba ON ba.block_id = b.id WHERE ba.area = ? AND ba.scope_id = ? ORDER BY ba.display_order ASC', array( $area, $scope ), 'Block' );
  944. $blocks = Plugins::filter( 'get_blocks', $blocks, $area, $scope, $theme );
  945. return $blocks;
  946. }
  947. /**
  948. * Matches the scope criteria against the current request
  949. *
  950. * @param array $criteria An array of scope criteria data in RPN, where values are arrays and operators are strings
  951. * @return boolean True if the criteria matches the current request
  952. */
  953. function check_scope_criteria( $criteria )
  954. {
  955. $stack = array();
  956. foreach ( $criteria as $crit ) {
  957. if ( is_array( $crit ) ) {
  958. $value = false;
  959. switch ( $crit[0] ) {
  960. case 'request':
  961. $value = URL::get_matched_rule()->name == $crit[1];
  962. break;
  963. case 'token':
  964. if ( isset( $crit[2] ) ) {
  965. $value = User::identify()->can( $crit[1], $crit[2] );
  966. }
  967. else {
  968. $value = User::identify()->can( $crit[1] );
  969. }
  970. break;
  971. default:
  972. $value = Plugins::filter( 'scope_criteria_value', $value, $crit[1], $crit[2] );
  973. break;
  974. }
  975. $stack[] = $value;
  976. }
  977. else {
  978. switch ( $crit ) {
  979. case 'not':
  980. $stack[] = ! array_pop( $stack );
  981. break;
  982. case 'or':
  983. $value1 = array_pop( $stack );
  984. $value2 = array_pop( $stack );
  985. $stack[] = $value1 || $value2;
  986. break;
  987. case 'and':
  988. $value1 = array_pop( $stack );
  989. $value2 = array_pop( $stack );
  990. $stack[] = $value1 && $value2;
  991. break;
  992. default:
  993. Plugins::act( 'scope_criteria_operator', $stack, $crit );
  994. break;
  995. }
  996. }
  997. }
  998. return array_pop( $stack );
  999. }
  1000. /**
  1001. * Retrieve current scope data from the database based on the requested area
  1002. *
  1003. * @param string $area The area for which a scope may be applied
  1004. * @return array An array of scope data
  1005. */
  1006. public function get_scopes( $area )
  1007. {
  1008. $scopes = DB::get_results( 'SELECT * FROM {scopes} s INNER JOIN {blocks_areas} ba ON ba.scope_id = s.id WHERE ba.area = ? ORDER BY s.priority DESC', array( $area ) );
  1009. foreach ( $scopes as $key => $value ) {
  1010. $scopes[$key]->criteria = unserialize( $value->criteria );
  1011. }
  1012. $scopes = Plugins::filter( 'get_scopes', $scopes );
  1013. usort( $scopes, array( $this, 'sort_scopes' ) );
  1014. return $scopes;
  1015. }
  1016. /**
  1017. * Sort function for ordering scope object rows by priority
  1018. * @param StdObject $scope1 A scope to compare
  1019. * @param StdObject $scope2 A scope to compare
  1020. * @return integer A sort return value, -1 to 1
  1021. **/
  1022. public function sort_scopes( $scope1, $scope2 )
  1023. {
  1024. if ( $scope1->priority == $scope2->priority ) {
  1025. return 0;
  1026. }
  1027. return $scope1->priority < $scope2->priority ? 1 : -1;
  1028. }
  1029. /**
  1030. * Displays blocks associated to the specified area and current scope.
  1031. *
  1032. * @param Theme $theme The theme with which this area will be output
  1033. * @param string $area The area to which blocks will be output
  1034. * @param string $context The area of context within the theme that could adjust the template used
  1035. * @param string $scope Used to force a specific scope
  1036. * @return string the output of all the blocks
  1037. */
  1038. public function theme_area( $theme, $area, $context = null, $scope = null )
  1039. {
  1040. // This array would normally come from the database via:
  1041. $scopes = $this->get_scopes( $area );
  1042. $active_scope = 0;
  1043. foreach ( $scopes as $scope_id => $scope_object ) {
  1044. if ( $this->check_scope_criteria( $scope_object->criteria ) ) {
  1045. $scope_block_count = DB::get_value( 'SELECT count( *) FROM {blocks_areas} ba WHERE ba.scope_id = ?', array( $scope_object->id ) );
  1046. if ( $scope_block_count > 0 ) {
  1047. $active_scope = $scope_object->id;
  1048. }
  1049. break;
  1050. }
  1051. }
  1052. $area_blocks = $this->get_blocks( $area, $active_scope, $theme );
  1053. $this->area = $area;
  1054. // This is the block wrapper fallback template list
  1055. $fallback = array(
  1056. $context . '.' . $area . '.blockwrapper',
  1057. $context . '.blockwrapper',
  1058. $area . '.blockwrapper',
  1059. 'blockwrapper',
  1060. 'content',
  1061. );
  1062. $output = '';
  1063. $i = 0;
  1064. foreach ( $area_blocks as $block_instance_id => $block ) {
  1065. // Temporarily set some values into the block
  1066. $block->_area = $area;
  1067. $block->_instance_id = $block_instance_id;
  1068. $block->_area_index = $i++;
  1069. $hook = 'block_content_' . $block->type;
  1070. Plugins::act( $hook, $block, $this );
  1071. Plugins::act( 'block_content', $block, $this );
  1072. $block->_content = implode( '', $this->content_return( $block, $context ) );
  1073. if ( trim( $block->_content ) == '' ) {
  1074. unset( $area_blocks[$block_instance_id] );
  1075. }
  1076. }
  1077. // Potentially render each block inside of a wrapper.
  1078. reset( $area_blocks );
  1079. $firstkey = key( $area_blocks );
  1080. end( $area_blocks );
  1081. $lastkey = key( $area_blocks );
  1082. foreach ( $area_blocks as $block_instance_id => $block ) {
  1083. $block->_first = $block_instance_id == $firstkey;
  1084. $block->_last = $block_instance_id == $lastkey;
  1085. // Set up the theme for the wrapper
  1086. $this->block = $block;
  1087. $this->content = $block->_content;
  1088. // This pattern renders the block inside the wrapper template only if a matching template exists
  1089. $newoutput = $this->display_fallback( $fallback, 'fetch' );
  1090. if ( $newoutput === false ) {
  1091. $output .= $block->_content;
  1092. }
  1093. else {
  1094. $output .= $newoutput;
  1095. }
  1096. // Remove temporary values from the block so they're not saved to the database
  1097. unset( $block->_area );
  1098. unset( $block->_instance_id );
  1099. unset( $block->_area_index );
  1100. unset( $block->_first );
  1101. unset( $block->_last );
  1102. }
  1103. // This is the area fallback template list
  1104. $fallback = array(
  1105. $context . '.area.' . $area,
  1106. $context . '.area',
  1107. 'area.' . $area,
  1108. 'area',
  1109. );
  1110. $this->content = $output;
  1111. $newoutput = $this->display_fallback( $fallback, 'fetch' );
  1112. if ( $newoutput !== false ) {
  1113. $output = $newoutput;
  1114. }
  1115. $this->area = '';
  1116. return $output;
  1117. }
  1118. /**
  1119. * A theme function for outputting CSS classes based on the requested content
  1120. * @param Theme $theme A Theme object instance
  1121. * @param mixed $args Additional classes that should be added to the ones generated
  1122. * @return string The resultant classes
  1123. */
  1124. function theme_body_class( $theme, $args = array() )
  1125. {
  1126. $body_class = array();
  1127. foreach ( get_object_vars( $this->request ) as $key => $value ) {
  1128. if ( $value ) {
  1129. $body_class[$key] = $key;
  1130. }
  1131. }
  1132. $body_class = array_unique( array_merge( $body_class, Stack::get_named_stack( 'body_class' ), Utils::single_array( $args ) ) );
  1133. $body_class = Plugins::filter( 'body_class', $body_class, $theme );
  1134. return implode( ' ', $body_class );
  1135. }
  1136. /**
  1137. * Add javascript to the stack to be output in the theme.
  1138. *
  1139. * @param string $where Where should it be output? Options are header and footer.
  1140. * @param string $value Either a URL or raw JS to be output inline.
  1141. * @param string $name A name to reference this script by. Used for removing or using in $requires by other scripts.
  1142. * @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires.
  1143. * @return boolean True if added successfully, false otherwise.
  1144. */
  1145. public function add_script ( $where = 'header', $value, $name = null, $requires = null )
  1146. {
  1147. $result = false;
  1148. switch ( $where ) {
  1149. case 'header':
  1150. $result = Stack::add( 'template_header_javascript', $value, $name, $requires );
  1151. break;
  1152. case 'footer':
  1153. $result = Stack::add( 'template_footer_javascript', $value, $name, $requires );
  1154. break;
  1155. }
  1156. return $result;
  1157. }
  1158. /**
  1159. * Add a stylesheet to the stack to be output in the theme.
  1160. *
  1161. * @param string $where Where should it be output? Options are header and footer.
  1162. * @param string $value Either a URL or raw CSS to be output inline.
  1163. * @param string $name A name to reference this script by. Used for removing or using in $after by other scripts.
  1164. * @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires.
  1165. * @return boolean True if added successfully, false otherwise.
  1166. */
  1167. public function add_style ( $where = 'header', $value, $name = null, $requires = null )
  1168. {
  1169. $result = false;
  1170. switch ( $where ) {
  1171. case 'header':
  1172. $result = Stack::add( 'template_stylesheet', $value, $name, $requires );
  1173. break;
  1174. case 'footer':
  1175. $result = Stack::add( 'template_footer_stylesheet', $value, $name, $requires );
  1176. break;
  1177. }
  1178. return $result;
  1179. }
  1180. /**
  1181. * Provide a method to return the version number from the theme xml
  1182. * @return string The theme version from XML
  1183. **/
  1184. public function get_version()
  1185. {
  1186. return (string)$this->info()->version;
  1187. }
  1188. }
  1189. ?>