1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Michael Klier <chi@chimeric.de> 5 */ 6// must be run within Dokuwiki 7if(!defined('DOKU_INC')) die(); 8 9/** 10 * Class helper_plugin_blogtng_entry 11 */ 12class helper_plugin_blogtng_entry extends DokuWiki_Plugin { 13 14 const RET_OK = 1; 15 const RET_ERR_DB = -1; 16 const RET_ERR_BADPID = -2; 17 const RET_ERR_NOENTRY = -3; 18 const RET_ERR_DEL = -4; 19 const RET_ERR_RES = -5; 20 21 /** @var array|null */ 22 public $entry = null; 23 /** @var helper_plugin_blogtng_sqlite */ 24 private $sqlitehelper = null; 25 /** @var helper_plugin_blogtng_comments */ 26 private $commenthelper = null; 27 /** @var helper_plugin_blogtng_tags */ 28 private $taghelper = null; 29 /** @var helper_plugin_blogtng_tools */ 30 private $toolshelper = null; 31 /** @var Doku_Renderer_xhtml */ 32 private $renderer = null; 33 34 /** 35 * Constructor, loads the sqlite helper plugin 36 * 37 * @author Michael Klier <chi@chimeric.de> 38 */ 39 public function __construct() { 40 $this->sqlitehelper = plugin_load('helper', 'blogtng_sqlite'); 41 $this->entry = $this->prototype(); 42 } 43 44 45 //~~ data access methods 46 47 /** 48 * Load all entries with @$pid 49 * 50 * @param string $pid 51 * @return int 52 */ 53 public function load_by_pid($pid) { 54 $this->entry = $this->prototype(); 55 $this->taghelper = null; 56 $this->commenthelper = null; 57 58 $pid = trim($pid); 59 if (!$this->is_valid_pid($pid)) { 60 msg('BlogTNG plugin: "'.$pid.'" is not a valid pid!', -1); 61 return self::RET_ERR_BADPID; 62 } 63 64 if(!$this->sqlitehelper->ready()) { 65 msg('BlogTNG plugin: failed to load sqlite helper plugin', -1); 66 return self::RET_ERR_DB; 67 } 68 $query = 'SELECT pid, page, title, blog, image, created, lastmod, author, login, mail, commentstatus 69 FROM entries 70 WHERE pid = ?'; 71 $resid = $this->sqlitehelper->getDB()->query($query, $pid); 72 if ($resid === false) { 73 msg('BlogTNG plugin: failed to load entry!', -1); 74 return self::RET_ERR_DB; 75 } 76 if ($this->sqlitehelper->getDB()->res2count($resid) == 0) { 77 $this->entry['pid'] = $pid; 78 return self::RET_ERR_NOENTRY; 79 } 80 81 $result = $this->sqlitehelper->getDB()->res2arr($resid); 82 $this->entry = $result[0]; 83 $this->entry['pid'] = $pid; 84 if($this->poke()){ 85 return self::RET_OK; 86 }else{ 87 return self::RET_ERR_DEL; 88 } 89 } 90 91 /** 92 * Sets @$row as the current entry and returns RET_OK if it references 93 * a valid blog entry. Otherwise the entry will be deleted and 94 * RET_ERR_DEL is returned. 95 * 96 * @param $row 97 * @return int 98 */ 99 public function load_by_row($row) { 100 $this->entry = $row; 101 if($this->poke()){ 102 return self::RET_OK; 103 }else{ 104 return self::RET_ERR_DEL; 105 } 106 } 107 108 /** 109 * Copy all array entries from @$entry 110 * 111 * @param $entry 112 */ 113 public function set($entry) { 114 foreach (array_keys($entry) as $key) { 115 if (!in_array($key, array('pid', 'page', 'created', 'login')) || empty($this->entry[$key])) { 116 $this->entry[$key] = $entry[$key]; 117 } 118 } 119 } 120 121 /** 122 * Create and return empty prototype array with all items set to null. 123 * 124 * @return array 125 */ 126 private function prototype() { 127 return array( 128 'pid' => null, 129 'page' => null, 130 'title' => null, 131 'blog' => null, 132 'image' => null, 133 'created' => null, 134 'lastmod' => null, 135 'author' => null, 136 'login' => null, 137 'mail' => null, 138 ); 139 } 140 141 /** 142 * Poke the entry with a stick and see if it is alive 143 * 144 * If page does not exist or is not a blog, delete DB entry 145 */ 146 public function poke(){ 147 if(!$this->entry['page'] or !page_exists($this->entry['page']) OR !$this->entry['blog']){ 148 $this->delete(); 149 return false; 150 } 151 return true; 152 } 153 154 /** 155 * Delete the current entry 156 */ 157 private function delete(){ 158 if(!$this->entry['pid']) return false; 159 if(!$this->sqlitehelper->ready()) { 160 msg('BlogTNG plugin: failed to load sqlite helper plugin', -1); 161 return false; 162 } 163 // delete comment 164 if(!$this->commenthelper) { 165 $this->commenthelper = plugin_load('helper', 'blogtng_comments'); 166 } 167 $this->commenthelper->delete_all($this->entry['pid']); 168 169 // delete tags 170 if(!$this->taghelper) { 171 $this->taghelper = plugin_load('helper', 'blogtng_tags'); 172 } 173 $this->taghelper->setPid($this->entry['pid']); 174 $this->taghelper->setTags(array()); //empty tag set 175 $this->taghelper->save(); 176 177 // delete entry 178 $sql = "DELETE FROM entries WHERE pid = ?"; 179 $ret = $this->sqlitehelper->getDB()->query($sql,$this->entry['pid']); 180 $this->entry = $this->prototype(); 181 182 183 return (bool) $ret; 184 } 185 186 /** 187 * Save an entry into the database 188 */ 189 public function save() { 190 if(!$this->entry['pid'] || $this->entry['pid'] == md5('')){ 191 msg('blogtng: no pid, refusing to save',-1); 192 return false; 193 } 194 if (!$this->sqlitehelper->ready()) { 195 msg('BlogTNG: no sqlite helper plugin available', -1); 196 return false; 197 } 198 199 $query = 'INSERT OR IGNORE INTO entries (pid, page, title, blog, image, created, lastmod, author, login, mail, commentstatus) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; 200 $this->sqlitehelper->getDB()->query( 201 $query, 202 $this->entry['pid'], 203 $this->entry['page'], 204 $this->entry['title'], 205 $this->entry['blog'], 206 $this->entry['image'], 207 $this->entry['created'], 208 $this->entry['lastmod'], 209 $this->entry['author'], 210 $this->entry['login'], 211 $this->entry['mail'], 212 $this->entry['commentstatus'] 213 ); 214 $query = 'UPDATE entries SET page = ?, title=?, blog=?, image=?, created = ?, lastmod=?, login = ?, author=?, mail=?, commentstatus=? WHERE pid=?'; 215 $result = $this->sqlitehelper->getDB()->query( 216 $query, 217 $this->entry['page'], 218 $this->entry['title'], 219 $this->entry['blog'], 220 $this->entry['image'], 221 $this->entry['created'], 222 $this->entry['lastmod'], 223 $this->entry['login'], 224 $this->entry['author'], 225 $this->entry['mail'], 226 $this->entry['commentstatus'], 227 $this->entry['pid'] 228 ); 229 if(!$result) { 230 msg('blogtng plugin: failed to save new entry!', -1); 231 return false; 232 } else { 233 return true; 234 } 235 } 236 237 //~~ xhtml functions 238 239 /** 240 * List matching blog entries 241 * 242 * Calls the *_list template for each entry in the result set 243 * 244 * @param $conf 245 * @param null $renderer 246 * @param string $templatetype 247 * @return string 248 */ 249 public function xhtml_list($conf, &$renderer=null, $templatetype='list'){ 250 $posts = $this->get_posts($conf); 251 if (!$posts) return ''; 252 253 $rendererBackup =& $this->renderer; 254 $this->renderer =& $renderer; 255 $entryBackup = $this->entry; 256 257 ob_start(); 258 if($conf['listwrap']) echo "<ul class=\"blogtng_$templatetype\">"; 259 foreach ($posts as $row) { 260 $this->load_by_row($row); 261 $this->tpl_content($conf['tpl'], $templatetype); 262 } 263 if($conf['listwrap']) echo '</ul>'; 264 $output = ob_get_contents(); 265 ob_end_clean(); 266 267 $this->entry = $entryBackup; // restore previous entry in order to allow nesting 268 $this->renderer =& $rendererBackup; // clean up again 269 return $output; 270 } 271 272 /** 273 * List matching pages for one or more tags 274 * 275 * Calls the *_tagsearch template for each entry in the result set 276 */ 277 public function xhtml_tagsearch($conf, &$renderer=null){ 278 if (count($conf['tags']) == 0) { 279 return ''; 280 }; 281 282 return $this->xhtml_list($conf, $renderer, $templatetype='tagsearch'); 283 } 284 285 /** 286 * Display pagination links for the configured list of entries 287 * 288 * @author Andreas Gohr <gohr@cosmocode.de> 289 */ 290 public function xhtml_pagination($conf){ 291 if(!$this->sqlitehelper->ready()) return ''; 292 293 $blog_query = '(blog = '. 294 $this->sqlitehelper->getDB()->quote_and_join($conf['blog'], 295 ' OR blog = ').')'; 296 $tag_query = $tag_table = ""; 297 if(count($conf['tags'])){ 298 $tag_query = ' AND (tag = '. 299 $this->sqlitehelper->getDB()->quote_and_join($conf['tags'], 300 ' OR tag = '). 301 ') AND A.pid = B.pid GROUP BY A.pid'; 302 $tag_table = ', tags B'; 303 } 304 305 // get the number of all matching entries 306 $query = 'SELECT A.pid, A.page 307 FROM entries A'.$tag_table.' 308 WHERE '.$blog_query.$tag_query.' 309 AND GETACCESSLEVEL(page) >= '.AUTH_READ; 310 $resid = $this->sqlitehelper->getDB()->query($query); 311 if (!$resid) return ''; 312 $count = $this->sqlitehelper->getDB()->res2count($resid); 313 if($count <= $conf['limit']) return ''; 314 315 // we now prepare an array of pages to show 316 $pages = array(); 317 318 // calculate page boundaries 319 $max = ceil($count/$conf['limit']); 320 $cur = floor($conf['offset']/$conf['limit'])+1; 321 322 $pages[] = 1; // first page always 323 $pages[] = $max; // last page always 324 $pages[] = $cur; // current always 325 326 if($max > 1){ // if enough pages 327 $pages[] = 2; // second and .. 328 $pages[] = $max-1; // one before last 329 } 330 331 // three around current 332 if($cur-1 > 0) $pages[] = $cur-1; 333 if($cur-2 > 0) $pages[] = $cur-2; 334 if($cur-3 > 0) $pages[] = $cur-3; 335 if($cur+1 < $max) $pages[] = $cur+1; 336 if($cur+2 < $max) $pages[] = $cur+2; 337 if($cur+3 < $max) $pages[] = $cur+3; 338 339 sort($pages); 340 $pages = array_unique($pages); 341 342 // we're done - build the output 343 $out = ''; 344 $out .= '<div class="blogtng_pagination">'; 345 if($cur > 1){ 346 $out .= '<a href="'.wl($conf['target'], 347 array('btng[pagination][start]'=>$conf['limit']*($cur-2), 348 'btng[post][tags]'=>join(',',$conf['tags']))). 349 '" class="prev">'.$this->getLang('prev').'</a> '; 350 } 351 $out .= '<span class="blogtng_pages">'; 352 $last = 0; 353 foreach($pages as $page){ 354 if($page - $last > 1){ 355 $out .= ' <span class="sep">...</span> '; 356 } 357 if($page == $cur){ 358 $out .= '<span class="cur">'.$page.'</span> '; 359 }else{ 360 $out .= '<a href="'.wl($conf['target'], 361 array('btng[pagination][start]'=>$conf['limit']*($page-1), 362 'btng[post][tags]'=>join(',',$conf['tags']))). 363 '">'.$page.'</a> '; 364 } 365 $last = $page; 366 } 367 $out .= '</span>'; 368 if($cur < $max){ 369 $out .= '<a href="'.wl($conf['target'], 370 array('btng[pagination][start]'=>$conf['limit']*($cur), 371 'btng[post][tags]'=>join(',',$conf['tags']))). 372 '" class="next">'.$this->getLang('next').'</a> '; 373 } 374 $out .= '</div>'; 375 376 return $out; 377 } 378 379 /** 380 * Displays a list of related blog entries 381 * 382 * @param $conf 383 * @return string 384 */ 385 public function xhtml_related($conf){ 386 ob_start(); 387 $this->tpl_related($conf['limit'],$conf['blog'],$conf['page'],$conf['tags']); 388 $output = ob_get_contents(); 389 ob_end_clean(); 390 return $output; 391 } 392 393 /** 394 * Displays a form to create new entries 395 * 396 * @param $conf 397 * @return string 398 */ 399 public function xhtml_newform($conf){ 400 global $ID; 401 402 // allowed to create? 403 if(!$this->toolshelper) { 404 $this->toolshelper = plugin_load('helper', 'blogtng_tools'); 405 } 406 $new = $this->toolshelper->mkpostid($conf['format'],'dummy'); 407 if(auth_quickaclcheck($new) < AUTH_CREATE) return ''; 408 409 $form = new Doku_Form($ID, wl($ID,array('do'=>'btngnew'),false,'&')); 410 if ($conf['title']) { 411 $form->addElement(form_makeOpenTag('h3')); 412 $form->addElement(hsc($conf['title'])); 413 $form->addElement(form_makeCloseTag('h3')); 414 } 415 if (isset($conf['select'])) { 416 $form->addElement(form_makeMenuField('btng[new][title]', helper_plugin_blogtng_tools::filterExplodeCSVinput($conf['select']), '', $this->getLang('title'), 'btng__nt', 'edit')); 417 } else { 418 $form->addElement(form_makeTextField('btng[new][title]', '', $this->getLang('title'), 'btng__nt', 'edit')); 419 } 420 if ($conf['tags']) { 421 if($conf['tags'][0] == '?') $conf['tags'] = helper_plugin_blogtng_tools::filterExplodeCSVinput($this->getConf('default_tags')); 422 $form->addElement(form_makeTextField('btng[post][tags]', implode(', ', $conf['tags']), $this->getLang('tags'), 'btng__ntags', 'edit')); 423 } 424 if ($conf['type']) { 425 if($conf['type'][0] == '?') $conf['type'] = $this->getConf('default_commentstatus'); 426 $form->addElement(form_makeMenuField('btng[post][commentstatus]', array('enabled', 'closed', 'disabled'), $conf['type'], $this->getLang('commentstatus'), 'blogtng__ncommentstatus', 'edit')); 427 } 428 429 430 $form->addElement(form_makeButton('submit', null, $this->getLang('create'))); 431 $form->addHidden('btng[new][format]', hsc($conf['format'])); 432 $form->addHidden('btng[post][blog]', hsc($conf['blog'][0])); 433 434 return '<div class="blogtng_newform">' . $form->getForm() . '</div>'; 435 } 436 437 //~~ template methods 438 439 /** 440 * Render content for the given @$type using template @$name. 441 * $type must be one of 'list', 'entry', 'feed' or 'tagsearch'. 442 * 443 * @param $name Template name 444 * @param $type Type to render. 445 */ 446 public function tpl_content($name, $type) { 447 $whitelist = array('list', 'entry', 'feed', 'tagsearch'); 448 if(!in_array($type, $whitelist)) return; 449 450 $tpl = helper_plugin_blogtng_tools::getTplFile($name, $type); 451 if($tpl !== false) { 452 /** @noinspection PhpUnusedLocalVariableInspection */ 453 $entry = $this; //used in the included template 454 include($tpl); 455 } 456 } 457 458 /** 459 * Print the whole entry, reformat it or cut it when needed 460 * 461 * @param bool $included - set true if you want content to be reformated 462 * @param string $readmore - where to cut the entry valid: 'syntax', FIXME -->add 'firstsection'?? 463 * @param bool $inc_level - FIXME --> this attribute is always set to false 464 * @param bool $skipheader - Remove the first header 465 * @return bool false if a recursion was detected and the entry could not be printed, true otherwise 466 */ 467 public function tpl_entry($included=true, $readmore='syntax', $inc_level=true, $skipheader=false) { 468 $content = $this->get_entrycontent($readmore, $inc_level, $skipheader); 469 470 if ($included) { 471 $content = $this->_convert_footnotes($content); 472 $content .= $this->_edit_button(); 473 } else { 474 $content = tpl_toc(true).$content; 475 } 476 477 echo html_secedit($content, !$included); 478 return true; 479 } 480 481 /** 482 * Print link to page or anchor. 483 * 484 * @param string $anchor 485 */ 486 public function tpl_link($anchor=''){ 487 echo wl($this->entry['page']).(!empty($anchor) ? '#'.$anchor : ''); 488 } 489 490 /** 491 * Print permalink to page or anchor. 492 * 493 * @param $str 494 */ 495 public function tpl_permalink($str) { 496 echo '<a href="' . wl ($this->entry['page']) . '" title="' . hsc($this->entry['title']) . '">' . $str . '</a>'; 497 } 498 499 /** 500 * Print abstract data 501 * FIXME: what's in $this->entry['abstract']? 502 * 503 * @param int $len 504 */ 505 public function tpl_abstract($len=0) { 506 $this->_load_abstract(); 507 if($len){ 508 $abstract = utf8_substr($this->entry['abstract'], 0, $len).'…'; 509 }else{ 510 $abstract = $this->entry['abstract']; 511 } 512 echo hsc($abstract); 513 } 514 515 /** 516 * Print title. 517 */ 518 public function tpl_title() { 519 print hsc($this->entry['title']); 520 } 521 522 /** 523 * Print creation date. 524 * 525 * @param string $format 526 */ 527 public function tpl_created($format='') { 528 if(!$this->entry['created']) return; // uh oh, something went wrong 529 print dformat($this->entry['created'],$format); 530 } 531 532 /** 533 * Print last modified date. 534 * 535 * @param string $format 536 */ 537 public function tpl_lastmodified($format='') { 538 if(!$this->entry['lastmod']) return; // uh oh, something went wrong 539 print dformat($this->entry['lastmod'], $format); 540 } 541 542 /** 543 * Print author. 544 */ 545 public function tpl_author() { 546 if(empty($this->entry['author'])) return; 547 print hsc($this->entry['author']); 548 } 549 550 /** 551 * Print a simple hcard 552 * 553 * @author Michael Klier <chi@chimeric.de> 554 */ 555 public function tpl_hcard() { 556 if(empty($this->entry['author'])) return; 557 558 // FIXME 559 // which url to link email/wiki/user page 560 // option to link author name with email or webpage? 561 562 $html = '<div class="vcard">' 563 . DOKU_TAB . '<a href="FIXME" class="fn nickname">' . 564 hsc($this->entry['author']) . '</a>' . DOKU_LF 565 . '</div>' . DOKU_LF; 566 567 print $html; 568 } 569 570 /** 571 * Print comments 572 * 573 * Wrapper around commenthelper->tpl_comments() 574 * 575 * @param $name 576 * @param null $types 577 */ 578 public function tpl_comments($name,$types=null) { 579 if ($this->entry['commentstatus'] == 'disabled') return; 580 if(!$this->commenthelper) { 581 $this->commenthelper = plugin_load('helper', 'blogtng_comments'); 582 } 583 $this->commenthelper->setPid($this->entry['pid']); 584 $this->commenthelper->tpl_comments($name,$types); 585 } 586 587 /** 588 * Print comment count 589 * 590 * Wrapper around commenthelper->tpl_commentcount() 591 * 592 * @param string $fmt_zero_comments 593 * @param string $fmt_one_comment 594 * @param string $fmt_comments 595 * @param null $types 596 */ 597 public function tpl_commentcount($fmt_zero_comments='', $fmt_one_comment='', $fmt_comments='',$types=null) { 598 if(!$this->commenthelper) { 599 $this->commenthelper = plugin_load('helper', 'blogtng_comments'); 600 } 601 $this->commenthelper->setPid($this->entry['pid']); 602 $this->commenthelper->tpl_count($fmt_zero_comments, $fmt_one_comment, $fmt_comments); 603 } 604 605 /** 606 * Print a list of related posts 607 * 608 * Can be called statically. Also exported as syntax <blog related> 609 * 610 * @param int $num - maximum number of links 611 * @param array $blogs - blogs to search 612 * @param bool|string $id - reference page (false for current) 613 * @param array $tags - additional tags to consider 614 */ 615 public function tpl_related($num=5,$blogs=array('default'),$id=false,$tags=array()){ 616 if(!$this->sqlitehelper->ready()) return; 617 618 global $INFO; 619 if($id === false) $id = $INFO['id']; //sidebar safe 620 621 $pid = md5(cleanID($id)); 622 623 $query = "SELECT tag 624 FROM tags 625 WHERE pid = '$pid'"; 626 $res = $this->sqlitehelper->getDB()->query($query); 627 $res = $this->sqlitehelper->getDB()->res2arr($res); 628 foreach($res as $row){ 629 $tags[] = $row['tag']; 630 } 631 $tags = array_unique($tags); 632 $tags = array_filter($tags); 633 if(!count($tags)) return; // no tags for comparison 634 635 $tags = $this->sqlitehelper->getDB()->quote_and_join($tags,','); 636 $blog_query = '(A.blog = '. 637 $this->sqlitehelper->getDB()->quote_and_join($blogs, 638 ' OR A.blog = ').')'; 639 640 $query = "SELECT page, title, COUNT(B.pid) AS cnt 641 FROM entries A, tags B 642 WHERE $blog_query 643 AND A.pid != '$pid' 644 AND A.pid = B.pid 645 AND B.tag IN ($tags) 646 AND GETACCESSLEVEL(page) >= ".AUTH_READ." 647 GROUP BY B.pid HAVING cnt > 0 648 ORDER BY cnt DESC, created DESC 649 LIMIT ".(int) $num; 650 $res = $this->sqlitehelper->getDB()->query($query); 651 if(!$this->sqlitehelper->getDB()->res2count($res)) return; // no results found 652 $res = $this->sqlitehelper->getDB()->res2arr($res); 653 654 // now do the output 655 echo '<ul class="related">'; 656 foreach($res as $row){ 657 echo '<li class="level1"><div class="li">'; 658 echo '<a href="'.wl($row['page']).'" class="wikilink1">'.hsc($row['title']).'</a>'; 659 echo '</div></li>'; 660 } 661 echo '</ul>'; 662 } 663 664 /** 665 * Print comment form 666 * 667 * Wrapper around commenthelper->tpl_form() 668 */ 669 public function tpl_commentform() { 670 if ($this->entry['commentstatus'] == 'closed' || $this->entry['commentstatus'] == 'disabled') return; 671 if(!$this->commenthelper) { 672 $this->commenthelper = plugin_load('helper', 'blogtng_comments'); 673 } 674 $this->commenthelper->tpl_form($this->entry['page'], $this->entry['pid'], $this->entry['blog']); 675 } 676 677 public function tpl_linkbacks() {} 678 679 /** 680 * Print a list of tags associated with the entry 681 * 682 * @param string $target - tag links will point to this page, tag is passed as parameter 683 */ 684 public function tpl_tags($target) { 685 if (!$this->taghelper) { 686 $this->taghelper = plugin_load('helper', 'blogtng_tags'); 687 } 688 $this->taghelper->load($this->entry['pid']); 689 $this->taghelper->tpl_tags($target); 690 } 691 692 /** 693 * @param $target 694 * @param string $separator 695 */ 696 public function tpl_tagstring($target, $separator=', ') { 697 if (!$this->taghelper) { 698 $this->taghelper = plugin_load('helper', 'blogtng_tags'); 699 } 700 $this->taghelper->load($this->entry['pid']); 701 $this->taghelper->tpl_tagstring($target, $separator); 702 } 703 704 /** 705 * Renders the link to the previous blog post using the given template. 706 * 707 * @param string $tpl a template specifing the link text. May contain placeholders 708 * for title, author and creation date of post 709 * @param bool|string $id string page id of blog post for which to generate the adjacent link 710 * @param bool $return whether to return the link or print it, defaults to print 711 * @return bool/string if there is no such link, false. otherwise, if $return is true, 712 * a string containing the generated HTML link, otherwise true. 713 */ 714 public function tpl_previouslink($tpl, $id=false, $return=false) { 715 $out = $this->_navi_link($tpl, 'prev', $id); 716 if ($return) { 717 return $out; 718 } else if ($out !== false) { 719 echo $out; 720 return true; 721 } 722 return false; 723 } 724 725 /** 726 * Renders the link to the next blog post using the given template. 727 * 728 * @param string $tpl a template specifing the link text. May contain placeholders 729 * for title, author and creation date of post 730 * @param bool|string $id page id of blog post for which to generate the adjacent link 731 * @param bool $return whether to return the link or print it, defaults to print 732 * @return bool/string if there is no such link, false. otherwise, if $return is true, 733 * a string containing the generated HTML link, otherwise true. 734 */ 735 public function tpl_nextlink($tpl, $id=false, $return=false) { 736 $out = $this->_navi_link($tpl, 'next', $id); 737 if ($return) { 738 return $out; 739 } else if ($out !== false) { 740 echo $out; 741 return true; 742 } 743 return false; 744 } 745 746 //~~ utility methods 747 748 /** 749 * Return array of blog templates. 750 * 751 * @return array 752 */ 753 public static function get_blogs() { 754 $pattern = DOKU_PLUGIN . 'blogtng/tpl/*{_,/}entry.php'; 755 $files = glob($pattern, GLOB_BRACE); 756 $blogs = array(''); 757 foreach ($files as $file) { 758 array_push($blogs, substr($file, strlen(DOKU_PLUGIN . 'blogtng/tpl/'), -10)); 759 } 760 return $blogs; 761 } 762 763 /** 764 * Get blog from this entry 765 * 766 * @return string 767 */ 768 public function get_blog() { 769 if ($this->entry != null) { 770 return $this->entry['blog']; 771 } else { 772 return ''; 773 } 774 } 775 776 /** 777 * FIXME parsing of tags by using taghelper->parse_tag_query 778 * @param $conf 779 * @return array 780 */ 781 public function get_posts($conf) { 782 if(!$this->sqlitehelper->ready()) return array(); 783 784 $sortkey = ($conf['sortby'] == 'random') ? 'Random()' : $conf['sortby']; 785 786 $blog_query = ''; 787 if(count($conf['blog']) > 0) { 788 $blog_query = '(blog = ' . $this->sqlitehelper->getDB()->quote_and_join($conf['blog'], ' OR blog = ') . ')'; 789 } 790 791 $tag_query = $tag_table = ""; 792 if(count($conf['tags'])) { 793 $tag_query = ''; 794 if(count($conf['blog']) > 0) { 795 $tag_query .= ' AND'; 796 } 797 $tag_query .= ' (tag = ' . $this->sqlitehelper->getDB()->quote_and_join($conf['tags'], ' OR tag = ') . ')'; 798 $tag_query .= ' AND A.pid = B.pid'; 799 800 $tag_table = ', tags B'; 801 } 802 803 $query = 'SELECT A.pid as pid, page, title, blog, image, created, 804 lastmod, login, author, mail, commentstatus 805 FROM entries A'.$tag_table.' 806 WHERE '.$blog_query.$tag_query.' 807 AND GETACCESSLEVEL(page) >= '.AUTH_READ.' 808 GROUP BY A.pid 809 ORDER BY '.$sortkey.' '.$conf['sortorder']. 810 ' LIMIT '.$conf['limit']. 811 ' OFFSET '.$conf['offset']; 812 813 $resid = $this->sqlitehelper->getDB()->query($query); 814 return $this->sqlitehelper->getDB()->res2arr($resid); 815 } 816 817 /** 818 * FIXME 819 * @param $readmore 820 * @param $inc_level 821 * @param $skipheader 822 * @return bool|string html of content 823 */ 824 public function get_entrycontent($readmore='syntax', $inc_level=true, $skipheader=false) { 825 static $recursion = array(); 826 827 $id = $this->entry['page']; 828 829 if(in_array($id, $recursion)){ 830 msg('blogtng: preventing infinite loop',-1); 831 return false; // avoid infinite loops 832 } 833 834 $recursion[] = $id; 835 836 /* 837 * FIXME do some caching here! 838 * - of the converted instructions 839 * - of p_render 840 */ 841 global $ID, $TOC, $conf; 842 $info = array(); 843 844 $backupID = $ID; 845 $ID = $id; // p_cached_instructions doesn't change $ID, so we need to do it or plugins like the discussion plugin might store information for the wrong page 846 $ins = p_cached_instructions(wikiFN($id)); 847 $ID = $backupID; // restore the original $ID as otherwise _convert_instructions won't do anything 848 $this->_convert_instructions($ins, $inc_level, $readmore, $skipheader); 849 $ID = $id; 850 851 $handleTOC = ($this->renderer !== null); // the call to p_render below might set the renderer 852 853 $renderer = null; 854 $backupTOC = null; 855 $backupTocminheads = null; 856 if ($handleTOC){ 857 $renderer =& $this->renderer; // save the renderer before p_render changes it 858 $backupTOC = $TOC; // the renderer overwrites the global $TOC 859 $backupTocminheads = $conf['tocminheads']; 860 $conf['tocminheads'] = 1; // let the renderer always generate a toc 861 } 862 863 $content = p_render('xhtml', $ins, $info); 864 865 if ($handleTOC){ 866 if ($TOC && $backupTOC !== $TOC && $info['toc']){ 867 $renderer->toc = array_merge($renderer->toc, $TOC); 868 $TOC = null; // Reset the global toc as it is included in the renderer now 869 // and if the renderer decides to not to output it the 870 // global one should be empty 871 } 872 $conf['tocminheads'] = $backupTocminheads; 873 $this->renderer =& $renderer; 874 } 875 876 $ID = $backupID; 877 878 array_pop($recursion); 879 return $content; 880 } 881 882 /** 883 * @param $pid 884 * @return int 885 */ 886 public function is_valid_pid($pid) { 887 return (preg_match('/^[0-9a-f]{32}$/', trim($pid))); 888 } 889 890 /** 891 * @return bool 892 */ 893 public function has_tags() { 894 if (!$this->taghelper) { 895 $this->taghelper = plugin_load('helper', 'blogtng_tags'); 896 } 897 return ($this->taghelper->count($this->entry['pid']) > 0); 898 } 899 900 /** 901 * Gets the adjacent (previous and next) links of a blog entry. 902 * 903 * @param bool|string $id page id of the entry for which to get said links 904 * @return array 2d assoziative array containing page id, title, author and creation date 905 * for both prev and next link 906 */ 907 public function getAdjacentLinks($id = false) { 908 global $INFO; 909 if($id === false) $id = $INFO['id']; //sidebar safe 910 $pid = md5(cleanID($id)); 911 912 $related = array(); 913 if(!$this->sqlitehelper->ready()) return $related; 914 915 foreach (array('prev', 'next') as $type) { 916 $operator = (($type == 'prev') ? '<' : '>'); 917 $order = (($type == 'prev') ? 'DESC' : 'ASC'); 918 $query = "SELECT A.page AS page, A.title AS title, 919 A.author AS author, A.created AS created 920 FROM entries A, entries B 921 WHERE B.pid = ? 922 AND A.pid != B.pid 923 AND A.created $operator B.created 924 AND A.blog = B.blog 925 AND GETACCESSLEVEL(A.page) >= ".AUTH_READ." 926 ORDER BY A.created $order 927 LIMIT 1"; 928 $res = $this->sqlitehelper->getDB()->query($query, $pid); 929 if ($this->sqlitehelper->getDB()->res2count($res) > 0) { 930 $result = $this->sqlitehelper->getDB()->res2arr($res); 931 $related[$type] = $result[0]; 932 } 933 } 934 return $related; 935 } 936 937 /** 938 * Returns a reference to the comment helper plugin preloaded with 939 * the current entry 940 */ 941 public function &getCommentHelper(){ 942 if(!$this->commenthelper) { 943 $this->commenthelper = plugin_load('helper', 'blogtng_comments'); 944 $this->commenthelper->setPid($this->entry['pid']); 945 } 946 return $this->commenthelper; 947 } 948 949 /** 950 * Returns a reference to the tag helper plugin preloaded with 951 * the current entry 952 */ 953 public function &getTagHelper(){ 954 if (!$this->taghelper) { 955 $this->taghelper = plugin_load('helper', 'blogtng_tags'); 956 $this->taghelper->load($this->entry['pid']); 957 } 958 return $this->taghelper; 959 } 960 961 962 963 //~~ private methods 964 965 private function _load_abstract(){ 966 if(isset($this->entry['abstract'])) return; 967 $id = $this->entry['page']; 968 969 $this->entry['abstract'] = p_get_metadata($id,'description abstract',true); 970 } 971 972 /** 973 * @param array &$ins 974 * @param bool $inc_level 975 * @param bool|string $readmore 976 * @param $skipheader 977 * @return bool 978 */ 979 private function _convert_instructions(&$ins, $inc_level, $readmore, $skipheader) { 980 global $ID; 981 982 $id = $this->entry['page']; 983 if (!page_exists($id)) return false; 984 985 // check if included page is in same namespace 986 $ns = getNS($id); 987 $convert = (getNS($ID) == $ns) ? false : true; 988 989 $first_header = true; 990 $open_wraps = array( 991 'section' => 0, 992 'p' => 0, 993 'list' => 0, 994 'table' => 0, 995 'tablecell' => 0, 996 'tableheader' => 0 997 ); 998 999 $n = count($ins); 1000 for ($i = 0; $i < $n; $i++) { 1001 $current = $ins[$i][0]; 1002 if ($convert && (substr($current, 0, 8) == 'internal')) { 1003 // convert internal links and media from relative to absolute 1004 $ins[$i][1][0] = $this->_convert_internal_link($ins[$i][1][0], $ns); 1005 } else { 1006 switch($current) { 1007 case 'header': 1008 // convert header levels and convert first header to permalink 1009 $text = $ins[$i][1][0]; 1010 $level = $ins[$i][1][1]; 1011 1012 // change first header to permalink 1013 if ($first_header) { 1014 if($skipheader){ 1015 unset($ins[$i]); 1016 }else{ 1017 $ins[$i] = array('plugin', 1018 array( 1019 'blogtng_header', 1020 array( 1021 $text, 1022 $level 1023 ), 1024 ), 1025 $ins[$i][1][2] 1026 ); 1027 } 1028 } 1029 $first_header = false; 1030 1031 // increase level of header 1032 if ($inc_level) { 1033 $level = $level + 1; 1034 if ($level > 5) $level = 5; 1035 if (is_array($ins[$i][1][1])) { 1036 // permalink header 1037 $ins[$i][1][1][1] = $level; 1038 } else { 1039 // normal header 1040 $ins[$i][1][1] = $level; 1041 } 1042 } 1043 break; 1044 1045 //fallthroughs for counting tags 1046 /** @noinspection PhpMissingBreakStatementInspection */ 1047 case 'section_open'; 1048 // the same for sections 1049 $level = $ins[$i][1][0]; 1050 if ($inc_level) $level = $level + 1; 1051 if ($level > 5) $level = 5; 1052 $ins[$i][1][0] = $level; 1053 /* fallthrough */ 1054 case 'section_close': 1055 case 'p_open': 1056 case 'p_close': 1057 case 'listu_open': 1058 case 'listu_close': 1059 case 'table_open': 1060 case 'table_close': 1061 case 'tablecell_open': 1062 case 'tableheader_open': 1063 case 'tablecell_close': 1064 case 'tableheader_close': 1065 list($item,$action) = explode('_', $current, 2); 1066 $open_wraps[$item] += ($action == 'open' ? 1 : -1); 1067 break; 1068 1069 case 'plugin': 1070 if(($ins[$i][1][0] == 'blogtng_readmore') && $readmore) { 1071 // cut off the instructions here 1072 $this->_read_more($ins, $i, $open_wraps, $inc_level); 1073 $open_wraps['sections'] = 0; 1074 } 1075 break 2; 1076 } 1077 } 1078 } 1079 $this->_finish_convert($ins, $open_wraps['sections']); 1080 return true; 1081 } 1082 1083 /** 1084 * Convert relative internal links and media 1085 * 1086 * @param string $link: internal links or media 1087 * @param string $ns: namespace of included page 1088 * @return string $link converted, now absolute link 1089 */ 1090 private function _convert_internal_link($link, $ns) { 1091 if ($link{0} == '.') { 1092 // relative subnamespace 1093 if ($link{1} == '.') { 1094 // parent namespace 1095 return getNS($ns).':'.substr($link, 2); 1096 } else { 1097 // current namespace 1098 return $ns.':'.substr($link, 1); 1099 } 1100 } elseif (strpos($link, ':') === false) { 1101 // relative link 1102 return $ns.':'.$link; 1103 } elseif ($link{0} == '#') { 1104 // anchor 1105 return $this->entry['page'].$link; 1106 } else { 1107 // absolute link - don't change 1108 return $link; 1109 } 1110 } 1111 1112 /** 1113 * @param $ins 1114 * @param $i 1115 * @param $open_wraps 1116 * @param $inc_level 1117 */ 1118 private function _read_more(&$ins, $i, $open_wraps, $inc_level) { 1119 $append_link = (is_array($ins[$i+1]) && $ins[$i+1][0] != 'document_end'); 1120 1121 //iterate to the end of a tablerow 1122 if($append_link && $open_wraps['table'] && ($open_wraps['tablecell'] || $open_wraps['tableheader'])) { 1123 for(; $i < count($ins); $i++) { 1124 if($ins[$i][0] == 'tablerow_close') { 1125 $i++; //include tablerow_close instruction 1126 break; 1127 } 1128 } 1129 } 1130 $ins = array_slice($ins, 0, $i); 1131 1132 if ($append_link) { 1133 $last = $ins[$i-1]; 1134 1135 //close open wrappers 1136 if($open_wraps['p']) { 1137 $ins[] = array('p_close', array(), $last[2]); 1138 } 1139 for ($i = 0; $i < $open_wraps['listu']; $i++) { 1140 if($i === 0) { 1141 $ins[] = array('listcontent_close', array(), $last[2]); 1142 } 1143 $ins[] = array('listitem_close', array(), $last[2]); 1144 $ins[] = array('listu_close', array(), $last[2]); 1145 } 1146 if($open_wraps['table']) { 1147 $ins[] = array('table_close', array(), $last[2]); 1148 } 1149 for ($i = 0; $i < $open_wraps['section']; $i++) { 1150 $ins[] = array('section_close', array(), $last[2]); 1151 } 1152 1153 $ins[] = array('section_open', array(($inc_level ? 2 : 1)), $last[2]); 1154 $ins[] = array('p_open', array(), $last[2]); 1155 $ins[] = array('internallink',array($this->entry['page'].'#readmore_'.str_replace(':', '_', $this->entry['page']), $this->getLang('readmore')),$last[2]); 1156 $ins[] = array('p_close', array(), $last[2]); 1157 $ins[] = array('section_close', array(), $last[2]); 1158 } 1159 } 1160 1161 /** 1162 * Adds 'document_start' and 'document_end' instructions if not already there 1163 * 1164 * @param $ins 1165 * @param $open_sections 1166 */ 1167 private function _finish_convert(&$ins, $open_sections) { 1168 if ($ins[0][0] != 'document_start') 1169 @array_unshift($ins, array('document_start', array(), 0)); 1170 // we can't use count here, instructions are not even indexed 1171 $keys = array_keys($ins); 1172 $c = array_pop($keys); 1173 if ($ins[$c][0] != 'document_end') 1174 $ins[] = array('document_end', array(), 0); 1175 } 1176 1177 /** 1178 * Converts footnotes 1179 * 1180 * @param string $html content of wikipage 1181 * @return string html with converted footnotes 1182 */ 1183 private function _convert_footnotes($html) { 1184 $id = str_replace(':', '_', $this->entry['page']); 1185 $replace = array( 1186 '!<a href="#fn__(\d+)" name="fnt__(\d+)" id="fnt__(\d+)" class="fn_top">!' => 1187 '<a href="#fn__'.$id.'__\1" name="fnt__'.$id.'__\2" id="fnt__'.$id.'__\3" class="fn_top">', 1188 '!<a href="#fnt__(\d+)" id="fn__(\d+)" name="fn__(\d+)" class="fn_bot">!' => 1189 '<a href="#fnt__'.$id.'__\1" name="fn__'.$id.'__\2" id="fn__'.$id.'__\3" class="fn_bot">', 1190 ); 1191 $html = preg_replace(array_keys($replace), array_values($replace), $html); 1192 return $html; 1193 } 1194 1195 /** 1196 * Display an edit button for the included page 1197 */ 1198 private function _edit_button() { 1199 global $ID; 1200 $id = $this->entry['page']; 1201 $perm = auth_quickaclcheck($id); 1202 1203 if (page_exists($id)) { 1204 if (($perm >= AUTH_EDIT) && (is_writable(wikiFN($id)))) { 1205 $action = 'edit'; 1206 } else { 1207 return ''; 1208 } 1209 } elseif ($perm >= AUTH_CREATE) { 1210 $action = 'create'; 1211 } else { 1212 return ''; 1213 } 1214 1215 $params = array('do' => 'edit'); 1216 $params['redirect_id'] = $ID; 1217 return '<div class="secedit">'.DOKU_LF.DOKU_TAB. 1218 html_btn($action, $id, '', $params, 'post').DOKU_LF. 1219 '</div>'.DOKU_LF; 1220 } 1221 1222 /** 1223 * Generates the HTML output of the link to the previous or to the next blog 1224 * entry in respect to the given page id using the specified template. 1225 * 1226 * @param string $tpl a template specifing the link text. May contain placeholders 1227 * for title, author and creation date of post 1228 * @param string $type type of link to generate, may be 'prev' or 'next' 1229 * @param bool|string $id page id of blog post for which to generate the adjacent link 1230 * @return bool|string a string containing the prepared HTML anchor tag, or false if there 1231 * is no fitting post to link to 1232 */ 1233 private function _navi_link($tpl, $type, $id = false) { 1234 $related = $this->getAdjacentLinks($id); 1235 if (isset($related[$type])) { 1236 $replace = array( 1237 '@TITLE@' => $related[$type]['title'], 1238 '@AUTHOR@' => $related[$type]['author'], 1239 '@DATE@' => dformat($related[$type]['created']), 1240 ); 1241 $out = '<a href="' . wl($related[$type]['page'], '') . '" class="wikilink1" rel="'.$type.'">' . str_replace(array_keys($replace), array_values($replace), $tpl) . '</a>'; 1242 return $out; 1243 } 1244 return false; 1245 } 1246 1247} 1248// vim:ts=4:sw=4:et: 1249