1<?php 2/** 3 * Include Plugin: Display a wiki page within another wiki page 4 * 5 * Action plugin component, for cache validity determination 6 * 7 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 8 * @author Christopher Smith <chris@jalakai.co.uk> 9 * @author Michael Klier <chi@chimeric.de> 10 */ 11 12/** 13 * All DokuWiki plugins to extend the parser/rendering mechanism 14 * need to inherit from this class 15 */ 16class action_plugin_include extends DokuWiki_Action_Plugin { 17 18 /* @var helper_plugin_include $helper */ 19 var $helper = null; 20 21 function __construct() { 22 $this->helper = plugin_load('helper', 'include'); 23 } 24 25 /** 26 * plugin should use this method to register its handlers with the dokuwiki's event controller 27 */ 28 function register(Doku_Event_Handler $controller) { 29 /* @var Doku_event_handler $controller */ 30 $controller->register_hook('INDEXER_PAGE_ADD', 'BEFORE', $this, 'handle_indexer'); 31 $controller->register_hook('INDEXER_VERSION_GET', 'BEFORE', $this, 'handle_indexer_version'); 32 $controller->register_hook('PARSER_CACHE_USE','BEFORE', $this, '_cache_prepare'); 33 $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form'); // todo remove 34 $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form'); 35 $controller->register_hook('HTML_CONFLICTFORM_OUTPUT', 'BEFORE', $this, 'handle_form'); // todo remove 36 $controller->register_hook('FORM_CONFLICT_OUTPUT', 'BEFORE', $this, 'handle_form'); 37 $controller->register_hook('HTML_DRAFTFORM_OUTPUT', 'BEFORE', $this, 'handle_form'); // todo remove 38 $controller->register_hook('FORM_DRAFT_OUTPUT', 'BEFORE', $this, 'handle_form'); 39 $controller->register_hook('ACTION_SHOW_REDIRECT', 'BEFORE', $this, 'handle_redirect'); 40 $controller->register_hook('PARSER_HANDLER_DONE', 'BEFORE', $this, 'handle_parser'); 41 $controller->register_hook('PARSER_METADATA_RENDER', 'AFTER', $this, 'handle_metadata'); 42 $controller->register_hook('HTML_SECEDIT_BUTTON', 'BEFORE', $this, 'handle_secedit_button'); 43 $controller->register_hook('PLUGIN_MOVE_HANDLERS_REGISTER', 'BEFORE', $this, 'handle_move_register'); 44 } 45 46 /** 47 * Add a version string to the index so it is rebuilt 48 * whenever the handler is updated or the safeindex setting is changed 49 */ 50 public function handle_indexer_version($event, $param) { 51 $event->data['plugin_include'] = '0.1.safeindex='.$this->getConf('safeindex'); 52 } 53 54 /** 55 * Handles the INDEXER_PAGE_ADD event, prevents indexing of metadata from included pages that aren't public if enabled 56 * 57 * @param Doku_Event $event the event object 58 * @param array $params optional parameters (unused) 59 */ 60 public function handle_indexer(Doku_Event $event, $params) { 61 global $USERINFO; 62 63 // check if the feature is enabled at all 64 if (!$this->getConf('safeindex')) return; 65 66 // is there a user logged in at all? If not everything is fine already 67 if (is_null($USERINFO) && !isset($_SERVER['REMOTE_USER'])) return; 68 69 // get the include metadata in order to see which pages were included 70 $inclmeta = p_get_metadata($event->data['page'], 'plugin_include', METADATA_RENDER_UNLIMITED); 71 $all_public = true; // are all included pages public? 72 // check if the current metadata indicates that non-public pages were included 73 if ($inclmeta !== null && isset($inclmeta['pages'])) { 74 foreach ($inclmeta['pages'] as $page) { 75 if (auth_aclcheck($page['id'], '', array()) < AUTH_READ) { // is $page public? 76 $all_public = false; 77 break; 78 } 79 } 80 } 81 82 if (!$all_public) { // there were non-public pages included - action required! 83 // backup the user information 84 $userinfo_backup = $USERINFO; 85 $remote_user = $_SERVER['REMOTE_USER']; 86 // unset user information - temporary logoff! 87 $USERINFO = null; 88 unset($_SERVER['REMOTE_USER']); 89 90 // metadata is only rendered once for a page in one request - thus we need to render manually. 91 $meta = p_read_metadata($event->data['page']); // load the original metdata 92 $meta = p_render_metadata($event->data['page'], $meta); // render the metadata 93 p_save_metadata($event->data['page'], $meta); // save the metadata so other event handlers get the public metadata, too 94 95 $meta = $meta['current']; // we are only interested in current metadata. 96 97 // check if the tag plugin handler has already been called before the include plugin 98 $tag_called = isset($event->data['metadata']['subject']); 99 100 // Reset the metadata in the renderer. This removes data from all other event handlers, but we need to be on the safe side here. 101 $event->data['metadata'] = array('title' => $meta['title']); 102 103 // restore the relation references metadata 104 if (isset($meta['relation']['references'])) { 105 $event->data['metadata']['relation_references'] = array_keys($meta['relation']['references']); 106 } else { 107 $event->data['metadata']['relation_references'] = array(); 108 } 109 110 // restore the tag metadata if the tag plugin handler has been called before the include plugin handler. 111 if ($tag_called) { 112 $tag_helper = $this->loadHelper('tag', false); 113 if ($tag_helper) { 114 if (isset($meta['subject'])) { 115 $event->data['metadata']['subject'] = $tag_helper->_cleanTagList($meta['subject']); 116 } else { 117 $event->data['metadata']['subject'] = array(); 118 } 119 } 120 } 121 122 // restore user information 123 $USERINFO = $userinfo_backup; 124 $_SERVER['REMOTE_USER'] = $remote_user; 125 } 126 } 127 128 /** 129 * Used for debugging purposes only 130 */ 131 function handle_metadata(&$event, $param) { 132 global $conf; 133 if($conf['allowdebug'] && $this->getConf('debugoutput')) { 134 dbglog('---- PLUGIN INCLUDE META DATA START ----'); 135 dbglog($event->data); 136 dbglog('---- PLUGIN INCLUDE META DATA END ----'); 137 } 138 } 139 140 /** 141 * Supplies the current section level to the include syntax plugin 142 * 143 * @author Michael Klier <chi@chimeric.de> 144 * @author Michael Hamann <michael@content-space.de> 145 */ 146 function handle_parser(Doku_Event $event, $param) { 147 global $ID; 148 149 $level = 0; 150 $ins =& $event->data->calls; 151 $num = count($ins); 152 for($i=0; $i<$num; $i++) { 153 switch($ins[$i][0]) { 154 case 'plugin': 155 switch($ins[$i][1][0]) { 156 case 'include_include': 157 $ins[$i][1][1][4] = $level; 158 break; 159 /* FIXME: this doesn't work anymore that way with the new structure 160 // some plugins already close open sections 161 // so we need to make sure we don't close them twice 162 case 'box': 163 $this->helper->sec_close = false; 164 break; 165 */ 166 } 167 break; 168 case 'section_open': 169 $level = $ins[$i][1][0]; 170 break; 171 } 172 } 173 } 174 175 /** 176 * Add a hidden input to the form to preserve the redirect_id 177 */ 178 function handle_form(Doku_Event $event, $param) 179 { 180 if (!array_key_exists('redirect_id', $_REQUEST)) return; 181 182 if(is_a($event->data, \dokuwiki\Form\Form::class)) { 183 $event->data->setHiddenField('redirect_id', cleanID($_REQUEST['redirect_id'])); 184 } else { 185 // todo remove when old FORM events are no longer supported 186 $event->data->addHidden('redirect_id', cleanID($_REQUEST['redirect_id'])); 187 } 188 } 189 190 /** 191 * Modify the data for the redirect when there is a redirect_id set 192 */ 193 function handle_redirect(Doku_Event &$event, $param) { 194 if (array_key_exists('redirect_id', $_REQUEST)) { 195 // Render metadata when this is an older DokuWiki version where 196 // metadata is not automatically re-rendered as the page has probably 197 // been changed but is not directly displayed 198 $versionData = getVersionData(); 199 if ($versionData['date'] < '2010-11-23') { 200 p_set_metadata($event->data['id'], array(), true); 201 } 202 $event->data['id'] = cleanID($_REQUEST['redirect_id']); 203 $event->data['title'] = ''; 204 } 205 } 206 207 /** 208 * prepare the cache object for default _useCache action 209 */ 210 function _cache_prepare(Doku_Event &$event, $param) { 211 global $conf; 212 213 /* @var cache_renderer $cache */ 214 $cache =& $event->data; 215 216 if(!isset($cache->page)) return; 217 if(!isset($cache->mode) || $cache->mode == 'i') return; 218 219 $depends = p_get_metadata($cache->page, 'plugin_include'); 220 221 if($conf['allowdebug'] && $this->getConf('debugoutput')) { 222 dbglog('---- PLUGIN INCLUDE CACHE DEPENDS START ----'); 223 dbglog($depends); 224 dbglog('---- PLUGIN INCLUDE CACHE DEPENDS END ----'); 225 } 226 227 if (!is_array($depends)) return; // nothing to do for us 228 229 if (!is_array($depends['pages']) || 230 !is_array($depends['instructions']) || 231 $depends['pages'] != $this->helper->_get_included_pages_from_meta_instructions($depends['instructions']) || 232 // the include_content url parameter may change the behavior for included pages 233 $depends['include_content'] != isset($_REQUEST['include_content'])) { 234 235 $cache->depends['purge'] = true; // included pages changed or old metadata - request purge. 236 if($conf['allowdebug'] && $this->getConf('debugoutput')) { 237 dbglog('---- PLUGIN INCLUDE: REQUESTING CACHE PURGE ----'); 238 dbglog('---- PLUGIN INCLUDE CACHE PAGES FROM META START ----'); 239 dbglog($depends['pages']); 240 dbglog('---- PLUGIN INCLUDE CACHE PAGES FROM META END ----'); 241 dbglog('---- PLUGIN INCLUDE CACHE PAGES FROM META_INSTRUCTIONS START ----'); 242 dbglog($this->helper->_get_included_pages_from_meta_instructions($depends['instructions'])); 243 dbglog('---- PLUGIN INCLUDE CACHE PAGES FROM META_INSTRUCTIONS END ----'); 244 245 } 246 } else { 247 // add plugin.info.txt to depends for nicer upgrades 248 $cache->depends['files'][] = dirname(__FILE__) . '/plugin.info.txt'; 249 foreach ($depends['pages'] as $page) { 250 if (!$page['exists']) continue; 251 $file = wikiFN($page['id']); 252 if (!in_array($file, $cache->depends['files'])) { 253 $cache->depends['files'][] = $file; 254 } 255 } 256 } 257 } 258 259 /** 260 * Handle special section edit buttons for the include plugin to get the current page 261 * and replace normal section edit buttons when the current page is different from the 262 * global $ID. 263 */ 264 function handle_secedit_button(Doku_Event &$event, $params) { 265 // stack of included pages in the form ('id' => page, 'rev' => modification time, 'writable' => bool) 266 static $page_stack = array(); 267 268 global $ID, $lang; 269 270 $data = $event->data; 271 272 if ($data['target'] == 'plugin_include_start' || $data['target'] == 'plugin_include_start_noredirect') { 273 // handle the "section edits" added by the include plugin 274 $fn = wikiFN($data['name']); 275 $perm = auth_quickaclcheck($data['name']); 276 array_unshift($page_stack, array( 277 'id' => $data['name'], 278 'rev' => @filemtime($fn), 279 'writable' => (page_exists($data['name']) ? (is_writable($fn) && $perm >= AUTH_EDIT) : $perm >= AUTH_CREATE), 280 'redirect' => ($data['target'] == 'plugin_include_start'), 281 )); 282 } elseif ($data['target'] == 'plugin_include_end') { 283 array_shift($page_stack); 284 } elseif ($data['target'] == 'plugin_include_editbtn') { 285 if ($page_stack[0]['writable']) { 286 $params = array('do' => 'edit', 287 'id' => $page_stack[0]['id']); 288 if ($page_stack[0]['redirect']) { 289 $params['redirect_id'] = $ID; 290 $params['hid'] = $data['hid']; 291 } 292 $event->result = '<div class="secedit">' . DOKU_LF . 293 html_btn('incledit', $page_stack[0]['id'], '', 294 $params, 'post', 295 $data['name'], 296 $lang['btn_secedit'].' ('.$page_stack[0]['id'].')') . 297 '</div>' . DOKU_LF; 298 } 299 } elseif (!empty($page_stack)) { 300 301 // Special handling for the edittable plugin 302 if ($data['target'] == 'table' && !plugin_isdisabled('edittable')) { 303 /* @var action_plugin_edittable_editor $edittable */ 304 $edittable = plugin_load('action', 'edittable_editor'); 305 if (is_null($edittable)) 306 $edittable = plugin_load('action', 'edittable'); 307 $data['name'] = $edittable->getLang('secedit_name'); 308 } 309 310 if ($page_stack[0]['writable'] && isset($data['name']) && $data['name'] !== '') { 311 $name = $data['name']; 312 unset($data['name']); 313 314 $secid = $data['secid']; 315 unset($data['secid']); 316 317 if ($page_stack[0]['redirect']) 318 $data['redirect_id'] = $ID; 319 320 $event->result = "<div class='secedit editbutton_" . $data['target'] . 321 " editbutton_" . $secid . "'>" . 322 html_btn('secedit', $page_stack[0]['id'], '', 323 array_merge(array('do' => 'edit', 324 'rev' => $page_stack[0]['rev'], 325 'summary' => '['.$name.'] '), $data), 326 'post', $name) . '</div>'; 327 } else { 328 $event->result = ''; 329 } 330 } else { 331 return; // return so the event won't be stopped 332 } 333 334 $event->preventDefault(); 335 $event->stopPropagation(); 336 } 337 338 public function handle_move_register(Doku_Event $event, $params) { 339 $event->data['handlers']['include_include'] = array($this, 'rewrite_include'); 340 } 341 342 public function rewrite_include($match, $pos, $state, $plugin, helper_plugin_move_handler $handler) { 343 $syntax = substr($match, 2, -2); // strip markup 344 $replacers = explode('|', $syntax); 345 $syntax = array_shift($replacers); 346 list($syntax, $flags) = array_pad(explode('&', $syntax, 2), 2, ""); 347 348 // break the pattern up into its parts 349 list($mode, $page, $sect) = array_pad(preg_split('/>|#/u', $syntax, 3), 3, ""); 350 351 if (method_exists($handler, 'adaptRelativeId')) { // move plugin before version 2015-05-16 352 $newpage = $handler->adaptRelativeId($page); 353 } else { 354 $newpage = $handler->resolveMoves($page, 'page'); 355 $newpage = $handler->relativeLink($page, $newpage, 'page'); 356 } 357 358 if ($newpage == $page) { 359 return $match; 360 } else { 361 $result = '{{'.$mode.'>'.$newpage; 362 if ($sect) $result .= '#'.$sect; 363 if ($flags) $result .= '&'.$flags; 364 if ($replacers) $result .= '|'.$replacers; 365 $result .= '}}'; 366 return $result; 367 } 368 } 369} 370// vim:ts=4:sw=4:et: 371