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