[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> io.php (source)

   1  <?php
   2  
   3  /**
   4   * File IO functions
   5   *
   6   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   7   * @author     Andreas Gohr <andi@splitbrain.org>
   8   */
   9  
  10  use dokuwiki\Utf8\PhpString;
  11  use dokuwiki\HTTP\DokuHTTPClient;
  12  use dokuwiki\Extension\Event;
  13  
  14  /**
  15   * Removes empty directories
  16   *
  17   * Sends IO_NAMESPACE_DELETED events for 'pages' and 'media' namespaces.
  18   * Event data:
  19   * $data[0]    ns: The colon separated namespace path minus the trailing page name.
  20   * $data[1]    ns_type: 'pages' or 'media' namespace tree.
  21   *
  22   * @param string $id - a pageid, the namespace of that id will be tried to deleted
  23   * @param string $basedir - the config name of the type to delete (datadir or mediadir usally)
  24   * @return bool - true if at least one namespace was deleted
  25   *
  26   * @author  Andreas Gohr <andi@splitbrain.org>
  27   * @author Ben Coburn <btcoburn@silicodon.net>
  28   */
  29  function io_sweepNS($id, $basedir = 'datadir')
  30  {
  31      global $conf;
  32      $types = ['datadir' => 'pages', 'mediadir' => 'media'];
  33      $ns_type = ($types[$basedir] ?? false);
  34  
  35      $delone = false;
  36  
  37      //scan all namespaces
  38      while (($id = getNS($id)) !== false) {
  39          $dir = $conf[$basedir] . '/' . utf8_encodeFN(str_replace(':', '/', $id));
  40  
  41          //try to delete dir else return
  42          if (@rmdir($dir)) {
  43              if ($ns_type !== false) {
  44                  $data = [$id, $ns_type];
  45                  $delone = true; // we deleted at least one dir
  46                  Event::createAndTrigger('IO_NAMESPACE_DELETED', $data);
  47              }
  48          } else {
  49              return $delone;
  50          }
  51      }
  52      return $delone;
  53  }
  54  
  55  /**
  56   * Used to read in a DokuWiki page from file, and send IO_WIKIPAGE_READ events.
  57   *
  58   * Generates the action event which delegates to io_readFile().
  59   * Action plugins are allowed to modify the page content in transit.
  60   * The file path should not be changed.
  61   *
  62   * Event data:
  63   * $data[0]    The raw arguments for io_readFile as an array.
  64   * $data[1]    ns: The colon separated namespace path minus the trailing page name. (false if root ns)
  65   * $data[2]    page_name: The wiki page name.
  66   * $data[3]    rev: The page revision, false for current wiki pages.
  67   *
  68   * @param string $file filename
  69   * @param string $id page id
  70   * @param bool|int|string $rev revision timestamp
  71   * @return string
  72   *
  73   * @author Ben Coburn <btcoburn@silicodon.net>
  74   */
  75  function io_readWikiPage($file, $id, $rev = false)
  76  {
  77      if (empty($rev)) {
  78          $rev = false;
  79      }
  80      $data = [[$file, true], getNS($id), noNS($id), $rev];
  81      return Event::createAndTrigger('IO_WIKIPAGE_READ', $data, '_io_readWikiPage_action', false);
  82  }
  83  
  84  /**
  85   * Callback adapter for io_readFile().
  86   *
  87   * @param array $data event data
  88   * @return string
  89   *
  90   * @author Ben Coburn <btcoburn@silicodon.net>
  91   */
  92  function _io_readWikiPage_action($data)
  93  {
  94      if (is_array($data) && is_array($data[0]) && count($data[0]) === 2) {
  95          return io_readFile(...$data[0]);
  96      } else {
  97          return ''; //callback error
  98      }
  99  }
 100  
 101  /**
 102   * Returns content of $file as cleaned string.
 103   *
 104   * Uses gzip if extension is .gz
 105   *
 106   * If you want to use the returned value in unserialize
 107   * be sure to set $clean to false!
 108   *
 109   *
 110   * @param string $file filename
 111   * @param bool $clean
 112   * @return string|bool the file contents or false on error
 113   *
 114   * @author  Andreas Gohr <andi@splitbrain.org>
 115   */
 116  function io_readFile($file, $clean = true)
 117  {
 118      $ret = '';
 119      if (file_exists($file)) {
 120          if (str_ends_with($file, '.gz')) {
 121              if (!DOKU_HAS_GZIP) return false;
 122              $ret = gzfile($file);
 123              if (is_array($ret)) {
 124                  $ret = implode('', $ret);
 125              }
 126          } elseif (str_ends_with($file, '.bz2')) {
 127              if (!DOKU_HAS_BZIP) return false;
 128              $ret = bzfile($file);
 129          } else {
 130              $ret = file_get_contents($file);
 131          }
 132      }
 133      if ($ret === null) return false;
 134      if ($ret !== false && $clean) {
 135          return cleanText($ret);
 136      } else {
 137          return $ret;
 138      }
 139  }
 140  
 141  /**
 142   * Returns the content of a .bz2 compressed file as string
 143   *
 144   * @param string $file filename
 145   * @param bool $array return array of lines
 146   * @return string|array|bool content or false on error
 147   *
 148   * @author marcel senf <marcel@rucksackreinigung.de>
 149   * @author  Andreas Gohr <andi@splitbrain.org>
 150   */
 151  function bzfile($file, $array = false)
 152  {
 153      $bz = bzopen($file, "r");
 154      if ($bz === false) return false;
 155  
 156      if ($array) {
 157          $lines = [];
 158      }
 159      $str = '';
 160      while (!feof($bz)) {
 161          //8192 seems to be the maximum buffersize?
 162          $buffer = bzread($bz, 8192);
 163          if (($buffer === false) || (bzerrno($bz) !== 0)) {
 164              return false;
 165          }
 166          $str .= $buffer;
 167          if ($array) {
 168              $pos = strpos($str, "\n");
 169              while ($pos !== false) {
 170                  $lines[] = substr($str, 0, $pos + 1);
 171                  $str = substr($str, $pos + 1);
 172                  $pos = strpos($str, "\n");
 173              }
 174          }
 175      }
 176      bzclose($bz);
 177      if ($array) {
 178          if ($str !== '') {
 179              $lines[] = $str;
 180          }
 181          return $lines;
 182      }
 183      return $str;
 184  }
 185  
 186  /**
 187   * Used to write out a DokuWiki page to file, and send IO_WIKIPAGE_WRITE events.
 188   *
 189   * This generates an action event and delegates to io_saveFile().
 190   * Action plugins are allowed to modify the page content in transit.
 191   * The file path should not be changed.
 192   * (The append parameter is set to false.)
 193   *
 194   * Event data:
 195   * $data[0]    The raw arguments for io_saveFile as an array.
 196   * $data[1]    ns: The colon separated namespace path minus the trailing page name. (false if root ns)
 197   * $data[2]    page_name: The wiki page name.
 198   * $data[3]    rev: The page revision, false for current wiki pages.
 199   *
 200   * @param string $file filename
 201   * @param string $content
 202   * @param string $id page id
 203   * @param int|bool|string $rev timestamp of revision
 204   * @return bool
 205   *
 206   * @author Ben Coburn <btcoburn@silicodon.net>
 207   */
 208  function io_writeWikiPage($file, $content, $id, $rev = false)
 209  {
 210      if (empty($rev)) {
 211          $rev = false;
 212      }
 213      if ($rev === false) {
 214          io_createNamespace($id); // create namespaces as needed
 215      }
 216      $data = [[$file, $content, false], getNS($id), noNS($id), $rev];
 217      return Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data, '_io_writeWikiPage_action', false);
 218  }
 219  
 220  /**
 221   * Callback adapter for io_saveFile().
 222   *
 223   * @param array $data event data
 224   * @return bool
 225   *
 226   * @author Ben Coburn <btcoburn@silicodon.net>
 227   */
 228  function _io_writeWikiPage_action($data)
 229  {
 230      if (is_array($data) && is_array($data[0]) && count($data[0]) === 3) {
 231          $ok = io_saveFile(...$data[0]);
 232          // for attic files make sure the file has the mtime of the revision
 233          if ($ok && is_int($data[3]) && $data[3] > 0) {
 234              @touch($data[0][0], $data[3]);
 235          }
 236          return $ok;
 237      } else {
 238          return false; //callback error
 239      }
 240  }
 241  
 242  /**
 243   * Internal function to save contents to a file.
 244   *
 245   * @param string $file filename path to file
 246   * @param string $content
 247   * @param bool $append
 248   * @return bool true on success, otherwise false
 249   *
 250   * @author  Andreas Gohr <andi@splitbrain.org>
 251   */
 252  function _io_saveFile($file, $content, $append)
 253  {
 254      global $conf;
 255      $mode = ($append) ? 'ab' : 'wb';
 256      $fileexists = file_exists($file);
 257  
 258      if (str_ends_with($file, '.gz')) {
 259          if (!DOKU_HAS_GZIP) return false;
 260          $fh = @gzopen($file, $mode . '9');
 261          if (!$fh) return false;
 262          gzwrite($fh, $content);
 263          gzclose($fh);
 264      } elseif (str_ends_with($file, '.bz2')) {
 265          if (!DOKU_HAS_BZIP) return false;
 266          if ($append) {
 267              $bzcontent = bzfile($file);
 268              if ($bzcontent === false) return false;
 269              $content = $bzcontent . $content;
 270          }
 271          $fh = @bzopen($file, 'w');
 272          if (!$fh) return false;
 273          bzwrite($fh, $content);
 274          bzclose($fh);
 275      } else {
 276          $fh = @fopen($file, $mode);
 277          if (!$fh) return false;
 278          fwrite($fh, $content);
 279          fclose($fh);
 280      }
 281  
 282      if (!$fileexists && $conf['fperm']) {
 283          chmod($file, $conf['fperm']);
 284      }
 285      return true;
 286  }
 287  
 288  /**
 289   * Saves $content to $file.
 290   *
 291   * If the third parameter is set to true the given content
 292   * will be appended.
 293   *
 294   * Uses gzip if extension is .gz
 295   * and bz2 if extension is .bz2
 296   *
 297   * @param string $file filename path to file
 298   * @param string $content
 299   * @param bool $append
 300   * @return bool true on success, otherwise false
 301   *
 302   * @author  Andreas Gohr <andi@splitbrain.org>
 303   */
 304  function io_saveFile($file, $content, $append = false)
 305  {
 306      io_makeFileDir($file);
 307      io_lock($file);
 308      if (!_io_saveFile($file, $content, $append)) {
 309          msg("Writing $file failed", -1);
 310          io_unlock($file);
 311          return false;
 312      }
 313      io_unlock($file);
 314      return true;
 315  }
 316  
 317  /**
 318   * Replace one or more occurrences of a line in a file.
 319   *
 320   * The default, when $maxlines is 0 is to delete all matching lines then append a single line.
 321   * A regex that matches any part of the line will remove the entire line in this mode.
 322   * Captures in $newline are not available.
 323   *
 324   * Otherwise each line is matched and replaced individually, up to the first $maxlines lines
 325   * or all lines if $maxlines is -1. If $regex is true then captures can be used in $newline.
 326   *
 327   * Be sure to include the trailing newline in $oldline when replacing entire lines.
 328   *
 329   * Uses gzip if extension is .gz
 330   * and bz2 if extension is .bz2
 331   *
 332   * @param string $file filename
 333   * @param string $oldline exact linematch to remove
 334   * @param string $newline new line to insert
 335   * @param bool $regex use regexp?
 336   * @param int $maxlines number of occurrences of the line to replace
 337   * @return bool true on success
 338   *
 339   * @author Steven Danz <steven-danz@kc.rr.com>
 340   * @author Christopher Smith <chris@jalakai.co.uk>
 341   * @author Patrick Brown <ptbrown@whoopdedo.org>
 342   */
 343  function io_replaceInFile($file, $oldline, $newline, $regex = false, $maxlines = 0)
 344  {
 345      if ((string)$oldline === '') {
 346          trigger_error('$oldline parameter cannot be empty in io_replaceInFile()', E_USER_WARNING);
 347          return false;
 348      }
 349  
 350      if (!file_exists($file)) return true;
 351  
 352      io_lock($file);
 353  
 354      // load into array
 355      if (str_ends_with($file, '.gz')) {
 356          if (!DOKU_HAS_GZIP) return false;
 357          $lines = gzfile($file);
 358      } elseif (str_ends_with($file, '.bz2')) {
 359          if (!DOKU_HAS_BZIP) return false;
 360          $lines = bzfile($file, true);
 361      } else {
 362          $lines = file($file);
 363      }
 364  
 365      // make non-regexes into regexes
 366      $pattern = $regex ? $oldline : '/^' . preg_quote($oldline, '/') . '$/';
 367      $replace = $regex ? $newline : addcslashes($newline, '\$');
 368  
 369      // remove matching lines
 370      if ($maxlines > 0) {
 371          $count = 0;
 372          $matched = 0;
 373          foreach ($lines as $i => $line) {
 374              if ($count >= $maxlines) break;
 375              // $matched will be set to 0|1 depending on whether pattern is matched and line replaced
 376              $lines[$i] = preg_replace($pattern, $replace, $line, -1, $matched);
 377              if ($matched) {
 378                  $count++;
 379              }
 380          }
 381      } elseif ($maxlines == 0) {
 382          $lines = preg_grep($pattern, $lines, PREG_GREP_INVERT);
 383          if ((string)$newline !== '') {
 384              $lines[] = $newline;
 385          }
 386      } else {
 387          $lines = preg_replace($pattern, $replace, $lines);
 388      }
 389  
 390      if (count($lines)) {
 391          if (!_io_saveFile($file, implode('', $lines), false)) {
 392              msg("Removing content from $file failed", -1);
 393              io_unlock($file);
 394              return false;
 395          }
 396      } else {
 397          @unlink($file);
 398      }
 399  
 400      io_unlock($file);
 401      return true;
 402  }
 403  
 404  /**
 405   * Delete lines that match $badline from $file.
 406   *
 407   * Be sure to include the trailing newline in $badline
 408   *
 409   * @param string $file filename
 410   * @param string $badline exact linematch to remove
 411   * @param bool $regex use regexp?
 412   * @return bool true on success
 413   *
 414   * @author Patrick Brown <ptbrown@whoopdedo.org>
 415   */
 416  function io_deleteFromFile($file, $badline, $regex = false)
 417  {
 418      return io_replaceInFile($file, $badline, '', $regex, 0);
 419  }
 420  
 421  /**
 422   * Tries to lock a file
 423   *
 424   * Locking is only done for io_savefile and uses directories
 425   * inside $conf['lockdir']
 426   *
 427   * It waits maximal 3 seconds for the lock, after this time
 428   * the lock is assumed to be stale and the function goes on
 429   *
 430   * @param string $file filename
 431   *
 432   * @author Andreas Gohr <andi@splitbrain.org>
 433   */
 434  function io_lock($file)
 435  {
 436      global $conf;
 437  
 438      $lockDir = $conf['lockdir'] . '/' . md5($file);
 439      @ignore_user_abort(1);
 440  
 441      $timeStart = time();
 442      do {
 443          //waited longer than 3 seconds? -> stale lock
 444          if ((time() - $timeStart) > 3) break;
 445          $locked = @mkdir($lockDir);
 446          if ($locked) {
 447              if ($conf['dperm']) {
 448                  chmod($lockDir, $conf['dperm']);
 449              }
 450              break;
 451          }
 452          usleep(50);
 453      } while ($locked === false);
 454  }
 455  
 456  /**
 457   * Unlocks a file
 458   *
 459   * @param string $file filename
 460   *
 461   * @author Andreas Gohr <andi@splitbrain.org>
 462   */
 463  function io_unlock($file)
 464  {
 465      global $conf;
 466  
 467      $lockDir = $conf['lockdir'] . '/' . md5($file);
 468      @rmdir($lockDir);
 469      @ignore_user_abort(0);
 470  }
 471  
 472  /**
 473   * Create missing namespace directories and send the IO_NAMESPACE_CREATED events
 474   * in the order of directory creation. (Parent directories first.)
 475   *
 476   * Event data:
 477   * $data[0]    ns: The colon separated namespace path minus the trailing page name.
 478   * $data[1]    ns_type: 'pages' or 'media' namespace tree.
 479   *
 480   * @param string $id page id
 481   * @param string $ns_type 'pages' or 'media'
 482   *
 483   * @author Ben Coburn <btcoburn@silicodon.net>
 484   */
 485  function io_createNamespace($id, $ns_type = 'pages')
 486  {
 487      // verify ns_type
 488      $types = ['pages' => 'wikiFN', 'media' => 'mediaFN'];
 489      if (!isset($types[$ns_type])) {
 490          trigger_error('Bad $ns_type parameter for io_createNamespace().');
 491          return;
 492      }
 493      // make event list
 494      $missing = [];
 495      $ns_stack = explode(':', $id);
 496      $ns = $id;
 497      $tmp = dirname($file = call_user_func($types[$ns_type], $ns));
 498      while (!@is_dir($tmp) && !(file_exists($tmp) && !is_dir($tmp))) {
 499          array_pop($ns_stack);
 500          $ns = implode(':', $ns_stack);
 501          if (strlen($ns) == 0) {
 502              break;
 503          }
 504          $missing[] = $ns;
 505          $tmp = dirname(call_user_func($types[$ns_type], $ns));
 506      }
 507      // make directories
 508      io_makeFileDir($file);
 509      // send the events
 510      $missing = array_reverse($missing); // inside out
 511      foreach ($missing as $ns) {
 512          $data = [$ns, $ns_type];
 513          Event::createAndTrigger('IO_NAMESPACE_CREATED', $data);
 514      }
 515  }
 516  
 517  /**
 518   * Create the directory needed for the given file
 519   *
 520   * @param string $file file name
 521   *
 522   * @author  Andreas Gohr <andi@splitbrain.org>
 523   */
 524  function io_makeFileDir($file)
 525  {
 526      $dir = dirname($file);
 527      if (!@is_dir($dir)) {
 528          if (!io_mkdir_p($dir)) {
 529              msg("Creating directory $dir failed", -1);
 530          }
 531      }
 532  }
 533  
 534  /**
 535   * Creates a directory hierachy.
 536   *
 537   * @param string $target filename
 538   * @return bool
 539   *
 540   * @link    http://php.net/manual/en/function.mkdir.php
 541   * @author  <saint@corenova.com>
 542   * @author  Andreas Gohr <andi@splitbrain.org>
 543   */
 544  function io_mkdir_p($target)
 545  {
 546      global $conf;
 547      if (@is_dir($target) || empty($target)) return true; // best case check first
 548      if (file_exists($target) && !is_dir($target)) return false;
 549      //recursion
 550      if (io_mkdir_p(substr($target, 0, strrpos($target, '/')))) {
 551          $ret = @mkdir($target); // crawl back up & create dir tree
 552          if ($ret && !empty($conf['dperm'])) {
 553              chmod($target, $conf['dperm']);
 554          }
 555          return $ret;
 556      }
 557      return false;
 558  }
 559  
 560  /**
 561   * Recursively delete a directory
 562   *
 563   * @param string $path
 564   * @param bool $removefiles defaults to false which will delete empty directories only
 565   * @return bool
 566   *
 567   * @author Andreas Gohr <andi@splitbrain.org>
 568   */
 569  function io_rmdir($path, $removefiles = false)
 570  {
 571      if (!is_string($path) || $path == "") return false;
 572      if (!file_exists($path)) return true; // it's already gone or was never there, count as success
 573  
 574      if (is_dir($path) && !is_link($path)) {
 575          $dirs = [];
 576          $files = [];
 577          if (!$dh = @opendir($path)) return false;
 578          while (false !== ($f = readdir($dh))) {
 579              if ($f == '..' || $f == '.') continue;
 580  
 581              // collect dirs and files first
 582              if (is_dir("$path/$f") && !is_link("$path/$f")) {
 583                  $dirs[] = "$path/$f";
 584              } elseif ($removefiles) {
 585                  $files[] = "$path/$f";
 586              } else {
 587                  return false; // abort when non empty
 588              }
 589          }
 590          closedir($dh);
 591          // now traverse into  directories first
 592          foreach ($dirs as $dir) {
 593              if (!io_rmdir($dir, $removefiles)) return false; // abort on any error
 594          }
 595          // now delete files
 596          foreach ($files as $file) {
 597              if (!@unlink($file)) return false; //abort on any error
 598          }
 599          // remove self
 600          return @rmdir($path);
 601      } elseif ($removefiles) {
 602          return @unlink($path);
 603      }
 604      return false;
 605  }
 606  
 607  /**
 608   * Creates a unique temporary directory and returns
 609   * its path.
 610   *
 611   * @return false|string path to new directory or false
 612   * @throws Exception
 613   *
 614   * @author Michael Klier <chi@chimeric.de>
 615   */
 616  function io_mktmpdir()
 617  {
 618      global $conf;
 619  
 620      $base = $conf['tmpdir'];
 621      $dir = md5(uniqid(random_int(0, mt_getrandmax()), true));
 622      $tmpdir = $base . '/' . $dir;
 623  
 624      if (io_mkdir_p($tmpdir)) {
 625          return $tmpdir;
 626      } else {
 627          return false;
 628      }
 629  }
 630  
 631  /**
 632   * downloads a file from the net and saves it
 633   *
 634   * if $useAttachment is false,
 635   * - $file is the full filename to save the file, incl. path
 636   * - if successful will return true, false otherwise
 637   *
 638   * if $useAttachment is true,
 639   * - $file is the directory where the file should be saved
 640   * - if successful will return the name used for the saved file, false otherwise
 641   *
 642   * @param string $url url to download
 643   * @param string $file path to file or directory where to save
 644   * @param bool $useAttachment true: try to use name of download, uses otherwise $defaultName
 645   *                            false: uses $file as path to file
 646   * @param string $defaultName fallback for if using $useAttachment
 647   * @param int $maxSize maximum file size
 648   * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
 649   *
 650   * @author Andreas Gohr <andi@splitbrain.org>
 651   * @author Chris Smith <chris@jalakai.co.uk>
 652   */
 653  function io_download($url, $file, $useAttachment = false, $defaultName = '', $maxSize = 2_097_152)
 654  {
 655      global $conf;
 656      $http = new DokuHTTPClient();
 657      $http->max_bodysize = $maxSize;
 658      $http->timeout = 25; //max. 25 sec
 659      $http->keep_alive = false; // we do single ops here, no need for keep-alive
 660  
 661      $data = $http->get($url);
 662      if (!$data) return false;
 663  
 664      $name = '';
 665      if ($useAttachment) {
 666          if (isset($http->resp_headers['content-disposition'])) {
 667              $content_disposition = $http->resp_headers['content-disposition'];
 668              $match = [];
 669              if (
 670                  is_string($content_disposition) &&
 671                  preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)
 672              ) {
 673                  $name = PhpString::basename($match[1]);
 674              }
 675          }
 676  
 677          if (!$name) {
 678              if (!$defaultName) return false;
 679              $name = $defaultName;
 680          }
 681  
 682          $file .= $name;
 683      }
 684  
 685      $fileexists = file_exists($file);
 686      $fp = @fopen($file, "w");
 687      if (!$fp) return false;
 688      fwrite($fp, $data);
 689      fclose($fp);
 690      if (!$fileexists && $conf['fperm']) {
 691          chmod($file, $conf['fperm']);
 692      }
 693      if ($useAttachment) return $name;
 694      return true;
 695  }
 696  
 697  /**
 698   * Windows compatible rename
 699   *
 700   * rename() can not overwrite existing files on Windows
 701   * this function will use copy/unlink instead
 702   *
 703   * @param string $from
 704   * @param string $to
 705   * @return bool succes or fail
 706   */
 707  function io_rename($from, $to)
 708  {
 709      global $conf;
 710      if (!@rename($from, $to)) {
 711          if (@copy($from, $to)) {
 712              if ($conf['fperm']) {
 713                  chmod($to, $conf['fperm']);
 714              }
 715              @unlink($from);
 716              return true;
 717          }
 718          return false;
 719      }
 720      return true;
 721  }
 722  
 723  /**
 724   * Runs an external command with input and output pipes.
 725   * Returns the exit code from the process.
 726   *
 727   * @param string $cmd
 728   * @param string $input input pipe
 729   * @param string $output output pipe
 730   * @return int exit code from process
 731   *
 732   * @author Tom N Harris <tnharris@whoopdedo.org>
 733   */
 734  function io_exec($cmd, $input, &$output)
 735  {
 736      $descspec = [
 737          0 => ["pipe", "r"],
 738          1 => ["pipe", "w"],
 739          2 => ["pipe", "w"]
 740      ];
 741      $ph = proc_open($cmd, $descspec, $pipes);
 742      if (!$ph) return -1;
 743      fclose($pipes[2]); // ignore stderr
 744      fwrite($pipes[0], $input);
 745      fclose($pipes[0]);
 746      $output = stream_get_contents($pipes[1]);
 747      fclose($pipes[1]);
 748      return proc_close($ph);
 749  }
 750  
 751  /**
 752   * Search a file for matching lines
 753   *
 754   * This is probably not faster than file()+preg_grep() but less
 755   * memory intensive because not the whole file needs to be loaded
 756   * at once.
 757   *
 758   * @param string $file The file to search
 759   * @param string $pattern PCRE pattern
 760   * @param int $max How many lines to return (0 for all)
 761   * @param bool $backref When true returns array with backreferences instead of lines
 762   * @return array matching lines or backref, false on error
 763   *
 764   * @author Andreas Gohr <andi@splitbrain.org>
 765   */
 766  function io_grep($file, $pattern, $max = 0, $backref = false)
 767  {
 768      $fh = @fopen($file, 'r');
 769      if (!$fh) return false;
 770      $matches = [];
 771  
 772      $cnt = 0;
 773      $line = '';
 774      while (!feof($fh)) {
 775          $line .= fgets($fh, 4096);  // read full line
 776          if (!str_ends_with($line, "\n")) continue;
 777  
 778          // check if line matches
 779          if (preg_match($pattern, $line, $match)) {
 780              if ($backref) {
 781                  $matches[] = $match;
 782              } else {
 783                  $matches[] = $line;
 784              }
 785              $cnt++;
 786          }
 787          if ($max && $max == $cnt) break;
 788          $line = '';
 789      }
 790      fclose($fh);
 791      return $matches;
 792  }
 793  
 794  
 795  /**
 796   * Get size of contents of a file, for a compressed file the uncompressed size
 797   * Warning: reading uncompressed size of content of bz-files requires uncompressing
 798   *
 799   * @param string $file filename path to file
 800   * @return int size of file
 801   *
 802   * @author  Gerrit Uitslag <klapinklapin@gmail.com>
 803   */
 804  function io_getSizeFile($file)
 805  {
 806      if (!file_exists($file)) return 0;
 807  
 808      if (str_ends_with($file, '.gz')) {
 809          $fp = @fopen($file, "rb");
 810          if ($fp === false) return 0;
 811          fseek($fp, -4, SEEK_END);
 812          $buffer = fread($fp, 4);
 813          fclose($fp);
 814          $array = unpack("V", $buffer);
 815          $uncompressedsize = end($array);
 816      } elseif (str_ends_with($file, '.bz2')) {
 817          if (!DOKU_HAS_BZIP) return 0;
 818          $bz = bzopen($file, "r");
 819          if ($bz === false) return 0;
 820          $uncompressedsize = 0;
 821          while (!feof($bz)) {
 822              //8192 seems to be the maximum buffersize?
 823              $buffer = bzread($bz, 8192);
 824              if (($buffer === false) || (bzerrno($bz) !== 0)) {
 825                  return 0;
 826              }
 827              $uncompressedsize += strlen($buffer);
 828          }
 829      } else {
 830          $uncompressedsize = filesize($file);
 831      }
 832  
 833      return $uncompressedsize;
 834  }