[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> media.php (source)

   1  <?php
   2  
   3  /**
   4   * All output and handler function needed for the media management popup
   5   *
   6   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   7   * @author     Andreas Gohr <andi@splitbrain.org>
   8   */
   9  
  10  use dokuwiki\Ui\MediaRevisions;
  11  use dokuwiki\Cache\CacheImageMod;
  12  use splitbrain\slika\Exception;
  13  use dokuwiki\PassHash;
  14  use dokuwiki\ChangeLog\MediaChangeLog;
  15  use dokuwiki\Extension\Event;
  16  use dokuwiki\Form\Form;
  17  use dokuwiki\HTTP\DokuHTTPClient;
  18  use dokuwiki\Logger;
  19  use dokuwiki\Subscriptions\MediaSubscriptionSender;
  20  use dokuwiki\Ui\Media\DisplayRow;
  21  use dokuwiki\Ui\Media\DisplayTile;
  22  use dokuwiki\Ui\MediaDiff;
  23  use dokuwiki\Utf8\PhpString;
  24  use dokuwiki\Utf8\Sort;
  25  use splitbrain\slika\Slika;
  26  
  27  /**
  28   * Lists pages which currently use a media file selected for deletion
  29   *
  30   * References uses the same visual as search results and share
  31   * their CSS tags except pagenames won't be links.
  32   *
  33   * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
  34   *
  35   * @param array $data
  36   * @param string $id
  37   */
  38  function media_filesinuse($data, $id)
  39  {
  40      global $lang;
  41      echo '<h1>' . $lang['reference'] . ' <code>' . hsc(noNS($id)) . '</code></h1>';
  42      echo '<p>' . hsc($lang['ref_inuse']) . '</p>';
  43  
  44      $hidden = 0; //count of hits without read permission
  45      foreach ($data as $row) {
  46          if (auth_quickaclcheck($row) >= AUTH_READ && isVisiblePage($row)) {
  47              echo '<div class="search_result">';
  48              echo '<span class="mediaref_ref">' . hsc($row) . '</span>';
  49              echo '</div>';
  50          } else $hidden++;
  51      }
  52      if ($hidden) {
  53          echo '<div class="mediaref_hidden">' . $lang['ref_hidden'] . '</div>';
  54      }
  55  }
  56  
  57  /**
  58   * Handles the saving of image meta data
  59   *
  60   * @author Andreas Gohr <andi@splitbrain.org>
  61   * @author Kate Arzamastseva <pshns@ukr.net>
  62   *
  63   * @param string $id media id
  64   * @param int $auth permission level
  65   * @param array $data
  66   * @return false|string
  67   */
  68  function media_metasave($id, $auth, $data)
  69  {
  70      if ($auth < AUTH_UPLOAD) return false;
  71      if (!checkSecurityToken()) return false;
  72      global $lang;
  73      global $conf;
  74      $src = mediaFN($id);
  75  
  76      $meta = new JpegMeta($src);
  77      $meta->_parseAll();
  78  
  79      foreach ($data as $key => $val) {
  80          $val = trim($val);
  81          if (empty($val)) {
  82              $meta->deleteField($key);
  83          } else {
  84              $meta->setField($key, $val);
  85          }
  86      }
  87  
  88      $old = @filemtime($src);
  89      if (!file_exists(mediaFN($id, $old)) && file_exists($src)) {
  90          // add old revision to the attic
  91          media_saveOldRevision($id);
  92      }
  93      $filesize_old = filesize($src);
  94      if ($meta->save()) {
  95          if ($conf['fperm']) chmod($src, $conf['fperm']);
  96          @clearstatcache(true, $src);
  97          $new = @filemtime($src);
  98          $filesize_new = filesize($src);
  99          $sizechange = $filesize_new - $filesize_old;
 100  
 101          // add a log entry to the media changelog
 102          addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, $lang['media_meta_edited'], '', null, $sizechange);
 103  
 104          msg($lang['metasaveok'], 1);
 105          return $id;
 106      } else {
 107          msg($lang['metasaveerr'], -1);
 108          return false;
 109      }
 110  }
 111  
 112  /**
 113   * check if a media is external source
 114   *
 115   * @author Gerrit Uitslag <klapinklapin@gmail.com>
 116   *
 117   * @param string $id the media ID or URL
 118   * @return bool
 119   */
 120  function media_isexternal($id)
 121  {
 122      if (preg_match('#^(?:https?|ftp)://#i', $id)) return true;
 123      return false;
 124  }
 125  
 126  /**
 127   * Check if a media item is public (eg, external URL or readable by @ALL)
 128   *
 129   * @author Andreas Gohr <andi@splitbrain.org>
 130   *
 131   * @param string $id  the media ID or URL
 132   * @return bool
 133   */
 134  function media_ispublic($id)
 135  {
 136      if (media_isexternal($id)) return true;
 137      $id = cleanID($id);
 138      if (auth_aclcheck(getNS($id) . ':*', '', []) >= AUTH_READ) return true;
 139      return false;
 140  }
 141  
 142  /**
 143   * Display the form to edit image meta data
 144   *
 145   * @author Andreas Gohr <andi@splitbrain.org>
 146   * @author Kate Arzamastseva <pshns@ukr.net>
 147   *
 148   * @param string $id media id
 149   * @param int $auth permission level
 150   * @return bool
 151   */
 152  function media_metaform($id, $auth)
 153  {
 154      global $lang;
 155  
 156      if ($auth < AUTH_UPLOAD) {
 157          echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . DOKU_LF;
 158          return false;
 159      }
 160  
 161      // load the field descriptions
 162      static $fields = null;
 163      if ($fields === null) {
 164          $config_files = getConfigFiles('mediameta');
 165          foreach ($config_files as $config_file) {
 166              if (file_exists($config_file)) include($config_file);
 167          }
 168      }
 169  
 170      $src = mediaFN($id);
 171  
 172      // output
 173      $form = new Form([
 174              'action' => media_managerURL(['tab_details' => 'view'], '&'),
 175              'class' => 'meta'
 176      ]);
 177      $form->addTagOpen('div')->addClass('no');
 178      $form->setHiddenField('img', $id);
 179      $form->setHiddenField('mediado', 'save');
 180      foreach ($fields as $key => $field) {
 181          // get current value
 182          if (empty($field[0])) continue;
 183          $tags = [$field[0]];
 184          if (isset($field[3]) && is_array($field[3])) $tags = array_merge($tags, $field[3]);
 185          $value = tpl_img_getTag($tags, '', $src);
 186          $value = cleanText($value);
 187  
 188          // prepare attributes
 189          $p = [
 190              'class' => 'edit',
 191              'id'    => 'meta__' . $key,
 192              'name'  => 'meta[' . $field[0] . ']'
 193          ];
 194  
 195          $form->addTagOpen('div')->addClass('row');
 196          if ($field[2] == 'text') {
 197              $form->addTextInput(
 198                  $p['name'],
 199                  ($lang[$field[1]] ?: $field[1] . ':')
 200              )->id($p['id'])->addClass($p['class'])->val($value);
 201          } else {
 202              $form->addTextarea($p['name'], $lang[$field[1]])->id($p['id'])
 203                  ->val(formText($value))
 204                  ->addClass($p['class'])
 205                  ->attr('rows', '6')->attr('cols', '50');
 206          }
 207          $form->addTagClose('div');
 208      }
 209      $form->addTagOpen('div')->addClass('buttons');
 210      $form->addButton('mediado[save]', $lang['btn_save'])->attr('type', 'submit')
 211          ->attrs(['accesskey' => 's']);
 212      $form->addTagClose('div');
 213  
 214      $form->addTagClose('div');
 215      echo $form->toHTML();
 216      return true;
 217  }
 218  
 219  /**
 220   * Convenience function to check if a media file is still in use
 221   *
 222   * @author Michael Klier <chi@chimeric.de>
 223   *
 224   * @param string $id media id
 225   * @return array|bool
 226   */
 227  function media_inuse($id)
 228  {
 229      global $conf;
 230  
 231      if ($conf['refcheck']) {
 232          $mediareferences = ft_mediause($id, true);
 233          if ($mediareferences === []) {
 234              return false;
 235          } else {
 236              return $mediareferences;
 237          }
 238      } else {
 239          return false;
 240      }
 241  }
 242  
 243  /**
 244   * Handles media file deletions
 245   *
 246   * If configured, checks for media references before deletion
 247   *
 248   * @author             Andreas Gohr <andi@splitbrain.org>
 249   *
 250   * @param string $id media id
 251   * @param int $auth no longer used
 252   * @return int One of: 0,
 253   *                     DOKU_MEDIA_DELETED,
 254   *                     DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS,
 255   *                     DOKU_MEDIA_NOT_AUTH,
 256   *                     DOKU_MEDIA_INUSE
 257   */
 258  function media_delete($id, $auth)
 259  {
 260      global $lang;
 261      $auth = auth_quickaclcheck(ltrim(getNS($id) . ':*', ':'));
 262      if ($auth < AUTH_DELETE) return DOKU_MEDIA_NOT_AUTH;
 263      if (media_inuse($id)) return DOKU_MEDIA_INUSE;
 264  
 265      $file = mediaFN($id);
 266  
 267      // trigger an event - MEDIA_DELETE_FILE
 268      $data = [];
 269      $data['id']   = $id;
 270      $data['name'] = PhpString::basename($file);
 271      $data['path'] = $file;
 272      $data['size'] = (file_exists($file)) ? filesize($file) : 0;
 273  
 274      $data['unl'] = false;
 275      $data['del'] = false;
 276      $evt = new Event('MEDIA_DELETE_FILE', $data);
 277      if ($evt->advise_before()) {
 278          $old = @filemtime($file);
 279          if (!file_exists(mediaFN($id, $old)) && file_exists($file)) {
 280              // add old revision to the attic
 281              media_saveOldRevision($id);
 282          }
 283  
 284          $data['unl'] = @unlink($file);
 285          if ($data['unl']) {
 286              $sizechange = 0 - $data['size'];
 287              addMediaLogEntry(time(), $id, DOKU_CHANGE_TYPE_DELETE, $lang['deleted'], '', null, $sizechange);
 288  
 289              $data['del'] = io_sweepNS($id, 'mediadir');
 290          }
 291      }
 292      $evt->advise_after();
 293      unset($evt);
 294  
 295      if ($data['unl'] && $data['del']) {
 296          return DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS;
 297      }
 298  
 299      return $data['unl'] ? DOKU_MEDIA_DELETED : 0;
 300  }
 301  
 302  /**
 303   * Handle file uploads via XMLHttpRequest
 304   *
 305   * @param string $ns   target namespace
 306   * @param int    $auth current auth check result
 307   * @return false|string false on error, id of the new file on success
 308   */
 309  function media_upload_xhr($ns, $auth)
 310  {
 311      if (!checkSecurityToken()) return false;
 312      global $INPUT;
 313  
 314      $id = $INPUT->get->str('qqfile');
 315      [$ext, $mime] = mimetype($id);
 316      $input = fopen("php://input", "r");
 317      if (!($tmp = io_mktmpdir())) return false;
 318      $path = $tmp . '/' . md5($id);
 319      $target = fopen($path, "w");
 320      $realSize = stream_copy_to_stream($input, $target);
 321      fclose($target);
 322      fclose($input);
 323      if ($INPUT->server->has('CONTENT_LENGTH') && ($realSize != $INPUT->server->int('CONTENT_LENGTH'))) {
 324          unlink($path);
 325          return false;
 326      }
 327  
 328      $res = media_save(
 329          ['name' => $path, 'mime' => $mime, 'ext'  => $ext],
 330          $ns . ':' . $id,
 331          ($INPUT->get->str('ow') == 'true'),
 332          $auth,
 333          'copy'
 334      );
 335      unlink($path);
 336      if ($tmp) io_rmdir($tmp, true);
 337      if (is_array($res)) {
 338          msg($res[0], $res[1]);
 339          return false;
 340      }
 341      return $res;
 342  }
 343  
 344  /**
 345   * Handles media file uploads
 346   *
 347   * @author Andreas Gohr <andi@splitbrain.org>
 348   * @author Michael Klier <chi@chimeric.de>
 349   *
 350   * @param string     $ns    target namespace
 351   * @param int        $auth  current auth check result
 352   * @param bool|array $file  $_FILES member, $_FILES['upload'] if false
 353   * @return false|string false on error, id of the new file on success
 354   */
 355  function media_upload($ns, $auth, $file = false)
 356  {
 357      if (!checkSecurityToken()) return false;
 358      global $lang;
 359      global $INPUT;
 360  
 361      // get file and id
 362      $id   = $INPUT->post->str('mediaid');
 363      if (!$file) $file = $_FILES['upload'];
 364      if (empty($id)) $id = $file['name'];
 365  
 366      // check for errors (messages are done in lib/exe/mediamanager.php)
 367      if ($file['error']) return false;
 368  
 369      // check extensions
 370      [$fext, $fmime] = mimetype($file['name']);
 371      [$iext, $imime] = mimetype($id);
 372      if ($fext && !$iext) {
 373          // no extension specified in id - read original one
 374          $id   .= '.' . $fext;
 375          $imime = $fmime;
 376      } elseif ($fext && $fext != $iext) {
 377          // extension was changed, print warning
 378          msg(sprintf($lang['mediaextchange'], $fext, $iext));
 379      }
 380  
 381      $res = media_save(
 382          [
 383              'name' => $file['tmp_name'],
 384              'mime' => $imime,
 385              'ext' => $iext
 386          ],
 387          $ns . ':' . $id,
 388          $INPUT->post->bool('ow'),
 389          $auth,
 390          'copy_uploaded_file'
 391      );
 392      if (is_array($res)) {
 393          msg($res[0], $res[1]);
 394          return false;
 395      }
 396      return $res;
 397  }
 398  
 399  /**
 400   * An alternative to move_uploaded_file that copies
 401   *
 402   * Using copy, makes sure any setgid bits on the media directory are honored
 403   *
 404   * @see   move_uploaded_file()
 405   *
 406   * @param string $from
 407   * @param string $to
 408   * @return bool
 409   */
 410  function copy_uploaded_file($from, $to)
 411  {
 412      if (!is_uploaded_file($from)) return false;
 413      $ok = copy($from, $to);
 414      @unlink($from);
 415      return $ok;
 416  }
 417  
 418  /**
 419   * This generates an action event and delegates to _media_upload_action().
 420   * Action plugins are allowed to pre/postprocess the uploaded file.
 421   * (The triggered event is preventable.)
 422   *
 423   * Event data:
 424   * $data[0]     fn_tmp:    the temporary file name (read from $_FILES)
 425   * $data[1]     fn:        the file name of the uploaded file
 426   * $data[2]     id:        the future directory id of the uploaded file
 427   * $data[3]     imime:     the mimetype of the uploaded file
 428   * $data[4]     overwrite: if an existing file is going to be overwritten
 429   * $data[5]     move:      name of function that performs move/copy/..
 430   *
 431   * @triggers MEDIA_UPLOAD_FINISH
 432   *
 433   * @param array  $file
 434   * @param string $id   media id
 435   * @param bool   $ow   overwrite?
 436   * @param int    $auth permission level
 437   * @param string $move name of functions that performs move/copy/..
 438   * @return false|array|string
 439   */
 440  function media_save($file, $id, $ow, $auth, $move)
 441  {
 442      if ($auth < AUTH_UPLOAD) {
 443          return ["You don't have permissions to upload files.", -1];
 444      }
 445  
 446      if (!isset($file['mime']) || !isset($file['ext'])) {
 447          [$ext, $mime] = mimetype($id);
 448          if (!isset($file['mime'])) {
 449              $file['mime'] = $mime;
 450          }
 451          if (!isset($file['ext'])) {
 452              $file['ext'] = $ext;
 453          }
 454      }
 455  
 456      global $lang, $conf;
 457  
 458      // get filename
 459      $id   = cleanID($id);
 460      $fn   = mediaFN($id);
 461  
 462      // get filetype regexp
 463      $types = array_keys(getMimeTypes());
 464      $types = array_map(
 465          static fn($q) => preg_quote($q, "/"),
 466          $types
 467      );
 468      $regex = implode('|', $types);
 469  
 470      // because a temp file was created already
 471      if (!preg_match('/\.(' . $regex . ')$/i', $fn)) {
 472          return [$lang['uploadwrong'], -1];
 473      }
 474  
 475      //check for overwrite
 476      $overwrite = file_exists($fn);
 477      $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
 478      if ($overwrite && (!$ow || $auth < $auth_ow)) {
 479          return [$lang['uploadexist'], 0];
 480      }
 481      // check for valid content
 482      $ok = media_contentcheck($file['name'], $file['mime']);
 483      if ($ok == -1) {
 484          return [sprintf($lang['uploadbadcontent'], '.' . $file['ext']), -1];
 485      } elseif ($ok == -2) {
 486          return [$lang['uploadspam'], -1];
 487      } elseif ($ok == -3) {
 488          return [$lang['uploadxss'], -1];
 489      }
 490  
 491      // prepare event data
 492      $data = [];
 493      $data[0] = $file['name'];
 494      $data[1] = $fn;
 495      $data[2] = $id;
 496      $data[3] = $file['mime'];
 497      $data[4] = $overwrite;
 498      $data[5] = $move;
 499  
 500      // trigger event
 501      return Event::createAndTrigger('MEDIA_UPLOAD_FINISH', $data, '_media_upload_action', true);
 502  }
 503  
 504  /**
 505   * Callback adapter for media_upload_finish() triggered by MEDIA_UPLOAD_FINISH
 506   *
 507   * @author Michael Klier <chi@chimeric.de>
 508   *
 509   * @param array $data event data
 510   * @return false|array|string
 511   */
 512  function _media_upload_action($data)
 513  {
 514      // fixme do further sanity tests of given data?
 515      if (is_array($data) && count($data) === 6) {
 516          return media_upload_finish($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]);
 517      } else {
 518          return false; //callback error
 519      }
 520  }
 521  
 522  /**
 523   * Saves an uploaded media file
 524   *
 525   * @author Andreas Gohr <andi@splitbrain.org>
 526   * @author Michael Klier <chi@chimeric.de>
 527   * @author Kate Arzamastseva <pshns@ukr.net>
 528   *
 529   * @param string $fn_tmp
 530   * @param string $fn
 531   * @param string $id        media id
 532   * @param string $imime     mime type
 533   * @param bool   $overwrite overwrite existing?
 534   * @param string $move      function name
 535   * @return array|string
 536   */
 537  function media_upload_finish($fn_tmp, $fn, $id, $imime, $overwrite, $move = 'move_uploaded_file')
 538  {
 539      global $conf;
 540      global $lang;
 541      global $REV;
 542  
 543      $old = @filemtime($fn);
 544      if (!file_exists(mediaFN($id, $old)) && file_exists($fn)) {
 545          // add old revision to the attic if missing
 546          media_saveOldRevision($id);
 547      }
 548  
 549      // prepare directory
 550      io_createNamespace($id, 'media');
 551  
 552      $filesize_old = file_exists($fn) ? filesize($fn) : 0;
 553  
 554      if ($move($fn_tmp, $fn)) {
 555          @clearstatcache(true, $fn);
 556          $new = @filemtime($fn);
 557          // Set the correct permission here.
 558          // Always chmod media because they may be saved with different permissions than expected from the php umask.
 559          // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
 560          chmod($fn, $conf['fmode']);
 561          msg($lang['uploadsucc'], 1);
 562          media_notify($id, $fn, $imime, $old, $new);
 563          // add a log entry to the media changelog
 564          $filesize_new = filesize($fn);
 565          $sizechange = $filesize_new - $filesize_old;
 566          if ($REV) {
 567              addMediaLogEntry(
 568                  $new,
 569                  $id,
 570                  DOKU_CHANGE_TYPE_REVERT,
 571                  sprintf($lang['restored'], dformat($REV)),
 572                  $REV,
 573                  null,
 574                  $sizechange
 575              );
 576          } elseif ($overwrite) {
 577              addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
 578          } else {
 579              addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
 580          }
 581          return $id;
 582      } else {
 583          return [$lang['uploadfail'], -1];
 584      }
 585  }
 586  
 587  /**
 588   * Moves the current version of media file to the media_attic
 589   * directory
 590   *
 591   * @author Kate Arzamastseva <pshns@ukr.net>
 592   *
 593   * @param string $id
 594   * @return int - revision date
 595   */
 596  function media_saveOldRevision($id)
 597  {
 598      global $conf, $lang;
 599  
 600      $oldf = mediaFN($id);
 601      if (!file_exists($oldf)) return '';
 602      $date = filemtime($oldf);
 603      if (!$conf['mediarevisions']) return $date;
 604  
 605      $medialog = new MediaChangeLog($id);
 606      if (!$medialog->getRevisionInfo($date)) {
 607          // there was an external edit,
 608          // there is no log entry for current version of file
 609          $sizechange = filesize($oldf);
 610          if (!file_exists(mediaMetaFN($id, '.changes'))) {
 611              addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
 612          } else {
 613              $oldRev = $medialog->getRevisions(-1, 1); // from changelog
 614              $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]);
 615              $filesize_old = filesize(mediaFN($id, $oldRev));
 616              $sizechange -= $filesize_old;
 617  
 618              addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
 619          }
 620      }
 621  
 622      $newf = mediaFN($id, $date);
 623      io_makeFileDir($newf);
 624      if (copy($oldf, $newf)) {
 625          // Set the correct permission here.
 626          // Always chmod media because they may be saved with different permissions than expected from the php umask.
 627          // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
 628          chmod($newf, $conf['fmode']);
 629      }
 630      return $date;
 631  }
 632  
 633  /**
 634   * This function checks if the uploaded content is really what the
 635   * mimetype says it is. We also do spam checking for text types here.
 636   *
 637   * We need to do this stuff because we can not rely on the browser
 638   * to do this check correctly. Yes, IE is broken as usual.
 639   *
 640   * @author Andreas Gohr <andi@splitbrain.org>
 641   * @link   http://www.splitbrain.org/blog/2007-02/12-internet_explorer_facilitates_cross_site_scripting
 642   * @fixme  check all 26 magic IE filetypes here?
 643   *
 644   * @param string $file path to file
 645   * @param string $mime mimetype
 646   * @return int
 647   */
 648  function media_contentcheck($file, $mime)
 649  {
 650      global $conf;
 651      if ($conf['iexssprotect']) {
 652          $fh = @fopen($file, 'rb');
 653          if ($fh) {
 654              $bytes = fread($fh, 256);
 655              fclose($fh);
 656              if (preg_match('/<(script|a|img|html|body|iframe)[\s>]/i', $bytes)) {
 657                  return -3; //XSS: possibly malicious content
 658              }
 659          }
 660      }
 661      if (str_starts_with($mime, 'image/')) {
 662          $info = @getimagesize($file);
 663          if ($mime == 'image/gif' && $info[2] != 1) {
 664              return -1; // uploaded content did not match the file extension
 665          } elseif ($mime == 'image/jpeg' && $info[2] != 2) {
 666              return -1;
 667          } elseif ($mime == 'image/png' && $info[2] != 3) {
 668              return -1;
 669          }
 670          # fixme maybe check other images types as well
 671      } elseif (str_starts_with($mime, 'text/')) {
 672          global $TEXT;
 673          $TEXT = io_readFile($file);
 674          if (checkwordblock()) {
 675              return -2; //blocked by the spam blacklist
 676          }
 677      }
 678      return 0;
 679  }
 680  
 681  /**
 682   * Send a notify mail on uploads
 683   *
 684   * @author Andreas Gohr <andi@splitbrain.org>
 685   *
 686   * @param string   $id      media id
 687   * @param string   $file    path to file
 688   * @param string   $mime    mime type
 689   * @param bool|int $old_rev revision timestamp or false
 690   */
 691  function media_notify($id, $file, $mime, $old_rev = false, $current_rev = false)
 692  {
 693      global $conf;
 694      if (empty($conf['notify'])) return; //notify enabled?
 695  
 696      $subscription = new MediaSubscriptionSender();
 697      $subscription->sendMediaDiff($conf['notify'], 'uploadmail', $id, $old_rev, $current_rev);
 698  }
 699  
 700  /**
 701   * List all files in a given Media namespace
 702   *
 703   * @param string      $ns             namespace
 704   * @param null|int    $auth           permission level
 705   * @param string      $jump           id
 706   * @param bool        $fullscreenview
 707   * @param bool|string $sort           sorting order, false skips sorting
 708   */
 709  function media_filelist($ns, $auth = null, $jump = '', $fullscreenview = false, $sort = false)
 710  {
 711      global $conf;
 712      global $lang;
 713      $ns = cleanID($ns);
 714  
 715      // check auth our self if not given (needed for ajax calls)
 716      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
 717  
 718      if (!$fullscreenview) echo '<h1 id="media__ns">:' . hsc($ns) . '</h1>' . NL;
 719  
 720      if ($auth < AUTH_READ) {
 721          // FIXME: print permission warning here instead?
 722          echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
 723      } else {
 724          if (!$fullscreenview) {
 725              media_uploadform($ns, $auth);
 726              media_searchform($ns);
 727          }
 728  
 729          $dir = utf8_encodeFN(str_replace(':', '/', $ns));
 730          $data = [];
 731          search(
 732              $data,
 733              $conf['mediadir'],
 734              'search_mediafiles',
 735              ['showmsg' => true, 'depth' => 1],
 736              $dir,
 737              1,
 738              $sort
 739          );
 740  
 741          if (!count($data)) {
 742              echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
 743          } else {
 744              if ($fullscreenview) {
 745                  echo '<ul class="' . _media_get_list_type() . '">';
 746              }
 747              foreach ($data as $item) {
 748                  if (!$fullscreenview) {
 749                      //FIXME old call: media_printfile($item,$auth,$jump);
 750                      $display = new DisplayRow($item);
 751                      $display->scrollIntoView($jump == $item->getID());
 752                      $display->show();
 753                  } else {
 754                      //FIXME old call: media_printfile_thumbs($item,$auth,$jump);
 755                      echo '<li>';
 756                      $display = new DisplayTile($item);
 757                      $display->scrollIntoView($jump == $item->getID());
 758                      $display->show();
 759                      echo '</li>';
 760                  }
 761              }
 762              if ($fullscreenview) echo '</ul>' . NL;
 763          }
 764      }
 765  }
 766  
 767  /**
 768   * Prints tabs for files list actions
 769   *
 770   * @author Kate Arzamastseva <pshns@ukr.net>
 771   * @author Adrian Lang <mail@adrianlang.de>
 772   *
 773   * @param string $selected_tab - opened tab
 774   */
 775  
 776  function media_tabs_files($selected_tab = '')
 777  {
 778      global $lang;
 779      $tabs = [];
 780      foreach (
 781          [
 782              'files' => 'mediaselect',
 783              'upload' => 'media_uploadtab',
 784              'search' => 'media_searchtab'
 785          ] as $tab => $caption
 786      ) {
 787          $tabs[$tab] = [
 788              'href'    => media_managerURL(['tab_files' => $tab], '&'),
 789              'caption' => $lang[$caption]
 790          ];
 791      }
 792  
 793      html_tabs($tabs, $selected_tab);
 794  }
 795  
 796  /**
 797   * Prints tabs for files details actions
 798   *
 799   * @author Kate Arzamastseva <pshns@ukr.net>
 800   * @param string $image filename of the current image
 801   * @param string $selected_tab opened tab
 802   */
 803  function media_tabs_details($image, $selected_tab = '')
 804  {
 805      global $lang, $conf;
 806  
 807      $tabs = [];
 808      $tabs['view'] = [
 809          'href'    => media_managerURL(['tab_details' => 'view'], '&'),
 810          'caption' => $lang['media_viewtab']
 811      ];
 812  
 813      [, $mime] = mimetype($image);
 814      if ($mime == 'image/jpeg' && file_exists(mediaFN($image))) {
 815          $tabs['edit'] = [
 816              'href'    => media_managerURL(['tab_details' => 'edit'], '&'),
 817              'caption' => $lang['media_edittab']
 818          ];
 819      }
 820      if ($conf['mediarevisions']) {
 821          $tabs['history'] = [
 822              'href'    => media_managerURL(['tab_details' => 'history'], '&'),
 823              'caption' => $lang['media_historytab']
 824          ];
 825      }
 826  
 827      html_tabs($tabs, $selected_tab);
 828  }
 829  
 830  /**
 831   * Prints options for the tab that displays a list of all files
 832   *
 833   * @author Kate Arzamastseva <pshns@ukr.net>
 834   */
 835  function media_tab_files_options()
 836  {
 837      global $lang;
 838      global $INPUT;
 839      global $ID;
 840  
 841      $form = new Form([
 842              'method' => 'get',
 843              'action' => wl($ID),
 844              'class' => 'options'
 845      ]);
 846      $form->addTagOpen('div')->addClass('no');
 847      $form->setHiddenField('sectok', null);
 848      $media_manager_params = media_managerURL([], '', false, true);
 849      foreach ($media_manager_params as $pKey => $pVal) {
 850          $form->setHiddenField($pKey, $pVal);
 851      }
 852      if ($INPUT->has('q')) {
 853          $form->setHiddenField('q', $INPUT->str('q'));
 854      }
 855      $form->addHTML('<ul>' . NL);
 856      foreach (
 857          [
 858              'list' => ['listType', ['thumbs', 'rows']],
 859              'sort' => ['sortBy', ['name', 'date']]
 860          ] as $group => $content
 861      ) {
 862          $checked = "_media_get_{$group}_type";
 863          $checked = $checked();
 864  
 865          $form->addHTML('<li class="' . $content[0] . '">');
 866          foreach ($content[1] as $option) {
 867              $attrs = [];
 868              if ($checked == $option) {
 869                  $attrs['checked'] = 'checked';
 870              }
 871              $radio = $form->addRadioButton(
 872                  $group . '_dwmedia',
 873                  $lang['media_' . $group . '_' . $option]
 874              )->val($option)->id($content[0] . '__' . $option)->addClass($option);
 875              $radio->attrs($attrs);
 876          }
 877          $form->addHTML('</li>' . NL);
 878      }
 879      $form->addHTML('<li>');
 880      $form->addButton('', $lang['btn_apply'])->attr('type', 'submit');
 881      $form->addHTML('</li>' . NL);
 882      $form->addHTML('</ul>' . NL);
 883      $form->addTagClose('div');
 884      echo $form->toHTML();
 885  }
 886  
 887  /**
 888   * Returns type of sorting for the list of files in media manager
 889   *
 890   * @author Kate Arzamastseva <pshns@ukr.net>
 891   *
 892   * @return string - sort type
 893   */
 894  function _media_get_sort_type()
 895  {
 896      return _media_get_display_param('sort', ['default' => 'name', 'date']);
 897  }
 898  
 899  /**
 900   * Returns type of listing for the list of files in media manager
 901   *
 902   * @author Kate Arzamastseva <pshns@ukr.net>
 903   *
 904   * @return string - list type
 905   */
 906  function _media_get_list_type()
 907  {
 908      return _media_get_display_param('list', ['default' => 'thumbs', 'rows']);
 909  }
 910  
 911  /**
 912   * Get display parameters
 913   *
 914   * @param string $param   name of parameter
 915   * @param array  $values  allowed values, where default value has index key 'default'
 916   * @return string the parameter value
 917   */
 918  function _media_get_display_param($param, $values)
 919  {
 920      global $INPUT;
 921      if (in_array($INPUT->str($param), $values)) {
 922          // FIXME: Set cookie
 923          return $INPUT->str($param);
 924      } else {
 925          $val = get_doku_pref($param, $values['default']);
 926          if (!in_array($val, $values)) {
 927              $val = $values['default'];
 928          }
 929          return $val;
 930      }
 931  }
 932  
 933  /**
 934   * Prints tab that displays a list of all files
 935   *
 936   * @author Kate Arzamastseva <pshns@ukr.net>
 937   *
 938   * @param string    $ns
 939   * @param null|int  $auth permission level
 940   * @param string    $jump item id
 941   */
 942  function media_tab_files($ns, $auth = null, $jump = '')
 943  {
 944      global $lang;
 945      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
 946  
 947      if ($auth < AUTH_READ) {
 948          echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
 949      } else {
 950          media_filelist($ns, $auth, $jump, true, _media_get_sort_type());
 951      }
 952  }
 953  
 954  /**
 955   * Prints tab that displays uploading form
 956   *
 957   * @author Kate Arzamastseva <pshns@ukr.net>
 958   *
 959   * @param string   $ns
 960   * @param null|int $auth permission level
 961   * @param string   $jump item id
 962   */
 963  function media_tab_upload($ns, $auth = null, $jump = '')
 964  {
 965      global $lang;
 966      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
 967  
 968      echo '<div class="upload">' . NL;
 969      if ($auth >= AUTH_UPLOAD) {
 970          echo '<p>' . $lang['mediaupload'] . '</p>';
 971      }
 972      media_uploadform($ns, $auth, true);
 973      echo '</div>' . NL;
 974  }
 975  
 976  /**
 977   * Prints tab that displays search form
 978   *
 979   * @author Kate Arzamastseva <pshns@ukr.net>
 980   *
 981   * @param string $ns
 982   * @param null|int $auth permission level
 983   */
 984  function media_tab_search($ns, $auth = null)
 985  {
 986      global $INPUT;
 987  
 988      $do = $INPUT->str('mediado');
 989      $query = $INPUT->str('q');
 990      echo '<div class="search">' . NL;
 991  
 992      media_searchform($ns, $query, true);
 993      if ($do == 'searchlist' || $query) {
 994          media_searchlist($query, $ns, $auth, true, _media_get_sort_type());
 995      }
 996      echo '</div>' . NL;
 997  }
 998  
 999  /**
1000   * Prints tab that displays mediafile details
1001   *
1002   * @author Kate Arzamastseva <pshns@ukr.net>
1003   *
1004   * @param string     $image media id
1005   * @param string     $ns
1006   * @param null|int   $auth  permission level
1007   * @param string|int $rev   revision timestamp or empty string
1008   */
1009  function media_tab_view($image, $ns, $auth = null, $rev = '')
1010  {
1011      global $lang;
1012      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1013  
1014      if ($image && $auth >= AUTH_READ) {
1015          $meta = new JpegMeta(mediaFN($image, $rev));
1016          media_preview($image, $auth, $rev, $meta);
1017          media_preview_buttons($image, $auth, $rev);
1018          media_details($image, $auth, $rev, $meta);
1019      } else {
1020          echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
1021      }
1022  }
1023  
1024  /**
1025   * Prints tab that displays form for editing mediafile metadata
1026   *
1027   * @author Kate Arzamastseva <pshns@ukr.net>
1028   *
1029   * @param string     $image media id
1030   * @param string     $ns
1031   * @param null|int   $auth permission level
1032   */
1033  function media_tab_edit($image, $ns, $auth = null)
1034  {
1035      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1036  
1037      if ($image) {
1038          [, $mime] = mimetype($image);
1039          if ($mime == 'image/jpeg') media_metaform($image, $auth);
1040      }
1041  }
1042  
1043  /**
1044   * Prints tab that displays mediafile revisions
1045   *
1046   * @author Kate Arzamastseva <pshns@ukr.net>
1047   *
1048   * @param string     $image media id
1049   * @param string     $ns
1050   * @param null|int   $auth permission level
1051   */
1052  function media_tab_history($image, $ns, $auth = null)
1053  {
1054      global $lang;
1055      global $INPUT;
1056  
1057      if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1058      $do = $INPUT->str('mediado');
1059  
1060      if ($auth >= AUTH_READ && $image) {
1061          if ($do == 'diff') {
1062              (new MediaDiff($image))->show(); //media_diff($image, $ns, $auth);
1063          } else {
1064              $first = $INPUT->int('first', -1);
1065              (new MediaRevisions($image))->show($first);
1066          }
1067      } else {
1068          echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
1069      }
1070  }
1071  
1072  /**
1073   * Prints mediafile details
1074   *
1075   * @param string         $image media id
1076   * @param int            $auth permission level
1077   * @param int|string     $rev revision timestamp or empty string
1078   * @param JpegMeta|bool  $meta
1079   *
1080   * @author Kate Arzamastseva <pshns@ukr.net>
1081   */
1082  function media_preview($image, $auth, $rev = '', $meta = false)
1083  {
1084  
1085      $size = media_image_preview_size($image, $rev, $meta);
1086  
1087      if ($size) {
1088          global $lang;
1089          echo '<div class="image">';
1090  
1091          $more = [];
1092          if ($rev) {
1093              $more['rev'] = $rev;
1094          } else {
1095              $t = @filemtime(mediaFN($image));
1096              $more['t'] = $t;
1097          }
1098  
1099          $more['w'] = $size[0];
1100          $more['h'] = $size[1];
1101          $src = ml($image, $more);
1102  
1103          echo '<a href="' . $src . '" target="_blank" title="' . $lang['mediaview'] . '">';
1104          echo '<img src="' . $src . '" alt="" style="max-width: ' . $size[0] . 'px;" />';
1105          echo '</a>';
1106  
1107          echo '</div>';
1108      }
1109  }
1110  
1111  /**
1112   * Prints mediafile action buttons
1113   *
1114   * @author Kate Arzamastseva <pshns@ukr.net>
1115   *
1116   * @param string     $image media id
1117   * @param int        $auth  permission level
1118   * @param int|string $rev   revision timestamp, or empty string
1119   */
1120  function media_preview_buttons($image, $auth, $rev = '')
1121  {
1122      global $lang, $conf;
1123  
1124      echo '<ul class="actions">';
1125  
1126      if ($auth >= AUTH_DELETE && !$rev && file_exists(mediaFN($image))) {
1127          // delete button
1128          $form = new Form([
1129              'id' => 'mediamanager__btn_delete',
1130              'action' => media_managerURL(['delete' => $image], '&'),
1131          ]);
1132          $form->addTagOpen('div')->addClass('no');
1133          $form->addButton('', $lang['btn_delete'])->attr('type', 'submit');
1134          $form->addTagClose('div');
1135          echo '<li>';
1136          echo $form->toHTML();
1137          echo '</li>';
1138      }
1139  
1140      $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
1141      if ($auth >= $auth_ow && !$rev) {
1142          // upload new version button
1143          $form = new Form([
1144              'id' => 'mediamanager__btn_update',
1145              'action' => media_managerURL(['image' => $image, 'mediado' => 'update'], '&'),
1146          ]);
1147          $form->addTagOpen('div')->addClass('no');
1148          $form->addButton('', $lang['media_update'])->attr('type', 'submit');
1149          $form->addTagClose('div');
1150          echo '<li>';
1151          echo $form->toHTML();
1152          echo '</li>';
1153      }
1154  
1155      if ($auth >= AUTH_UPLOAD && $rev && $conf['mediarevisions'] && file_exists(mediaFN($image, $rev))) {
1156          // restore button
1157          $form = new Form([
1158              'id' => 'mediamanager__btn_restore',
1159              'action' => media_managerURL(['image' => $image], '&'),
1160          ]);
1161          $form->addTagOpen('div')->addClass('no');
1162          $form->setHiddenField('mediado', 'restore');
1163          $form->setHiddenField('rev', $rev);
1164          $form->addButton('', $lang['media_restore'])->attr('type', 'submit');
1165          $form->addTagClose('div');
1166          echo '<li>';
1167          echo $form->toHTML();
1168          echo '</li>';
1169      }
1170  
1171      echo '</ul>';
1172  }
1173  
1174  /**
1175   * Returns image width and height for mediamanager preview panel
1176   *
1177   * @author Kate Arzamastseva <pshns@ukr.net>
1178   * @param string         $image
1179   * @param int|string     $rev
1180   * @param JpegMeta|bool  $meta
1181   * @param int            $size
1182   * @return array
1183   */
1184  function media_image_preview_size($image, $rev, $meta = false, $size = 500)
1185  {
1186      if (
1187          !preg_match("/\.(jpe?g|gif|png)$/", $image)
1188          || !file_exists($filename = mediaFN($image, $rev))
1189      ) return [];
1190  
1191      $info = getimagesize($filename);
1192      $w = $info[0];
1193      $h = $info[1];
1194  
1195      if ($meta && ($w > $size || $h > $size)) {
1196          $ratio = $meta->getResizeRatio($size, $size);
1197          $w = floor($w * $ratio);
1198          $h = floor($h * $ratio);
1199      }
1200      return [$w, $h];
1201  }
1202  
1203  /**
1204   * Returns the requested EXIF/IPTC tag from the image meta
1205   *
1206   * @author Kate Arzamastseva <pshns@ukr.net>
1207   *
1208   * @param array    $tags array with tags, first existing is returned
1209   * @param JpegMeta $meta
1210   * @param string   $alt  alternative value
1211   * @return string
1212   */
1213  function media_getTag($tags, $meta = false, $alt = '')
1214  {
1215      if (!$meta) return $alt;
1216      $info = $meta->getField($tags);
1217      if (!$info) return $alt;
1218      return $info;
1219  }
1220  
1221  /**
1222   * Returns mediafile tags
1223   *
1224   * @author Kate Arzamastseva <pshns@ukr.net>
1225   *
1226   * @param JpegMeta $meta
1227   * @return array list of tags of the mediafile
1228   */
1229  function media_file_tags($meta)
1230  {
1231      // load the field descriptions
1232      static $fields = null;
1233      if (is_null($fields)) {
1234          $config_files = getConfigFiles('mediameta');
1235          foreach ($config_files as $config_file) {
1236              if (file_exists($config_file)) include($config_file);
1237          }
1238      }
1239  
1240      $tags = [];
1241  
1242      foreach ($fields as $tag) {
1243          $t = [];
1244          if (!empty($tag[0])) $t = [$tag[0]];
1245          if (isset($tag[3]) && is_array($tag[3])) $t = array_merge($t, $tag[3]);
1246          $value = media_getTag($t, $meta);
1247          $tags[] = ['tag' => $tag, 'value' => $value];
1248      }
1249  
1250      return $tags;
1251  }
1252  
1253  /**
1254   * Prints mediafile tags
1255   *
1256   * @author Kate Arzamastseva <pshns@ukr.net>
1257   *
1258   * @param string        $image image id
1259   * @param int           $auth  permission level
1260   * @param string|int    $rev   revision timestamp, or empty string
1261   * @param bool|JpegMeta $meta  image object, or create one if false
1262   */
1263  function media_details($image, $auth, $rev = '', $meta = false)
1264  {
1265      global $lang;
1266  
1267      if (!$meta) $meta = new JpegMeta(mediaFN($image, $rev));
1268      $tags = media_file_tags($meta);
1269  
1270      echo '<dl>' . NL;
1271      foreach ($tags as $tag) {
1272          if ($tag['value']) {
1273              $value = cleanText($tag['value']);
1274              echo '<dt>' . $lang[$tag['tag'][1]] . '</dt><dd>';
1275              if ($tag['tag'][2] == 'date') echo dformat($value);
1276              else echo hsc($value);
1277              echo '</dd>' . NL;
1278          }
1279      }
1280      echo '</dl>' . NL;
1281      echo '<dl>' . NL;
1282      echo '<dt>' . $lang['reference'] . ':</dt>';
1283      $media_usage = ft_mediause($image, true);
1284      if ($media_usage !== []) {
1285          foreach ($media_usage as $path) {
1286              echo '<dd>' . html_wikilink($path) . '</dd>';
1287          }
1288      } else {
1289          echo '<dd>' . $lang['nothingfound'] . '</dd>';
1290      }
1291      echo '</dl>' . NL;
1292  }
1293  
1294  /**
1295   * Shows difference between two revisions of file
1296   *
1297   * @author Kate Arzamastseva <pshns@ukr.net>
1298   *
1299   * @param string $image  image id
1300   * @param string $ns
1301   * @param int $auth permission level
1302   * @param bool $fromajax
1303   *
1304   * @deprecated 2020-12-31
1305   */
1306  function media_diff($image, $ns, $auth, $fromajax = false)
1307  {
1308      dbg_deprecated('see ' . MediaDiff::class . '::show()');
1309  }
1310  
1311  /**
1312   * Callback for media file diff
1313   *
1314   * @param array $data event data
1315   *
1316   * @deprecated 2020-12-31
1317   */
1318  function _media_file_diff($data)
1319  {
1320      dbg_deprecated('see ' . MediaDiff::class . '::show()');
1321  }
1322  
1323  /**
1324   * Shows difference between two revisions of image
1325   *
1326   * @author Kate Arzamastseva <pshns@ukr.net>
1327   *
1328   * @param string $image
1329   * @param string|int $l_rev revision timestamp, or empty string
1330   * @param string|int $r_rev revision timestamp, or empty string
1331   * @param string $ns
1332   * @param int $auth permission level
1333   * @param bool $fromajax
1334   * @deprecated 2020-12-31
1335   */
1336  function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax)
1337  {
1338      dbg_deprecated('see ' . MediaDiff::class . '::showFileDiff()');
1339  }
1340  
1341  /**
1342   * Prints two images side by side
1343   * and slider
1344   *
1345   * @author Kate Arzamastseva <pshns@ukr.net>
1346   *
1347   * @param string $image   image id
1348   * @param int    $l_rev   revision timestamp, or empty string
1349   * @param int    $r_rev   revision timestamp, or empty string
1350   * @param array  $l_size  array with width and height
1351   * @param array  $r_size  array with width and height
1352   * @param string $type
1353   * @deprecated 2020-12-31
1354   */
1355  function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type)
1356  {
1357      dbg_deprecated('see ' . MediaDiff::class . '::showImageDiff()');
1358  }
1359  
1360  /**
1361   * Restores an old revision of a media file
1362   *
1363   * @param string $image media id
1364   * @param int    $rev   revision timestamp or empty string
1365   * @param int    $auth
1366   * @return string - file's id
1367   *
1368   * @author Kate Arzamastseva <pshns@ukr.net>
1369   */
1370  function media_restore($image, $rev, $auth)
1371  {
1372      global $conf;
1373      if ($auth < AUTH_UPLOAD || !$conf['mediarevisions']) return false;
1374      $removed = (!file_exists(mediaFN($image)) && file_exists(mediaMetaFN($image, '.changes')));
1375      if (!$image || (!file_exists(mediaFN($image)) && !$removed)) return false;
1376      if (!$rev || !file_exists(mediaFN($image, $rev))) return false;
1377      [, $imime, ] = mimetype($image);
1378      $res = media_upload_finish(
1379          mediaFN($image, $rev),
1380          mediaFN($image),
1381          $image,
1382          $imime,
1383          true,
1384          'copy'
1385      );
1386      if (is_array($res)) {
1387          msg($res[0], $res[1]);
1388          return false;
1389      }
1390      return $res;
1391  }
1392  
1393  /**
1394   * List all files found by the search request
1395   *
1396   * @author Tobias Sarnowski <sarnowski@cosmocode.de>
1397   * @author Andreas Gohr <gohr@cosmocode.de>
1398   * @author Kate Arzamastseva <pshns@ukr.net>
1399   * @triggers MEDIA_SEARCH
1400   *
1401   * @param string $query
1402   * @param string $ns
1403   * @param null|int $auth
1404   * @param bool $fullscreen
1405   * @param string $sort
1406   */
1407  function media_searchlist($query, $ns, $auth = null, $fullscreen = false, $sort = 'natural')
1408  {
1409      global $conf;
1410      global $lang;
1411  
1412      $ns = cleanID($ns);
1413      $evdata = [
1414          'ns'    => $ns,
1415          'data'  => [],
1416          'query' => $query
1417      ];
1418      if (!blank($query)) {
1419          $evt = new Event('MEDIA_SEARCH', $evdata);
1420          if ($evt->advise_before()) {
1421              $dir = utf8_encodeFN(str_replace(':', '/', $evdata['ns']));
1422              $quoted = preg_quote($evdata['query'], '/');
1423              //apply globbing
1424              $quoted = str_replace(['\*', '\?'], ['.*', '.'], $quoted, $count);
1425  
1426              //if we use globbing file name must match entirely but may be preceded by arbitrary namespace
1427              if ($count > 0) $quoted = '^([^:]*:)*' . $quoted . '$';
1428  
1429              $pattern = '/' . $quoted . '/i';
1430              search(
1431                  $evdata['data'],
1432                  $conf['mediadir'],
1433                  'search_mediafiles',
1434                  ['showmsg' => false, 'pattern' => $pattern],
1435                  $dir,
1436                  1,
1437                  $sort
1438              );
1439          }
1440          $evt->advise_after();
1441          unset($evt);
1442      }
1443  
1444      if (!$fullscreen) {
1445          echo '<h1 id="media__ns">' . sprintf($lang['searchmedia_in'], hsc($ns) . ':*') . '</h1>' . NL;
1446          media_searchform($ns, $query);
1447      }
1448  
1449      if (!count($evdata['data'])) {
1450          echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
1451      } else {
1452          if ($fullscreen) {
1453              echo '<ul class="' . _media_get_list_type() . '">';
1454          }
1455          foreach ($evdata['data'] as $item) {
1456              if (!$fullscreen) {
1457                  // FIXME old call: media_printfile($item,$item['perm'],'',true);
1458                  $display = new DisplayRow($item);
1459                  $display->relativeDisplay($ns);
1460                  $display->show();
1461              } else {
1462                  // FIXME old call: media_printfile_thumbs($item,$item['perm'],false,true);
1463                  $display = new DisplayTile($item);
1464                  $display->relativeDisplay($ns);
1465                  echo '<li>';
1466                  $display->show();
1467                  echo '</li>';
1468              }
1469          }
1470          if ($fullscreen) echo '</ul>' . NL;
1471      }
1472  }
1473  
1474  /**
1475   * Display a media icon
1476   *
1477   * @param string $filename media id
1478   * @param string $size     the size subfolder, if not specified 16x16 is used
1479   * @return string html
1480   */
1481  function media_printicon($filename, $size = '')
1482  {
1483      [$ext] = mimetype(mediaFN($filename), false);
1484  
1485      if (file_exists(DOKU_INC . 'lib/images/fileicons/' . $size . '/' . $ext . '.png')) {
1486          $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/' . $ext . '.png';
1487      } else {
1488          $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/file.png';
1489      }
1490  
1491      return '<img src="' . $icon . '" alt="' . $filename . '" class="icon" />';
1492  }
1493  
1494  /**
1495   * Build link based on the current, adding/rewriting parameters
1496   *
1497   * @author Kate Arzamastseva <pshns@ukr.net>
1498   *
1499   * @param array|bool $params
1500   * @param string     $amp           separator
1501   * @param bool       $abs           absolute url?
1502   * @param bool       $params_array  return the parmeters array?
1503   * @return string|array - link or link parameters
1504   */
1505  function media_managerURL($params = false, $amp = '&amp;', $abs = false, $params_array = false)
1506  {
1507      global $ID;
1508      global $INPUT;
1509  
1510      $gets = ['do' => 'media'];
1511      $media_manager_params = ['tab_files', 'tab_details', 'image', 'ns', 'list', 'sort'];
1512      foreach ($media_manager_params as $x) {
1513          if ($INPUT->has($x)) $gets[$x] = $INPUT->str($x);
1514      }
1515  
1516      if ($params) {
1517          $gets = $params + $gets;
1518      }
1519      unset($gets['id']);
1520      if (isset($gets['delete'])) {
1521          unset($gets['image']);
1522          unset($gets['tab_details']);
1523      }
1524  
1525      if ($params_array) return $gets;
1526  
1527      return wl($ID, $gets, $abs, $amp);
1528  }
1529  
1530  /**
1531   * Print the media upload form if permissions are correct
1532   *
1533   * @author Andreas Gohr <andi@splitbrain.org>
1534   * @author Kate Arzamastseva <pshns@ukr.net>
1535   *
1536   * @param string $ns
1537   * @param int    $auth permission level
1538   * @param bool  $fullscreen
1539   */
1540  function media_uploadform($ns, $auth, $fullscreen = false)
1541  {
1542      global $lang;
1543      global $conf;
1544      global $INPUT;
1545  
1546      if ($auth < AUTH_UPLOAD) {
1547          echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . NL;
1548          return;
1549      }
1550      $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
1551  
1552      $update = false;
1553      $id = '';
1554      if ($auth >= $auth_ow && $fullscreen && $INPUT->str('mediado') == 'update') {
1555          $update = true;
1556          $id = cleanID($INPUT->str('image'));
1557      }
1558  
1559      // The default HTML upload form
1560      $form = new Form([
1561          'id' => 'dw__upload',
1562          'enctype' => 'multipart/form-data',
1563          'action' => ($fullscreen)
1564                      ? media_managerURL(['tab_files' => 'files', 'tab_details' => 'view'], '&')
1565                      : DOKU_BASE . 'lib/exe/mediamanager.php',
1566      ]);
1567      $form->addTagOpen('div')->addClass('no');
1568      $form->setHiddenField('ns', hsc($ns));  // FIXME hsc required?
1569      $form->addTagOpen('p');
1570      $form->addTextInput('upload', $lang['txt_upload'])->id('upload__file')
1571              ->attrs(['type' => 'file']);
1572      $form->addTagClose('p');
1573      $form->addTagOpen('p');
1574      $form->addTextInput('mediaid', $lang['txt_filename'])->id('upload__name')
1575              ->val(noNS($id));
1576      $form->addButton('', $lang['btn_upload'])->attr('type', 'submit');
1577      $form->addTagClose('p');
1578      if ($auth >= $auth_ow) {
1579          $form->addTagOpen('p');
1580          $attrs = [];
1581          if ($update) $attrs['checked'] = 'checked';
1582          $form->addCheckbox('ow', $lang['txt_overwrt'])->id('dw__ow')->val('1')
1583              ->addClass('check')->attrs($attrs);
1584          $form->addTagClose('p');
1585      }
1586      $form->addTagClose('div');
1587  
1588      if (!$fullscreen) {
1589          echo '<div class="upload">' . $lang['mediaupload'] . '</div>' . DOKU_LF;
1590      } else {
1591          echo DOKU_LF;
1592      }
1593  
1594      echo '<div id="mediamanager__uploader">' . DOKU_LF;
1595      echo $form->toHTML('Upload');
1596      echo '</div>' . DOKU_LF;
1597  
1598      echo '<p class="maxsize">';
1599      printf($lang['maxuploadsize'], filesize_h(media_getuploadsize()));
1600      echo ' <a class="allowedmime" href="#">' . $lang['allowedmime'] . '</a>';
1601      echo ' <span>' . implode(', ', array_keys(getMimeTypes())) . '</span>';
1602      echo '</p>' . DOKU_LF;
1603  }
1604  
1605  /**
1606   * Returns the size uploaded files may have
1607   *
1608   * This uses a conservative approach using the lowest number found
1609   * in any of the limiting ini settings
1610   *
1611   * @returns int size in bytes
1612   */
1613  function media_getuploadsize()
1614  {
1615      $okay = 0;
1616  
1617      $post = php_to_byte(@ini_get('post_max_size'));
1618      $suho = php_to_byte(@ini_get('suhosin.post.max_value_length'));
1619      $upld = php_to_byte(@ini_get('upload_max_filesize'));
1620  
1621      if ($post && ($post < $okay || $okay === 0)) $okay = $post;
1622      if ($suho && ($suho < $okay || $okay == 0)) $okay = $suho;
1623      if ($upld && ($upld < $okay || $okay == 0)) $okay = $upld;
1624  
1625      return $okay;
1626  }
1627  
1628  /**
1629   * Print the search field form
1630   *
1631   * @author Tobias Sarnowski <sarnowski@cosmocode.de>
1632   * @author Kate Arzamastseva <pshns@ukr.net>
1633   *
1634   * @param string $ns
1635   * @param string $query
1636   * @param bool $fullscreen
1637   */
1638  function media_searchform($ns, $query = '', $fullscreen = false)
1639  {
1640      global $lang;
1641  
1642      // The default HTML search form
1643      $form = new Form([
1644          'id'     => 'dw__mediasearch',
1645          'action' => ($fullscreen)
1646                      ? media_managerURL([], '&')
1647                      : DOKU_BASE . 'lib/exe/mediamanager.php',
1648      ]);
1649      $form->addTagOpen('div')->addClass('no');
1650      $form->setHiddenField('ns', $ns);
1651      $form->setHiddenField($fullscreen ? 'mediado' : 'do', 'searchlist');
1652  
1653      $form->addTagOpen('p');
1654      $form->addTextInput('q', $lang['searchmedia'])
1655              ->attr('title', sprintf($lang['searchmedia_in'], hsc($ns) . ':*'))
1656              ->val($query);
1657      $form->addHTML(' ');
1658      $form->addButton('', $lang['btn_search'])->attr('type', 'submit');
1659      $form->addTagClose('p');
1660      $form->addTagClose('div');
1661      echo $form->toHTML('SearchMedia');
1662  }
1663  
1664  /**
1665   * Build a tree outline of available media namespaces
1666   *
1667   * @author Andreas Gohr <andi@splitbrain.org>
1668   *
1669   * @param string $ns
1670   */
1671  function media_nstree($ns)
1672  {
1673      global $conf;
1674      global $lang;
1675  
1676      // currently selected namespace
1677      $ns  = cleanID($ns);
1678      if (empty($ns)) {
1679          global $ID;
1680          $ns = (string)getNS($ID);
1681      }
1682  
1683      $ns_dir  = utf8_encodeFN(str_replace(':', '/', $ns));
1684  
1685      $data = [];
1686      search($data, $conf['mediadir'], 'search_index', ['ns' => $ns_dir, 'nofiles' => true]);
1687  
1688      // wrap a list with the root level around the other namespaces
1689      array_unshift($data, ['level' => 0, 'id' => '', 'open' => 'true', 'label' => '[' . $lang['mediaroot'] . ']']);
1690  
1691      // insert the current ns into the hierarchy if it isn't already part of it
1692      $ns_parts = explode(':', $ns);
1693      $tmp_ns = '';
1694      $pos = 0;
1695      foreach ($ns_parts as $level => $part) {
1696          if ($tmp_ns) $tmp_ns .= ':' . $part;
1697          else $tmp_ns = $part;
1698  
1699          // find the namespace parts or insert them
1700          while ($data[$pos]['id'] != $tmp_ns) {
1701              if (
1702                  $pos >= count($data) ||
1703                  ($data[$pos]['level'] <= $level + 1 && Sort::strcmp($data[$pos]['id'], $tmp_ns) > 0)
1704              ) {
1705                  array_splice($data, $pos, 0, [['level' => $level + 1, 'id' => $tmp_ns, 'open' => 'true']]);
1706                  break;
1707              }
1708              ++$pos;
1709          }
1710      }
1711  
1712      echo html_buildlist($data, 'idx', 'media_nstree_item', 'media_nstree_li');
1713  }
1714  
1715  /**
1716   * Userfunction for html_buildlist
1717   *
1718   * Prints a media namespace tree item
1719   *
1720   * @author Andreas Gohr <andi@splitbrain.org>
1721   *
1722   * @param array $item
1723   * @return string html
1724   */
1725  function media_nstree_item($item)
1726  {
1727      global $INPUT;
1728      $pos   = strrpos($item['id'], ':');
1729      $label = substr($item['id'], $pos > 0 ? $pos + 1 : 0);
1730      if (empty($item['label'])) $item['label'] = $label;
1731  
1732      $ret  = '';
1733      if ($INPUT->str('do') != 'media')
1734      $ret .= '<a href="' . DOKU_BASE . 'lib/exe/mediamanager.php?ns=' . idfilter($item['id']) . '" class="idx_dir">';
1735      else $ret .= '<a href="' . media_managerURL(['ns' => idfilter($item['id'], false), 'tab_files' => 'files'])
1736          . '" class="idx_dir">';
1737      $ret .= $item['label'];
1738      $ret .= '</a>';
1739      return $ret;
1740  }
1741  
1742  /**
1743   * Userfunction for html_buildlist
1744   *
1745   * Prints a media namespace tree item opener
1746   *
1747   * @author Andreas Gohr <andi@splitbrain.org>
1748   *
1749   * @param array $item
1750   * @return string html
1751   */
1752  function media_nstree_li($item)
1753  {
1754      $class = 'media level' . $item['level'];
1755      if ($item['open']) {
1756          $class .= ' open';
1757          $img   = DOKU_BASE . 'lib/images/minus.gif';
1758          $alt   = '−';
1759      } else {
1760          $class .= ' closed';
1761          $img   = DOKU_BASE . 'lib/images/plus.gif';
1762          $alt   = '+';
1763      }
1764      // TODO: only deliver an image if it actually has a subtree...
1765      return '<li class="' . $class . '">' .
1766          '<img src="' . $img . '" alt="' . $alt . '" />';
1767  }
1768  
1769  /**
1770   * Resizes or crop the given image to the given size
1771   *
1772   * @author  Andreas Gohr <andi@splitbrain.org>
1773   *
1774   * @param string $file filename, path to file
1775   * @param string $ext  extension
1776   * @param int    $w    desired width
1777   * @param int    $h    desired height
1778   * @param bool   $crop should a center crop be used?
1779   * @return string path to resized or original size if failed
1780   */
1781  function media_mod_image($file, $ext, $w, $h = 0, $crop = false)
1782  {
1783      global $conf;
1784      if (!$h) $h = 0;
1785      // we wont scale up to infinity
1786      if ($w > 2000 || $h > 2000) return $file;
1787  
1788      $operation = $crop ? 'crop' : 'resize';
1789  
1790      $options = [
1791          'quality' => $conf['jpg_quality'],
1792          'imconvert' => $conf['im_convert'],
1793      ];
1794  
1795      $cache = new CacheImageMod($file, $w, $h, $ext, $crop);
1796      if (!$cache->useCache()) {
1797          try {
1798              Slika::run($file, $options)
1799                   ->autorotate()
1800                   ->$operation($w, $h)
1801                   ->save($cache->cache, $ext);
1802              if ($conf['fperm']) @chmod($cache->cache, $conf['fperm']);
1803          } catch (Exception $e) {
1804              Logger::debug($e->getMessage());
1805              return $file;
1806          }
1807      }
1808  
1809      return $cache->cache;
1810  }
1811  
1812  /**
1813   * Resizes the given image to the given size
1814   *
1815   * @author  Andreas Gohr <andi@splitbrain.org>
1816   *
1817   * @param string $file filename, path to file
1818   * @param string $ext  extension
1819   * @param int    $w    desired width
1820   * @param int    $h    desired height
1821   * @return string path to resized or original size if failed
1822   */
1823  function media_resize_image($file, $ext, $w, $h = 0)
1824  {
1825      return media_mod_image($file, $ext, $w, $h, false);
1826  }
1827  
1828  /**
1829   * Center crops the given image to the wanted size
1830   *
1831   * @author  Andreas Gohr <andi@splitbrain.org>
1832   *
1833   * @param string $file filename, path to file
1834   * @param string $ext  extension
1835   * @param int    $w    desired width
1836   * @param int    $h    desired height
1837   * @return string path to resized or original size if failed
1838   */
1839  function media_crop_image($file, $ext, $w, $h = 0)
1840  {
1841      return media_mod_image($file, $ext, $w, $h, true);
1842  }
1843  
1844  /**
1845   * Calculate a token to be used to verify fetch requests for resized or
1846   * cropped images have been internally generated - and prevent external
1847   * DDOS attacks via fetch
1848   *
1849   * @author Christopher Smith <chris@jalakai.co.uk>
1850   *
1851   * @param string  $id    id of the image
1852   * @param int     $w     resize/crop width
1853   * @param int     $h     resize/crop height
1854   * @return string token or empty string if no token required
1855   */
1856  function media_get_token($id, $w, $h)
1857  {
1858      // token is only required for modified images
1859      if ($w || $h || media_isexternal($id)) {
1860          $token = $id;
1861          if ($w) $token .= '.' . $w;
1862          if ($h) $token .= '.' . $h;
1863  
1864          return substr(PassHash::hmac('md5', $token, auth_cookiesalt()), 0, 6);
1865      }
1866  
1867      return '';
1868  }
1869  
1870  /**
1871   * Download a remote file and return local filename
1872   *
1873   * returns false if download fails. Uses cached file if available and
1874   * wanted
1875   *
1876   * @author  Andreas Gohr <andi@splitbrain.org>
1877   * @author  Pavel Vitis <Pavel.Vitis@seznam.cz>
1878   *
1879   * @param string $url
1880   * @param string $ext   extension
1881   * @param int    $cache cachetime in seconds
1882   * @return false|string path to cached file
1883   */
1884  function media_get_from_URL($url, $ext, $cache)
1885  {
1886      global $conf;
1887  
1888      // if no cache or fetchsize just redirect
1889      if ($cache == 0)           return false;
1890      if (!$conf['fetchsize']) return false;
1891  
1892      $local = getCacheName(strtolower($url), ".media.$ext");
1893      $mtime = @filemtime($local); // 0 if not exists
1894  
1895      //decide if download needed:
1896      if (
1897          ($mtime == 0) || // cache does not exist
1898          ($cache != -1 && $mtime < time() - $cache) // 'recache' and cache has expired
1899      ) {
1900          if (media_image_download($url, $local)) {
1901              return $local;
1902          } else {
1903              return false;
1904          }
1905      }
1906  
1907      //if cache exists use it else
1908      if ($mtime) return $local;
1909  
1910      //else return false
1911      return false;
1912  }
1913  
1914  /**
1915   * Download image files
1916   *
1917   * @author Andreas Gohr <andi@splitbrain.org>
1918   *
1919   * @param string $url
1920   * @param string $file path to file in which to put the downloaded content
1921   * @return bool
1922   */
1923  function media_image_download($url, $file)
1924  {
1925      global $conf;
1926      $http = new DokuHTTPClient();
1927      $http->keep_alive = false; // we do single ops here, no need for keep-alive
1928  
1929      $http->max_bodysize = $conf['fetchsize'];
1930      $http->timeout = 25; //max. 25 sec
1931      $http->header_regexp = '!\r\nContent-Type: image/(jpe?g|gif|png)!i';
1932  
1933      $data = $http->get($url);
1934      if (!$data) return false;
1935  
1936      $fileexists = file_exists($file);
1937      $fp = @fopen($file, "w");
1938      if (!$fp) return false;
1939      fwrite($fp, $data);
1940      fclose($fp);
1941      if (!$fileexists && $conf['fperm']) chmod($file, $conf['fperm']);
1942  
1943      // check if it is really an image
1944      $info = @getimagesize($file);
1945      if (!$info) {
1946          @unlink($file);
1947          return false;
1948      }
1949  
1950      return true;
1951  }
1952  
1953  /**
1954   * resize images using external ImageMagick convert program
1955   *
1956   * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
1957   * @author Andreas Gohr <andi@splitbrain.org>
1958   *
1959   * @param string $ext     extension
1960   * @param string $from    filename path to file
1961   * @param int    $from_w  original width
1962   * @param int    $from_h  original height
1963   * @param string $to      path to resized file
1964   * @param int    $to_w    desired width
1965   * @param int    $to_h    desired height
1966   * @return bool
1967   */
1968  function media_resize_imageIM($ext, $from, $from_w, $from_h, $to, $to_w, $to_h)
1969  {
1970      global $conf;
1971  
1972      // check if convert is configured
1973      if (!$conf['im_convert']) return false;
1974  
1975      // prepare command
1976      $cmd  = $conf['im_convert'];
1977      $cmd .= ' -resize ' . $to_w . 'x' . $to_h . '!';
1978      if ($ext == 'jpg' || $ext == 'jpeg') {
1979          $cmd .= ' -quality ' . $conf['jpg_quality'];
1980      }
1981      $cmd .= " $from $to";
1982  
1983      @exec($cmd, $out, $retval);
1984      if ($retval == 0) return true;
1985      return false;
1986  }
1987  
1988  /**
1989   * crop images using external ImageMagick convert program
1990   *
1991   * @author Andreas Gohr <andi@splitbrain.org>
1992   *
1993   * @param string $ext     extension
1994   * @param string $from    filename path to file
1995   * @param int    $from_w  original width
1996   * @param int    $from_h  original height
1997   * @param string $to      path to resized file
1998   * @param int    $to_w    desired width
1999   * @param int    $to_h    desired height
2000   * @param int    $ofs_x   offset of crop centre
2001   * @param int    $ofs_y   offset of crop centre
2002   * @return bool
2003   * @deprecated 2020-09-01
2004   */
2005  function media_crop_imageIM($ext, $from, $from_w, $from_h, $to, $to_w, $to_h, $ofs_x, $ofs_y)
2006  {
2007      global $conf;
2008      dbg_deprecated('splitbrain\\Slika');
2009  
2010      // check if convert is configured
2011      if (!$conf['im_convert']) return false;
2012  
2013      // prepare command
2014      $cmd  = $conf['im_convert'];
2015      $cmd .= ' -crop ' . $to_w . 'x' . $to_h . '+' . $ofs_x . '+' . $ofs_y;
2016      if ($ext == 'jpg' || $ext == 'jpeg') {
2017          $cmd .= ' -quality ' . $conf['jpg_quality'];
2018      }
2019      $cmd .= " $from $to";
2020  
2021      @exec($cmd, $out, $retval);
2022      if ($retval == 0) return true;
2023      return false;
2024  }
2025  
2026  /**
2027   * resize or crop images using PHP's libGD support
2028   *
2029   * @author Andreas Gohr <andi@splitbrain.org>
2030   * @author Sebastian Wienecke <s_wienecke@web.de>
2031   *
2032   * @param string $ext     extension
2033   * @param string $from    filename path to file
2034   * @param int    $from_w  original width
2035   * @param int    $from_h  original height
2036   * @param string $to      path to resized file
2037   * @param int    $to_w    desired width
2038   * @param int    $to_h    desired height
2039   * @param int    $ofs_x   offset of crop centre
2040   * @param int    $ofs_y   offset of crop centre
2041   * @return bool
2042   * @deprecated 2020-09-01
2043   */
2044  function media_resize_imageGD($ext, $from, $from_w, $from_h, $to, $to_w, $to_h, $ofs_x = 0, $ofs_y = 0)
2045  {
2046      global $conf;
2047      dbg_deprecated('splitbrain\\Slika');
2048  
2049      if ($conf['gdlib'] < 1) return false; //no GDlib available or wanted
2050  
2051      // check available memory
2052      if (!is_mem_available(($from_w * $from_h * 4) + ($to_w * $to_h * 4))) {
2053          return false;
2054      }
2055  
2056      // create an image of the given filetype
2057      $image = false;
2058      if ($ext == 'jpg' || $ext == 'jpeg') {
2059          if (!function_exists("imagecreatefromjpeg")) return false;
2060          $image = @imagecreatefromjpeg($from);
2061      } elseif ($ext == 'png') {
2062          if (!function_exists("imagecreatefrompng")) return false;
2063          $image = @imagecreatefrompng($from);
2064      } elseif ($ext == 'gif') {
2065          if (!function_exists("imagecreatefromgif")) return false;
2066          $image = @imagecreatefromgif($from);
2067      }
2068      if (!$image) return false;
2069  
2070      $newimg = false;
2071      if (($conf['gdlib'] > 1) && function_exists("imagecreatetruecolor") && $ext != 'gif') {
2072          $newimg = @imagecreatetruecolor($to_w, $to_h);
2073      }
2074      if (!$newimg) $newimg = @imagecreate($to_w, $to_h);
2075      if (!$newimg) {
2076          imagedestroy($image);
2077          return false;
2078      }
2079  
2080      //keep png alpha channel if possible
2081      if ($ext == 'png' && $conf['gdlib'] > 1 && function_exists('imagesavealpha')) {
2082          imagealphablending($newimg, false);
2083          imagesavealpha($newimg, true);
2084      }
2085  
2086      //keep gif transparent color if possible
2087      if ($ext == 'gif' && function_exists('imagefill') && function_exists('imagecolorallocate')) {
2088          if (function_exists('imagecolorsforindex') && function_exists('imagecolortransparent')) {
2089              $transcolorindex = @imagecolortransparent($image);
2090              if ($transcolorindex >= 0) { //transparent color exists
2091                  $transcolor = @imagecolorsforindex($image, $transcolorindex);
2092                  $transcolorindex = @imagecolorallocate(
2093                      $newimg,
2094                      $transcolor['red'],
2095                      $transcolor['green'],
2096                      $transcolor['blue']
2097                  );
2098                  @imagefill($newimg, 0, 0, $transcolorindex);
2099                  @imagecolortransparent($newimg, $transcolorindex);
2100              } else { //filling with white
2101                  $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
2102                  @imagefill($newimg, 0, 0, $whitecolorindex);
2103              }
2104          } else { //filling with white
2105              $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
2106              @imagefill($newimg, 0, 0, $whitecolorindex);
2107          }
2108      }
2109  
2110      //try resampling first
2111      if (function_exists("imagecopyresampled")) {
2112          if (!@imagecopyresampled($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h)) {
2113              imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
2114          }
2115      } else {
2116          imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
2117      }
2118  
2119      $okay = false;
2120      if ($ext == 'jpg' || $ext == 'jpeg') {
2121          if (!function_exists('imagejpeg')) {
2122              $okay = false;
2123          } else {
2124              $okay = imagejpeg($newimg, $to, $conf['jpg_quality']);
2125          }
2126      } elseif ($ext == 'png') {
2127          if (!function_exists('imagepng')) {
2128              $okay = false;
2129          } else {
2130              $okay =  imagepng($newimg, $to);
2131          }
2132      } elseif ($ext == 'gif') {
2133          if (!function_exists('imagegif')) {
2134              $okay = false;
2135          } else {
2136              $okay = imagegif($newimg, $to);
2137          }
2138      }
2139  
2140      // destroy GD image resources
2141      imagedestroy($image);
2142      imagedestroy($newimg);
2143  
2144      return $okay;
2145  }
2146  
2147  /**
2148   * Return other media files with the same base name
2149   * but different extensions.
2150   *
2151   * @param string   $src     - ID of media file
2152   * @param string[] $exts    - alternative extensions to find other files for
2153   * @return array            - array(mime type => file ID)
2154   *
2155   * @author Anika Henke <anika@selfthinker.org>
2156   */
2157  function media_alternativefiles($src, $exts)
2158  {
2159  
2160      $files = [];
2161      [$srcExt, /* srcMime */] = mimetype($src);
2162      $filebase = substr($src, 0, -1 * (strlen($srcExt) + 1));
2163  
2164      foreach ($exts as $ext) {
2165          $fileid = $filebase . '.' . $ext;
2166          $file = mediaFN($fileid);
2167          if (file_exists($file)) {
2168              [/* fileExt */, $fileMime] = mimetype($file);
2169              $files[$fileMime] = $fileid;
2170          }
2171      }
2172      return $files;
2173  }
2174  
2175  /**
2176   * Check if video/audio is supported to be embedded.
2177   *
2178   * @param string $mime      - mimetype of media file
2179   * @param string $type      - type of media files to check ('video', 'audio', or null for all)
2180   * @return boolean
2181   *
2182   * @author Anika Henke <anika@selfthinker.org>
2183   */
2184  function media_supportedav($mime, $type = null)
2185  {
2186      $supportedAudio = [
2187          'ogg' => 'audio/ogg',
2188          'mp3' => 'audio/mpeg',
2189          'wav' => 'audio/wav'
2190      ];
2191      $supportedVideo = [
2192          'webm' => 'video/webm',
2193          'ogv' => 'video/ogg',
2194          'mp4' => 'video/mp4'
2195      ];
2196      if ($type == 'audio') {
2197          $supportedAv = $supportedAudio;
2198      } elseif ($type == 'video') {
2199          $supportedAv = $supportedVideo;
2200      } else {
2201          $supportedAv = array_merge($supportedAudio, $supportedVideo);
2202      }
2203      return in_array($mime, $supportedAv);
2204  }
2205  
2206  /**
2207   * Return track media files with the same base name
2208   * but extensions that indicate kind and lang.
2209   * ie for foo.webm search foo.sub.lang.vtt, foo.cap.lang.vtt...
2210   *
2211   * @param string   $src     - ID of media file
2212   * @return array            - array(mediaID => array( kind, srclang ))
2213   *
2214   * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
2215   */
2216  function media_trackfiles($src)
2217  {
2218      $kinds = [
2219          'sub' => 'subtitles',
2220          'cap' => 'captions',
2221          'des' => 'descriptions',
2222          'cha' => 'chapters',
2223          'met' => 'metadata'
2224      ];
2225  
2226      $files = [];
2227      $re = '/\\.(sub|cap|des|cha|met)\\.([^.]+)\\.vtt$/';
2228      $baseid = pathinfo($src, PATHINFO_FILENAME);
2229      $pattern = mediaFN($baseid) . '.*.*.vtt';
2230      $list = glob($pattern);
2231      foreach ($list as $track) {
2232          if (preg_match($re, $track, $matches)) {
2233              $files[$baseid . '.' . $matches[1] . '.' . $matches[2] . '.vtt'] = [$kinds[$matches[1]], $matches[2]];
2234          }
2235      }
2236      return $files;
2237  }
2238  
2239  /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */