Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   1  <?php
   2  
   3  namespace MatthiasMullie\PathConverter;
   4  
   5  /**
   6   * Convert paths relative from 1 file to another.
   7   *
   8   * E.g.
   9   *     ../../images/icon.jpg relative to /css/imports/icons.css
  10   * becomes
  11   *     ../images/icon.jpg relative to /css/minified.css
  12   *
  13   * Please report bugs on https://github.com/matthiasmullie/path-converter/issues
  14   *
  15   * @author Matthias Mullie <pathconverter@mullie.eu>
  16   * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
  17   * @license MIT License
  18   */
  19  class Converter implements ConverterInterface
  20  {
  21      /**
  22       * @var string
  23       */
  24      protected $from;
  25  
  26      /**
  27       * @var string
  28       */
  29      protected $to;
  30  
  31      /**
  32       * @param string $from The original base path (directory, not file!)
  33       * @param string $to   The new base path (directory, not file!)
  34       * @param string $root Root directory (defaults to `getcwd`)
  35       */
  36      public function __construct($from, $to, $root = '')
  37      {
  38          $shared = $this->shared($from, $to);
  39          if ($shared === '') {
  40              // when both paths have nothing in common, one of them is probably
  41              // absolute while the other is relative
  42              $root = $root ?: getcwd();
  43              $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from);
  44              $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to);
  45  
  46              // or traveling the tree via `..`
  47              // attempt to resolve path, or assume it's fine if it doesn't exist
  48              $from = @realpath($from) ?: $from;
  49              $to = @realpath($to) ?: $to;
  50          }
  51  
  52          $from = $this->dirname($from);
  53          $to = $this->dirname($to);
  54  
  55          $from = $this->normalize($from);
  56          $to = $this->normalize($to);
  57  
  58          $this->from = $from;
  59          $this->to = $to;
  60      }
  61  
  62      /**
  63       * Normalize path.
  64       *
  65       * @param string $path
  66       *
  67       * @return string
  68       */
  69      protected function normalize($path)
  70      {
  71          // deal with different operating systems' directory structure
  72          $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
  73  
  74          // remove leading current directory.
  75          if (substr($path, 0, 2) === './') {
  76              $path = substr($path, 2);
  77          }
  78  
  79          // remove references to current directory in the path.
  80          $path = str_replace('/./', '/', $path);
  81  
  82          /*
  83           * Example:
  84           *     /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif
  85           * to
  86           *     /home/forkcms/frontend/core/layout/images/img.gif
  87           */
  88          do {
  89              $path = preg_replace('/[^\/]+(?<!\.\.)\/\.\.\//', '', $path, -1, $count);
  90          } while ($count);
  91  
  92          return $path;
  93      }
  94  
  95      /**
  96       * Figure out the shared path of 2 locations.
  97       *
  98       * Example:
  99       *     /home/forkcms/frontend/core/layout/images/img.gif
 100       * and
 101       *     /home/forkcms/frontend/cache/minified_css
 102       * share
 103       *     /home/forkcms/frontend
 104       *
 105       * @param string $path1
 106       * @param string $path2
 107       *
 108       * @return string
 109       */
 110      protected function shared($path1, $path2)
 111      {
 112          // $path could theoretically be empty (e.g. no path is given), in which
 113          // case it shouldn't expand to array(''), which would compare to one's
 114          // root /
 115          $path1 = $path1 ? explode('/', $path1) : array();
 116          $path2 = $path2 ? explode('/', $path2) : array();
 117  
 118          $shared = array();
 119  
 120          // compare paths & strip identical ancestors
 121          foreach ($path1 as $i => $chunk) {
 122              if (isset($path2[$i]) && $path1[$i] == $path2[$i]) {
 123                  $shared[] = $chunk;
 124              } else {
 125                  break;
 126              }
 127          }
 128  
 129          return implode('/', $shared);
 130      }
 131  
 132      /**
 133       * Convert paths relative from 1 file to another.
 134       *
 135       * E.g.
 136       *     ../images/img.gif relative to /home/forkcms/frontend/core/layout/css
 137       * should become:
 138       *     ../../core/layout/images/img.gif relative to
 139       *     /home/forkcms/frontend/cache/minified_css
 140       *
 141       * @param string $path The relative path that needs to be converted
 142       *
 143       * @return string The new relative path
 144       */
 145      public function convert($path)
 146      {
 147          // quit early if conversion makes no sense
 148          if ($this->from === $this->to) {
 149              return $path;
 150          }
 151  
 152          $path = $this->normalize($path);
 153          // if we're not dealing with a relative path, just return absolute
 154          if (strpos($path, '/') === 0) {
 155              return $path;
 156          }
 157  
 158          // normalize paths
 159          $path = $this->normalize($this->from.'/'.$path);
 160  
 161          // strip shared ancestor paths
 162          $shared = $this->shared($path, $this->to);
 163          $path = mb_substr($path, mb_strlen($shared));
 164          $to = mb_substr($this->to, mb_strlen($shared));
 165  
 166          // add .. for every directory that needs to be traversed to new path
 167          $to = str_repeat('../', count(array_filter(explode('/', $to))));
 168  
 169          return $to.ltrim($path, '/');
 170      }
 171  
 172      /**
 173       * Attempt to get the directory name from a path.
 174       *
 175       * @param string $path
 176       *
 177       * @return string
 178       */
 179      protected function dirname($path)
 180      {
 181          if (@is_file($path)) {
 182              return dirname($path);
 183          }
 184  
 185          if (@is_dir($path)) {
 186              return rtrim($path, '/');
 187          }
 188  
 189          // no known file/dir, start making assumptions
 190  
 191          // ends in / = dir
 192          if (mb_substr($path, -1) === '/') {
 193              return rtrim($path, '/');
 194          }
 195  
 196          // has a dot in the name, likely a file
 197          if (preg_match('/.*\..*$/', basename($path)) !== 0) {
 198              return dirname($path);
 199          }
 200  
 201          // you're on your own here!
 202          return $path;
 203      }
 204  }