1<?php 2/** 3 * Common DokuWiki functions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/'); 10require_once(DOKU_CONF.'dokuwiki.php'); 11require_once(DOKU_INC.'inc/io.php'); 12require_once(DOKU_INC.'inc/changelog.php'); 13require_once(DOKU_INC.'inc/utf8.php'); 14require_once(DOKU_INC.'inc/mail.php'); 15require_once(DOKU_INC.'inc/parserutils.php'); 16require_once(DOKU_INC.'inc/infoutils.php'); 17 18/** 19 * These constants are used with the recents function 20 */ 21define('RECENTS_SKIP_DELETED',2); 22define('RECENTS_SKIP_MINORS',4); 23define('RECENTS_SKIP_SUBSPACES',8); 24 25/** 26 * Wrapper around htmlspecialchars() 27 * 28 * @author Andreas Gohr <andi@splitbrain.org> 29 * @see htmlspecialchars() 30 */ 31function hsc($string){ 32 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 33} 34 35/** 36 * print a newline terminated string 37 * 38 * You can give an indention as optional parameter 39 * 40 * @author Andreas Gohr <andi@splitbrain.org> 41 */ 42function ptln($string,$indent=0){ 43 echo str_repeat(' ', $indent)."$string\n"; 44} 45 46/** 47 * strips control characters (<32) from the given string 48 * 49 * @author Andreas Gohr <andi@splitbrain.org> 50 */ 51function stripctl($string){ 52 return preg_replace('/[\x00-\x1F]+/s','',$string); 53} 54 55/** 56 * Return a secret token to be used for CSRF attack prevention 57 * 58 * @author Andreas Gohr <andi@splitbrain.org> 59 * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery 60 * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html 61 * @return string 62 */ 63function getSecurityToken(){ 64 return md5(auth_cookiesalt().session_id()); 65} 66 67/** 68 * Check the secret CSRF token 69 */ 70function checkSecurityToken($token=null){ 71 if(is_null($token)) $token = $_REQUEST['sectok']; 72 if(getSecurityToken() != $token){ 73 msg('Security Token did not match. Possible CSRF attack.',-1); 74 return false; 75 } 76 return true; 77} 78 79/** 80 * Print a hidden form field with a secret CSRF token 81 * 82 * @author Andreas Gohr <andi@splitbrain.org> 83 */ 84function formSecurityToken($print=true){ 85 $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n"; 86 if($print){ 87 echo $ret; 88 }else{ 89 return $ret; 90 } 91} 92 93/** 94 * Return info about the current document as associative 95 * array. 96 * 97 * @author Andreas Gohr <andi@splitbrain.org> 98 */ 99function pageinfo(){ 100 global $ID; 101 global $REV; 102 global $USERINFO; 103 global $conf; 104 105 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml 106 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary 107 $info['id'] = $ID; 108 $info['rev'] = $REV; 109 110 if($_SERVER['REMOTE_USER']){ 111 $info['userinfo'] = $USERINFO; 112 $info['perm'] = auth_quickaclcheck($ID); 113 $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],false); 114 $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true); 115 $info['client'] = $_SERVER['REMOTE_USER']; 116 117 // set info about manager/admin status 118 $info['isadmin'] = false; 119 $info['ismanager'] = false; 120 if($info['perm'] == AUTH_ADMIN){ 121 $info['isadmin'] = true; 122 $info['ismanager'] = true; 123 }elseif(auth_ismanager()){ 124 $info['ismanager'] = true; 125 } 126 127 // if some outside auth were used only REMOTE_USER is set 128 if(!$info['userinfo']['name']){ 129 $info['userinfo']['name'] = $_SERVER['REMOTE_USER']; 130 } 131 132 }else{ 133 $info['perm'] = auth_aclcheck($ID,'',null); 134 $info['subscribed'] = false; 135 $info['client'] = clientIP(true); 136 } 137 138 $info['namespace'] = getNS($ID); 139 $info['locked'] = checklock($ID); 140 $info['filepath'] = fullpath(wikiFN($ID)); 141 $info['exists'] = @file_exists($info['filepath']); 142 if($REV){ 143 //check if current revision was meant 144 if($info['exists'] && (@filemtime($info['filepath'])==$REV)){ 145 $REV = ''; 146 }else{ 147 //really use old revision 148 $info['filepath'] = fullpath(wikiFN($ID,$REV)); 149 $info['exists'] = @file_exists($info['filepath']); 150 } 151 } 152 $info['rev'] = $REV; 153 if($info['exists']){ 154 $info['writable'] = (is_writable($info['filepath']) && 155 ($info['perm'] >= AUTH_EDIT)); 156 }else{ 157 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 158 } 159 $info['editable'] = ($info['writable'] && empty($info['lock'])); 160 $info['lastmod'] = @filemtime($info['filepath']); 161 162 //load page meta data 163 $info['meta'] = p_get_metadata($ID); 164 165 //who's the editor 166 if($REV){ 167 $revinfo = getRevisionInfo($ID, $REV, 1024); 168 }else{ 169 if (is_array($info['meta']['last_change'])) { 170 $revinfo = $info['meta']['last_change']; 171 } else { 172 $revinfo = getRevisionInfo($ID, $info['lastmod'], 1024); 173 // cache most recent changelog line in metadata if missing and still valid 174 if ($revinfo!==false) { 175 $info['meta']['last_change'] = $revinfo; 176 p_set_metadata($ID, array('last_change' => $revinfo)); 177 } 178 } 179 } 180 //and check for an external edit 181 if($revinfo!==false && $revinfo['date']!=$info['lastmod']){ 182 // cached changelog line no longer valid 183 $revinfo = false; 184 $info['meta']['last_change'] = $revinfo; 185 p_set_metadata($ID, array('last_change' => $revinfo)); 186 } 187 188 $info['ip'] = $revinfo['ip']; 189 $info['user'] = $revinfo['user']; 190 $info['sum'] = $revinfo['sum']; 191 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID. 192 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor']. 193 194 if($revinfo['user']){ 195 $info['editor'] = $revinfo['user']; 196 }else{ 197 $info['editor'] = $revinfo['ip']; 198 } 199 200 // draft 201 $draft = getCacheName($info['client'].$ID,'.draft'); 202 if(@file_exists($draft)){ 203 if(@filemtime($draft) < @filemtime(wikiFN($ID))){ 204 // remove stale draft 205 @unlink($draft); 206 }else{ 207 $info['draft'] = $draft; 208 } 209 } 210 211 // mobile detection 212 $info['ismobile'] = clientismobile(); 213 214 return $info; 215} 216 217/** 218 * Build an string of URL parameters 219 * 220 * @author Andreas Gohr 221 */ 222function buildURLparams($params, $sep='&'){ 223 $url = ''; 224 $amp = false; 225 foreach($params as $key => $val){ 226 if($amp) $url .= $sep; 227 228 $url .= $key.'='; 229 $url .= rawurlencode((string)$val); 230 $amp = true; 231 } 232 return $url; 233} 234 235/** 236 * Build an string of html tag attributes 237 * 238 * Skips keys starting with '_', values get HTML encoded 239 * 240 * @author Andreas Gohr 241 */ 242function buildAttributes($params,$skipempty=false){ 243 $url = ''; 244 foreach($params as $key => $val){ 245 if($key{0} == '_') continue; 246 if($val === '' && $skipempty) continue; 247 248 $url .= $key.'="'; 249 $url .= htmlspecialchars ($val); 250 $url .= '" '; 251 } 252 return $url; 253} 254 255 256/** 257 * This builds the breadcrumb trail and returns it as array 258 * 259 * @author Andreas Gohr <andi@splitbrain.org> 260 */ 261function breadcrumbs(){ 262 // we prepare the breadcrumbs early for quick session closing 263 static $crumbs = null; 264 if($crumbs != null) return $crumbs; 265 266 global $ID; 267 global $ACT; 268 global $conf; 269 $crumbs = $_SESSION[DOKU_COOKIE]['bc']; 270 271 //first visit? 272 if (!is_array($crumbs)){ 273 $crumbs = array(); 274 } 275 //we only save on show and existing wiki documents 276 $file = wikiFN($ID); 277 if($ACT != 'show' || !@file_exists($file)){ 278 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 279 return $crumbs; 280 } 281 282 // page names 283 $name = noNSorNS($ID); 284 if ($conf['useheading']) { 285 // get page title 286 $title = p_get_first_heading($ID,true); 287 if ($title) { 288 $name = $title; 289 } 290 } 291 292 //remove ID from array 293 if (isset($crumbs[$ID])) { 294 unset($crumbs[$ID]); 295 } 296 297 //add to array 298 $crumbs[$ID] = $name; 299 //reduce size 300 while(count($crumbs) > $conf['breadcrumbs']){ 301 array_shift($crumbs); 302 } 303 //save to session 304 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 305 return $crumbs; 306} 307 308/** 309 * Filter for page IDs 310 * 311 * This is run on a ID before it is outputted somewhere 312 * currently used to replace the colon with something else 313 * on Windows systems and to have proper URL encoding 314 * 315 * Urlencoding is ommitted when the second parameter is false 316 * 317 * @author Andreas Gohr <andi@splitbrain.org> 318 */ 319function idfilter($id,$ue=true){ 320 global $conf; 321 if ($conf['useslash'] && $conf['userewrite']){ 322 $id = strtr($id,':','/'); 323 }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 324 $conf['userewrite']) { 325 $id = strtr($id,':',';'); 326 } 327 if($ue){ 328 $id = rawurlencode($id); 329 $id = str_replace('%3A',':',$id); //keep as colon 330 $id = str_replace('%2F','/',$id); //keep as slash 331 } 332 return $id; 333} 334 335/** 336 * This builds a link to a wikipage 337 * 338 * It handles URL rewriting and adds additional parameter if 339 * given in $more 340 * 341 * @author Andreas Gohr <andi@splitbrain.org> 342 */ 343function wl($id='',$more='',$abs=false,$sep='&'){ 344 global $conf; 345 if(is_array($more)){ 346 $more = buildURLparams($more,$sep); 347 }else{ 348 $more = str_replace(',',$sep,$more); 349 } 350 351 $id = idfilter($id); 352 if($abs){ 353 $xlink = DOKU_URL; 354 }else{ 355 $xlink = DOKU_BASE; 356 } 357 358 if($conf['userewrite'] == 2){ 359 $xlink .= DOKU_SCRIPT.'/'.$id; 360 if($more) $xlink .= '?'.$more; 361 }elseif($conf['userewrite']){ 362 $xlink .= $id; 363 if($more) $xlink .= '?'.$more; 364 }elseif($id){ 365 $xlink .= DOKU_SCRIPT.'?id='.$id; 366 if($more) $xlink .= $sep.$more; 367 }else{ 368 $xlink .= DOKU_SCRIPT; 369 if($more) $xlink .= '?'.$more; 370 } 371 372 return $xlink; 373} 374 375/** 376 * This builds a link to an alternate page format 377 * 378 * Handles URL rewriting if enabled. Follows the style of wl(). 379 * 380 * @author Ben Coburn <btcoburn@silicodon.net> 381 */ 382function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&'){ 383 global $conf; 384 if(is_array($more)){ 385 $more = buildURLparams($more,$sep); 386 }else{ 387 $more = str_replace(',',$sep,$more); 388 } 389 390 $format = rawurlencode($format); 391 $id = idfilter($id); 392 if($abs){ 393 $xlink = DOKU_URL; 394 }else{ 395 $xlink = DOKU_BASE; 396 } 397 398 if($conf['userewrite'] == 2){ 399 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 400 if($more) $xlink .= $sep.$more; 401 }elseif($conf['userewrite'] == 1){ 402 $xlink .= '_export/'.$format.'/'.$id; 403 if($more) $xlink .= '?'.$more; 404 }else{ 405 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 406 if($more) $xlink .= $sep.$more; 407 } 408 409 return $xlink; 410} 411 412/** 413 * Build a link to a media file 414 * 415 * Will return a link to the detail page if $direct is false 416 * 417 * The $more parameter should always be given as array, the function then 418 * will strip default parameters to produce even cleaner URLs 419 * 420 * @param string $id - the media file id or URL 421 * @param mixed $more - string or array with additional parameters 422 * @param boolean $direct - link to detail page if false 423 * @param string $sep - URL parameter separator 424 * @param boolean $abs - Create an absolute URL 425 */ 426function ml($id='',$more='',$direct=true,$sep='&',$abs=false){ 427 global $conf; 428 if(is_array($more)){ 429 // strip defaults for shorter URLs 430 if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']); 431 if(!$more['w']) unset($more['w']); 432 if(!$more['h']) unset($more['h']); 433 if(isset($more['id']) && $direct) unset($more['id']); 434 $more = buildURLparams($more,$sep); 435 }else{ 436 $more = str_replace('cache=cache','',$more); //skip default 437 $more = str_replace(',,',',',$more); 438 $more = str_replace(',',$sep,$more); 439 } 440 441 if($abs){ 442 $xlink = DOKU_URL; 443 }else{ 444 $xlink = DOKU_BASE; 445 } 446 447 // external URLs are always direct without rewriting 448 if(preg_match('#^(https?|ftp)://#i',$id)){ 449 $xlink .= 'lib/exe/fetch.php'; 450 if($more){ 451 $xlink .= '?'.$more; 452 $xlink .= $sep.'media='.rawurlencode($id); 453 }else{ 454 $xlink .= '?media='.rawurlencode($id); 455 } 456 return $xlink; 457 } 458 459 $id = idfilter($id); 460 461 // decide on scriptname 462 if($direct){ 463 if($conf['userewrite'] == 1){ 464 $script = '_media'; 465 }else{ 466 $script = 'lib/exe/fetch.php'; 467 } 468 }else{ 469 if($conf['userewrite'] == 1){ 470 $script = '_detail'; 471 }else{ 472 $script = 'lib/exe/detail.php'; 473 } 474 } 475 476 // build URL based on rewrite mode 477 if($conf['userewrite']){ 478 $xlink .= $script.'/'.$id; 479 if($more) $xlink .= '?'.$more; 480 }else{ 481 if($more){ 482 $xlink .= $script.'?'.$more; 483 $xlink .= $sep.'media='.$id; 484 }else{ 485 $xlink .= $script.'?media='.$id; 486 } 487 } 488 489 return $xlink; 490} 491 492 493 494/** 495 * Just builds a link to a script 496 * 497 * @todo maybe obsolete 498 * @author Andreas Gohr <andi@splitbrain.org> 499 */ 500function script($script='doku.php'){ 501# $link = getBaseURL(); 502# $link .= $script; 503# return $link; 504 return DOKU_BASE.DOKU_SCRIPT; 505} 506 507/** 508 * Spamcheck against wordlist 509 * 510 * Checks the wikitext against a list of blocked expressions 511 * returns true if the text contains any bad words 512 * 513 * Triggers COMMON_WORDBLOCK_BLOCKED 514 * 515 * Action Plugins can use this event to inspect the blocked data 516 * and gain information about the user who was blocked. 517 * 518 * Event data: 519 * data['matches'] - array of matches 520 * data['userinfo'] - information about the blocked user 521 * [ip] - ip address 522 * [user] - username (if logged in) 523 * [mail] - mail address (if logged in) 524 * [name] - real name (if logged in) 525 * 526 * @author Andreas Gohr <andi@splitbrain.org> 527 * Michael Klier <chi@chimeric.de> 528 */ 529function checkwordblock(){ 530 global $TEXT; 531 global $conf; 532 global $INFO; 533 534 if(!$conf['usewordblock']) return false; 535 536 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 537 $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT); 538 539 $wordblocks = getWordblocks(); 540 //how many lines to read at once (to work around some PCRE limits) 541 if(version_compare(phpversion(),'4.3.0','<')){ 542 //old versions of PCRE define a maximum of parenthesises even if no 543 //backreferences are used - the maximum is 99 544 //this is very bad performancewise and may even be too high still 545 $chunksize = 40; 546 }else{ 547 //read file in chunks of 200 - this should work around the 548 //MAX_PATTERN_SIZE in modern PCRE 549 $chunksize = 200; 550 } 551 while($blocks = array_splice($wordblocks,0,$chunksize)){ 552 $re = array(); 553 #build regexp from blocks 554 foreach($blocks as $block){ 555 $block = preg_replace('/#.*$/','',$block); 556 $block = trim($block); 557 if(empty($block)) continue; 558 $re[] = $block; 559 } 560 if(count($re) && preg_match('#('.join('|',$re).')#si',$text,$matches)) { 561 //prepare event data 562 $data['matches'] = $matches; 563 $data['userinfo']['ip'] = $_SERVER['REMOTE_ADDR']; 564 if($_SERVER['REMOTE_USER']) { 565 $data['userinfo']['user'] = $_SERVER['REMOTE_USER']; 566 $data['userinfo']['name'] = $INFO['userinfo']['name']; 567 $data['userinfo']['mail'] = $INFO['userinfo']['mail']; 568 } 569 $callback = create_function('', 'return true;'); 570 return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 571 } 572 } 573 return false; 574} 575 576/** 577 * Return the IP of the client 578 * 579 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 580 * 581 * It returns a comma separated list of IPs if the above mentioned 582 * headers are set. If the single parameter is set, it tries to return 583 * a routable public address, prefering the ones suplied in the X 584 * headers 585 * 586 * @param boolean $single If set only a single IP is returned 587 * @author Andreas Gohr <andi@splitbrain.org> 588 */ 589function clientIP($single=false){ 590 $ip = array(); 591 $ip[] = $_SERVER['REMOTE_ADDR']; 592 if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) 593 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])); 594 if(!empty($_SERVER['HTTP_X_REAL_IP'])) 595 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP'])); 596 597 // some IPv4/v6 regexps borrowed from Feyd 598 // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479 599 $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])'; 600 $hex_digit = '[A-Fa-f0-9]'; 601 $h16 = "{$hex_digit}{1,4}"; 602 $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet"; 603 $ls32 = "(?:$h16:$h16|$IPv4Address)"; 604 $IPv6Address = 605 "(?:(?:{$IPv4Address})|(?:". 606 "(?:$h16:){6}$ls32" . 607 "|::(?:$h16:){5}$ls32" . 608 "|(?:$h16)?::(?:$h16:){4}$ls32" . 609 "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32" . 610 "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32" . 611 "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32" . 612 "|(?:(?:$h16:){0,4}$h16)?::$ls32" . 613 "|(?:(?:$h16:){0,5}$h16)?::$h16" . 614 "|(?:(?:$h16:){0,6}$h16)?::" . 615 ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)"; 616 617 // remove any non-IP stuff 618 $cnt = count($ip); 619 $match = array(); 620 for($i=0; $i<$cnt; $i++){ 621 if(preg_match("/^$IPv4Address$/",$ip[$i],$match) || preg_match("/^$IPv6Address$/",$ip[$i],$match)) { 622 $ip[$i] = $match[0]; 623 } else { 624 $ip[$i] = ''; 625 } 626 if(empty($ip[$i])) unset($ip[$i]); 627 } 628 $ip = array_values(array_unique($ip)); 629 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 630 631 if(!$single) return join(',',$ip); 632 633 // decide which IP to use, trying to avoid local addresses 634 $ip = array_reverse($ip); 635 foreach($ip as $i){ 636 if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){ 637 continue; 638 }else{ 639 return $i; 640 } 641 } 642 // still here? just use the first (last) address 643 return $ip[0]; 644} 645 646/** 647 * Check if the browser is on a mobile device 648 * 649 * Adapted from the example code at url below 650 * 651 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 652 */ 653function clientismobile(){ 654 655 if(isset($_SERVER['HTTP_X_WAP_PROFILE'])) return true; 656 657 if(preg_match('/wap\.|\.wap/i',$_SERVER['HTTP_ACCEPT'])) return true; 658 659 if(!isset($_SERVER['HTTP_USER_AGENT'])) return false; 660 661 $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto'; 662 663 if(preg_match("/$uamatches/i",$_SERVER['HTTP_USER_AGENT'])) return true; 664 665 return false; 666} 667 668 669/** 670 * Convert one or more comma separated IPs to hostnames 671 * 672 * @author Glen Harris <astfgl@iamnota.org> 673 * @returns a comma separated list of hostnames 674 */ 675function gethostsbyaddrs($ips){ 676 $hosts = array(); 677 $ips = explode(',',$ips); 678 679 if(is_array($ips)) { 680 foreach($ips as $ip){ 681 $hosts[] = gethostbyaddr(trim($ip)); 682 } 683 return join(',',$hosts); 684 } else { 685 return gethostbyaddr(trim($ips)); 686 } 687} 688 689/** 690 * Checks if a given page is currently locked. 691 * 692 * removes stale lockfiles 693 * 694 * @author Andreas Gohr <andi@splitbrain.org> 695 */ 696function checklock($id){ 697 global $conf; 698 $lock = wikiLockFN($id); 699 700 //no lockfile 701 if(!@file_exists($lock)) return false; 702 703 //lockfile expired 704 if((time() - filemtime($lock)) > $conf['locktime']){ 705 @unlink($lock); 706 return false; 707 } 708 709 //my own lock 710 $ip = io_readFile($lock); 711 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 712 return false; 713 } 714 715 return $ip; 716} 717 718/** 719 * Lock a page for editing 720 * 721 * @author Andreas Gohr <andi@splitbrain.org> 722 */ 723function lock($id){ 724 $lock = wikiLockFN($id); 725 if($_SERVER['REMOTE_USER']){ 726 io_saveFile($lock,$_SERVER['REMOTE_USER']); 727 }else{ 728 io_saveFile($lock,clientIP()); 729 } 730} 731 732/** 733 * Unlock a page if it was locked by the user 734 * 735 * @author Andreas Gohr <andi@splitbrain.org> 736 * @return bool true if a lock was removed 737 */ 738function unlock($id){ 739 $lock = wikiLockFN($id); 740 if(@file_exists($lock)){ 741 $ip = io_readFile($lock); 742 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 743 @unlink($lock); 744 return true; 745 } 746 } 747 return false; 748} 749 750/** 751 * convert line ending to unix format 752 * 753 * @see formText() for 2crlf conversion 754 * @author Andreas Gohr <andi@splitbrain.org> 755 */ 756function cleanText($text){ 757 $text = preg_replace("/(\015\012)|(\015)/","\012",$text); 758 return $text; 759} 760 761/** 762 * Prepares text for print in Webforms by encoding special chars. 763 * It also converts line endings to Windows format which is 764 * pseudo standard for webforms. 765 * 766 * @see cleanText() for 2unix conversion 767 * @author Andreas Gohr <andi@splitbrain.org> 768 */ 769function formText($text){ 770 $text = str_replace("\012","\015\012",$text); 771 return htmlspecialchars($text); 772} 773 774/** 775 * Returns the specified local text in raw format 776 * 777 * @author Andreas Gohr <andi@splitbrain.org> 778 */ 779function rawLocale($id){ 780 return io_readFile(localeFN($id)); 781} 782 783/** 784 * Returns the raw WikiText 785 * 786 * @author Andreas Gohr <andi@splitbrain.org> 787 */ 788function rawWiki($id,$rev=''){ 789 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 790} 791 792/** 793 * Returns the pagetemplate contents for the ID's namespace 794 * 795 * @author Andreas Gohr <andi@splitbrain.org> 796 */ 797function pageTemplate($data){ 798 $id = $data[0]; 799 global $conf; 800 global $INFO; 801 802 $path = dirname(wikiFN($id)); 803 804 if(@file_exists($path.'/_template.txt')){ 805 $tpl = io_readFile($path.'/_template.txt'); 806 }else{ 807 // search upper namespaces for templates 808 $len = strlen(rtrim($conf['datadir'],'/')); 809 while (strlen($path) >= $len){ 810 if(@file_exists($path.'/__template.txt')){ 811 $tpl = io_readFile($path.'/__template.txt'); 812 break; 813 } 814 $path = substr($path, 0, strrpos($path, '/')); 815 } 816 } 817 if(!$tpl) return ''; 818 819 // replace placeholders 820 $tpl = str_replace('@ID@',$id,$tpl); 821 $tpl = str_replace('@NS@',getNS($id),$tpl); 822 $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl); 823 $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl); 824 $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl); 825 $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl); 826 $tpl = str_replace('@DATE@',$conf['dformat'],$tpl); 827 // we need the callback to work around strftime's char limit 828 $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl); 829 830 return $tpl; 831} 832 833 834/** 835 * Returns the raw Wiki Text in three slices. 836 * 837 * The range parameter needs to have the form "from-to" 838 * and gives the range of the section in bytes - no 839 * UTF-8 awareness is needed. 840 * The returned order is prefix, section and suffix. 841 * 842 * @author Andreas Gohr <andi@splitbrain.org> 843 */ 844function rawWikiSlices($range,$id,$rev=''){ 845 list($from,$to) = split('-',$range,2); 846 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 847 if(!$from) $from = 0; 848 if(!$to) $to = strlen($text)+1; 849 850 $slices[0] = substr($text,0,$from-1); 851 $slices[1] = substr($text,$from-1,$to-$from); 852 $slices[2] = substr($text,$to); 853 854 return $slices; 855} 856 857/** 858 * Joins wiki text slices 859 * 860 * function to join the text slices with correct lineendings again. 861 * When the pretty parameter is set to true it adds additional empty 862 * lines between sections if needed (used on saving). 863 * 864 * @author Andreas Gohr <andi@splitbrain.org> 865 */ 866function con($pre,$text,$suf,$pretty=false){ 867 868 if($pretty){ 869 if($pre && substr($pre,-1) != "\n") $pre .= "\n"; 870 if($suf && substr($text,-1) != "\n") $text .= "\n"; 871 } 872 873 // Avoid double newline above section when saving section edit 874 //if($pre) $pre .= "\n"; 875 if($suf) $text .= "\n"; 876 return $pre.$text.$suf; 877} 878 879/** 880 * Saves a wikitext by calling io_writeWikiPage. 881 * Also directs changelog and attic updates. 882 * 883 * @author Andreas Gohr <andi@splitbrain.org> 884 * @author Ben Coburn <btcoburn@silicodon.net> 885 */ 886function saveWikiText($id,$text,$summary,$minor=false){ 887 /* Note to developers: 888 This code is subtle and delicate. Test the behavior of 889 the attic and changelog with dokuwiki and external edits 890 after any changes. External edits change the wiki page 891 directly without using php or dokuwiki. 892 */ 893 global $conf; 894 global $lang; 895 global $REV; 896 // ignore if no changes were made 897 if($text == rawWiki($id,'')){ 898 return; 899 } 900 901 $file = wikiFN($id); 902 $old = @filemtime($file); // from page 903 $wasRemoved = empty($text); 904 $wasCreated = !@file_exists($file); 905 $wasReverted = ($REV==true); 906 $newRev = false; 907 $oldRev = getRevisions($id, -1, 1, 1024); // from changelog 908 $oldRev = (int)(empty($oldRev)?0:$oldRev[0]); 909 if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) { 910 // add old revision to the attic if missing 911 saveOldRevision($id); 912 // add a changelog entry if this edit came from outside dokuwiki 913 if ($old>$oldRev) { 914 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true)); 915 // remove soon to be stale instructions 916 $cache = new cache_instructions($id, $file); 917 $cache->removeCache(); 918 } 919 } 920 921 if ($wasRemoved){ 922 // Send "update" event with empty data, so plugins can react to page deletion 923 $data = array(array($file, '', false), getNS($id), noNS($id), false); 924 trigger_event('IO_WIKIPAGE_WRITE', $data); 925 // pre-save deleted revision 926 @touch($file); 927 clearstatcache(); 928 $newRev = saveOldRevision($id); 929 // remove empty file 930 @unlink($file); 931 // remove old meta info... 932 $mfiles = metaFiles($id); 933 $changelog = metaFN($id, '.changes'); 934 $metadata = metaFN($id, '.meta'); 935 foreach ($mfiles as $mfile) { 936 // but keep per-page changelog to preserve page history and keep meta data 937 if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); } 938 } 939 // purge meta data 940 p_purge_metadata($id); 941 $del = true; 942 // autoset summary on deletion 943 if(empty($summary)) $summary = $lang['deleted']; 944 // remove empty namespaces 945 io_sweepNS($id, 'datadir'); 946 io_sweepNS($id, 'mediadir'); 947 }else{ 948 // save file (namespace dir is created in io_writeWikiPage) 949 io_writeWikiPage($file, $text, $id); 950 // pre-save the revision, to keep the attic in sync 951 $newRev = saveOldRevision($id); 952 $del = false; 953 } 954 955 // select changelog line type 956 $extra = ''; 957 $type = DOKU_CHANGE_TYPE_EDIT; 958 if ($wasReverted) { 959 $type = DOKU_CHANGE_TYPE_REVERT; 960 $extra = $REV; 961 } 962 else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; } 963 else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; } 964 else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users 965 966 addLogEntry($newRev, $id, $type, $summary, $extra); 967 // send notify mails 968 notify($id,'admin',$old,$summary,$minor); 969 notify($id,'subscribers',$old,$summary,$minor); 970 971 // update the purgefile (timestamp of the last time anything within the wiki was changed) 972 io_saveFile($conf['cachedir'].'/purgefile',time()); 973 974 // if useheading is enabled, purge the cache of all linking pages 975 if($conf['useheading']){ 976 require_once(DOKU_INC.'inc/fulltext.php'); 977 $pages = ft_backlinks($id); 978 foreach ($pages as $page) { 979 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 980 $cache->removeCache(); 981 } 982 } 983} 984 985/** 986 * moves the current version to the attic and returns its 987 * revision date 988 * 989 * @author Andreas Gohr <andi@splitbrain.org> 990 */ 991function saveOldRevision($id){ 992 global $conf; 993 $oldf = wikiFN($id); 994 if(!@file_exists($oldf)) return ''; 995 $date = filemtime($oldf); 996 $newf = wikiFN($id,$date); 997 io_writeWikiPage($newf, rawWiki($id), $id, $date); 998 return $date; 999} 1000 1001/** 1002 * Sends a notify mail on page change 1003 * 1004 * @param string $id The changed page 1005 * @param string $who Who to notify (admin|subscribers) 1006 * @param int $rev Old page revision 1007 * @param string $summary What changed 1008 * @param boolean $minor Is this a minor edit? 1009 * @param array $replace Additional string substitutions, @KEY@ to be replaced by value 1010 * 1011 * @author Andreas Gohr <andi@splitbrain.org> 1012 */ 1013function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ 1014 global $lang; 1015 global $conf; 1016 global $INFO; 1017 1018 // decide if there is something to do 1019 if($who == 'admin'){ 1020 if(empty($conf['notify'])) return; //notify enabled? 1021 $text = rawLocale('mailtext'); 1022 $to = $conf['notify']; 1023 $bcc = ''; 1024 }elseif($who == 'subscribers'){ 1025 if(!$conf['subscribers']) return; //subscribers enabled? 1026 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 1027 $bcc = subscriber_addresslist($id,false); 1028 if(empty($bcc)) return; 1029 $to = ''; 1030 $text = rawLocale('subscribermail'); 1031 }elseif($who == 'register'){ 1032 if(empty($conf['registernotify'])) return; 1033 $text = rawLocale('registermail'); 1034 $to = $conf['registernotify']; 1035 $bcc = ''; 1036 }else{ 1037 return; //just to be safe 1038 } 1039 1040 $ip = clientIP(); 1041 $text = str_replace('@DATE@',strftime($conf['dformat']),$text); 1042 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 1043 $text = str_replace('@IPADDRESS@',$ip,$text); 1044 $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); 1045 $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text); 1046 $text = str_replace('@PAGE@',$id,$text); 1047 $text = str_replace('@TITLE@',$conf['title'],$text); 1048 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 1049 $text = str_replace('@SUMMARY@',$summary,$text); 1050 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 1051 1052 foreach ($replace as $key => $substitution) { 1053 $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); 1054 } 1055 1056 if($who == 'register'){ 1057 $subject = $lang['mail_new_user'].' '.$summary; 1058 }elseif($rev){ 1059 $subject = $lang['mail_changed'].' '.$id; 1060 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); 1061 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 1062 $df = new Diff(split("\n",rawWiki($id,$rev)), 1063 split("\n",rawWiki($id))); 1064 $dformat = new UnifiedDiffFormatter(); 1065 $diff = $dformat->format($df); 1066 }else{ 1067 $subject=$lang['mail_newpage'].' '.$id; 1068 $text = str_replace('@OLDPAGE@','none',$text); 1069 $diff = rawWiki($id); 1070 } 1071 $text = str_replace('@DIFF@',$diff,$text); 1072 $subject = '['.$conf['title'].'] '.$subject; 1073 1074 $from = $conf['mailfrom']; 1075 $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from); 1076 $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from); 1077 $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from); 1078 1079 mail_send($to,$subject,$text,$from,'',$bcc); 1080} 1081 1082/** 1083 * extracts the query from a search engine referrer 1084 * 1085 * @author Andreas Gohr <andi@splitbrain.org> 1086 * @author Todd Augsburger <todd@rollerorgans.com> 1087 */ 1088function getGoogleQuery(){ 1089 $url = parse_url($_SERVER['HTTP_REFERER']); 1090 if(!$url) return ''; 1091 1092 $query = array(); 1093 parse_str($url['query'],$query); 1094 if(isset($query['q'])) 1095 $q = $query['q']; // google, live/msn, aol, ask, altavista, alltheweb, gigablast 1096 elseif(isset($query['p'])) 1097 $q = $query['p']; // yahoo 1098 elseif(isset($query['query'])) 1099 $q = $query['query']; // lycos, netscape, clusty, hotbot 1100 elseif(preg_match("#a9\.com#i",$url['host'])) // a9 1101 $q = urldecode(ltrim($url['path'],'/')); 1102 1103 if(!$q) return ''; 1104 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY); 1105 return $q; 1106} 1107 1108/** 1109 * Try to set correct locale 1110 * 1111 * @deprecated No longer used 1112 * @author Andreas Gohr <andi@splitbrain.org> 1113 */ 1114function setCorrectLocale(){ 1115 global $conf; 1116 global $lang; 1117 1118 $enc = strtoupper($lang['encoding']); 1119 foreach ($lang['locales'] as $loc){ 1120 //try locale 1121 if(@setlocale(LC_ALL,$loc)) return; 1122 //try loceale with encoding 1123 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1124 } 1125 //still here? try to set from environment 1126 @setlocale(LC_ALL,""); 1127} 1128 1129/** 1130 * Return the human readable size of a file 1131 * 1132 * @param int $size A file size 1133 * @param int $dec A number of decimal places 1134 * @author Martin Benjamin <b.martin@cybernet.ch> 1135 * @author Aidan Lister <aidan@php.net> 1136 * @version 1.0.0 1137 */ 1138function filesize_h($size, $dec = 1){ 1139 $sizes = array('B', 'KB', 'MB', 'GB'); 1140 $count = count($sizes); 1141 $i = 0; 1142 1143 while ($size >= 1024 && ($i < $count - 1)) { 1144 $size /= 1024; 1145 $i++; 1146 } 1147 1148 return round($size, $dec) . ' ' . $sizes[$i]; 1149} 1150 1151/** 1152 * return an obfuscated email address in line with $conf['mailguard'] setting 1153 * 1154 * @author Harry Fuecks <hfuecks@gmail.com> 1155 * @author Christopher Smith <chris@jalakai.co.uk> 1156 */ 1157function obfuscate($email) { 1158 global $conf; 1159 1160 switch ($conf['mailguard']) { 1161 case 'visible' : 1162 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1163 return strtr($email, $obfuscate); 1164 1165 case 'hex' : 1166 $encode = ''; 1167 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1168 return $encode; 1169 1170 case 'none' : 1171 default : 1172 return $email; 1173 } 1174} 1175 1176/** 1177 * Let us know if a user is tracking a page or a namespace 1178 * 1179 * @author Andreas Gohr <andi@splitbrain.org> 1180 */ 1181function is_subscribed($id,$uid,$ns=false){ 1182 if(!$ns) { 1183 $file=metaFN($id,'.mlist'); 1184 } else { 1185 if(!getNS($id)) { 1186 $file = metaFN(getNS($id),'.mlist'); 1187 } else { 1188 $file = metaFN(getNS($id),'/.mlist'); 1189 } 1190 } 1191 if (@file_exists($file)) { 1192 $mlist = file($file); 1193 $pos = array_search($uid."\n",$mlist); 1194 return is_int($pos); 1195 } 1196 1197 return false; 1198} 1199 1200/** 1201 * Return a string with the email addresses of all the 1202 * users subscribed to a page 1203 * 1204 * @author Steven Danz <steven-danz@kc.rr.com> 1205 */ 1206function subscriber_addresslist($id,$self=true){ 1207 global $conf; 1208 global $auth; 1209 1210 if (!$conf['subscribers']) return ''; 1211 1212 $users = array(); 1213 $emails = array(); 1214 1215 // load the page mlist file content 1216 $mlist = array(); 1217 $file=metaFN($id,'.mlist'); 1218 if (@file_exists($file)) { 1219 $mlist = file($file); 1220 foreach ($mlist as $who) { 1221 $who = rtrim($who); 1222 if(!$self && $who == $_SERVER['REMOTE_USER']) continue; 1223 $users[$who] = true; 1224 } 1225 } 1226 1227 // load also the namespace mlist file content 1228 $ns = getNS($id); 1229 while ($ns) { 1230 $nsfile = metaFN($ns,'/.mlist'); 1231 if (@file_exists($nsfile)) { 1232 $mlist = file($nsfile); 1233 foreach ($mlist as $who) { 1234 $who = rtrim($who); 1235 if(!$self && $who == $_SERVER['REMOTE_USER']) continue; 1236 $users[$who] = true; 1237 } 1238 } 1239 $ns = getNS($ns); 1240 } 1241 // root namespace 1242 $nsfile = metaFN('','.mlist'); 1243 if (@file_exists($nsfile)) { 1244 $mlist = file($nsfile); 1245 foreach ($mlist as $who) { 1246 $who = rtrim($who); 1247 if(!$self && $who == $_SERVER['REMOTE_USER']) continue; 1248 $users[$who] = true; 1249 } 1250 } 1251 if(!empty($users)) { 1252 foreach (array_keys($users) as $who) { 1253 $info = $auth->getUserData($who); 1254 if($info === false) continue; 1255 $level = auth_aclcheck($id,$who,$info['grps']); 1256 if ($level >= AUTH_READ) { 1257 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1258 $emails[] = $info['mail']; 1259 } 1260 } 1261 } 1262 } 1263 1264 return implode(',',$emails); 1265} 1266 1267/** 1268 * Removes quoting backslashes 1269 * 1270 * @author Andreas Gohr <andi@splitbrain.org> 1271 */ 1272function unslash($string,$char="'"){ 1273 return str_replace('\\'.$char,$char,$string); 1274} 1275 1276/** 1277 * Convert php.ini shorthands to byte 1278 * 1279 * @author <gilthans dot NO dot SPAM at gmail dot com> 1280 * @link http://de3.php.net/manual/en/ini.core.php#79564 1281 */ 1282function php_to_byte($v){ 1283 $l = substr($v, -1); 1284 $ret = substr($v, 0, -1); 1285 switch(strtoupper($l)){ 1286 case 'P': 1287 $ret *= 1024; 1288 case 'T': 1289 $ret *= 1024; 1290 case 'G': 1291 $ret *= 1024; 1292 case 'M': 1293 $ret *= 1024; 1294 case 'K': 1295 $ret *= 1024; 1296 break; 1297 } 1298 return $ret; 1299} 1300 1301/** 1302 * Wrapper around preg_quote adding the default delimiter 1303 */ 1304function preg_quote_cb($string){ 1305 return preg_quote($string,'/'); 1306} 1307 1308/** 1309 * Shorten a given string by removing data from the middle 1310 * 1311 * You can give the string in two parts, teh first part $keep 1312 * will never be shortened. The second part $short will be cut 1313 * in the middle to shorten but only if at least $min chars are 1314 * left to display it. Otherwise it will be left off. 1315 * 1316 * @param string $keep the part to keep 1317 * @param string $short the part to shorten 1318 * @param int $max maximum chars you want for the whole string 1319 * @param int $min minimum number of chars to have left for middle shortening 1320 * @param string $char the shortening character to use 1321 */ 1322function shorten($keep,$short,$max,$min=9,$char='⌇'){ 1323 $max = $max - utf8_strlen($keep); 1324 if($max < $min) return $keep; 1325 $len = utf8_strlen($short); 1326 if($len <= $max) return $keep.$short; 1327 $half = floor($max/2); 1328 return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half); 1329} 1330 1331/** 1332 * Return the users realname or e-mail address for use 1333 * in page footer and recent changes pages 1334 * 1335 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1336 */ 1337function editorinfo($username){ 1338 global $conf; 1339 global $auth; 1340 1341 switch($conf['showuseras']){ 1342 case 'username': 1343 case 'email': 1344 case 'email_link': 1345 $info = $auth->getUserData($username); 1346 break; 1347 default: 1348 return hsc($username); 1349 } 1350 1351 if(isset($info) && $info) { 1352 switch($conf['showuseras']){ 1353 case 'username': 1354 return hsc($info['name']); 1355 case 'email': 1356 return obfuscate($info['mail']); 1357 case 'email_link': 1358 $mail=obfuscate($info['mail']); 1359 return '<a href="mailto:'.$mail.'">'.$mail.'</a>'; 1360 default: 1361 return hsc($username); 1362 } 1363 } else { 1364 return hsc($username); 1365 } 1366} 1367 1368/** 1369 * Returns the path to a image file for the currently chosen license. 1370 * When no image exists, returns an empty string 1371 * 1372 * @author Andreas Gohr <andi@splitbrain.org> 1373 * @param string $type - type of image 'badge' or 'button' 1374 */ 1375function license_img($type){ 1376 global $license; 1377 global $conf; 1378 if(!$conf['license']) return ''; 1379 if(!is_array($license[$conf['license']])) return ''; 1380 $lic = $license[$conf['license']]; 1381 $try = array(); 1382 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1383 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1384 if(substr($conf['license'],0,3) == 'cc-'){ 1385 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1386 } 1387 foreach($try as $src){ 1388 if(@file_exists(DOKU_INC.$src)) return $src; 1389 } 1390 return ''; 1391} 1392 1393//Setup VIM: ex: et ts=2 enc=utf-8 : 1394