PageRenderTime 22ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/include/cli/modules/unpack.php

https://gitlab.com/milo-ft/osTicket
PHP | 270 lines | 206 code | 33 blank | 31 comment | 40 complexity | 1a249204d2df851851fe62c17cbe03db MD5 | raw file
  1. <?php
  2. class Unpacker extends Module {
  3. var $prologue = "Unpacks osTicket into target install path";
  4. var $epilog =
  5. "Copies an unpacked osticket tarball or zipfile into a production
  6. location, optionally placing the include/ folder in a separate
  7. location if requested";
  8. var $options = array(
  9. 'include' => array('-i','--include', 'metavar'=>'path', 'help'=>
  10. "The include/ folder, which contains the bulk of osTicket's source
  11. code can be located outside of the install path. This is recommended
  12. for better security. If you would like to install the include/
  13. folder somewhere else, give the path here. Note that the full
  14. path is assumed, so give path/to/include/ to unpack the source
  15. code in that folder. The folder will be automatically created if
  16. it doesn't already exist."
  17. ),
  18. 'verbose' => array('-v','--verbose', 'default'=>false, 'nargs'=>0,
  19. 'action'=>'store_true', 'help'=>
  20. "Move verbose logging to stdout"),
  21. );
  22. var $arguments = array(
  23. 'install-path' =>
  24. "The destination for osTicket to reside. Use the --include
  25. option to specify destination of the include/ folder, if the
  26. administrator should chose to locate it separate from the
  27. main installation path.",
  28. );
  29. var $manifest;
  30. var $source;
  31. var $destination;
  32. function realpath($path) {
  33. return ($p = realpath($path)) ? $p : $path;
  34. }
  35. function find_upload_folder() {
  36. # Hop up to the root folder
  37. $start = dirname(__file__);
  38. for (;;) {
  39. if (is_dir($start . '/upload')) break;
  40. $start .= '/..';
  41. }
  42. return self::realpath($start.'/upload');
  43. }
  44. function change_include_dir($include_path) {
  45. # Read the main.inc.php script
  46. $bootstrap_php = $this->destination . '/bootstrap.php';
  47. $lines = explode("\n", file_get_contents($bootstrap_php));
  48. $include_path = preg_replace('://+:', '/', $include_path);
  49. # Try and use ROOT_DIR
  50. if (strpos($include_path, $this->destination) === 0)
  51. $include_path = "ROOT_DIR . '" .
  52. str_replace($this->destination, '', $include_path) . "'";
  53. else
  54. $include_path = "'$include_path'";
  55. # Find the line that defines INCLUDE_DIR
  56. $match = array();
  57. foreach ($lines as &$line) {
  58. // TODO: Change THIS_VERSION inline to be current `git describe`
  59. if (preg_match("/(\s*)define\s*\(\s*'INCLUDE_DIR'/", $line, $match)) {
  60. # Replace the definition with the new locatin
  61. $line = $match[1] . "define('INCLUDE_DIR', "
  62. . $include_path
  63. . "); // Set by installer";
  64. break;
  65. }
  66. }
  67. if (!file_put_contents($bootstrap_php, implode("\n", $lines)))
  68. die("Unable to configure location of INCLUDE_DIR in bootstrap.php\n");
  69. }
  70. function exclude($pattern, $match) {
  71. if (!$pattern) {
  72. return false;
  73. } elseif (is_array($pattern)) {
  74. foreach ($pattern as $p)
  75. if (fnmatch($p, $match))
  76. return true;
  77. } else {
  78. return fnmatch($pattern, $match);
  79. }
  80. return false;
  81. }
  82. function readManifest($file) {
  83. if (isset($this->manifest))
  84. return @$this->manifest[$file] ?: null;
  85. $this->manifest = $lines = array();
  86. $path = $this->get_include_dir() . '/.MANIFEST';
  87. if (!is_file($path))
  88. return null;
  89. if (!preg_match_all('/^([\w:,]+) (.+)$/mu', file_get_contents($path),
  90. $lines, PREG_PATTERN_ORDER)
  91. ) {
  92. return null;
  93. }
  94. $this->manifest = array_combine($lines[2], $lines[1]);
  95. return @$this->manifest[$file] ?: null;
  96. }
  97. function hashFile($file) {
  98. static $hashes = array();
  99. if (!isset($hashes[$file])) {
  100. $md5 = md5_file($file);
  101. $sha1 = sha1_file($file);
  102. $hash = substr($md5, -20) . substr($sha1, -20);
  103. $hashes[$file] = $hash;
  104. }
  105. return $hashes[$file];
  106. }
  107. function isChanged($source, $hash=false) {
  108. $local = str_replace($this->source.'/', '', $source);
  109. $hash = $hash ?: $this->hashFile($source);
  110. return $this->readManifest($local) != $hash;
  111. }
  112. function updateManifest($file, $hash=false) {
  113. $hash = $hash ?: $this->hashFile($file);
  114. $local = str_replace($this->source.'/', '', $file);
  115. $this->manifest[$local] = $hash;
  116. }
  117. function copyFile($src, $dest, $hash=false, $mode=0644) {
  118. $this->updateManifest($src, $hash);
  119. return copy($src, $dest) && chmod($dest, $mode);
  120. }
  121. /**
  122. * Copy from source to desination, perhaps recursing up to n folders.
  123. * Exclusions are also permitted. If any files match an MD5 sum, they
  124. * will be excluded from the copy operation.
  125. *
  126. * Parameters:
  127. * folder - (string) source folder root
  128. * destination - (string) destination folder root
  129. * recurse - (int) recuse up to this many folders. Use 0 or false to
  130. * disable recursion, and -1 to recurse infinite folders.
  131. * exclude - (string | array<string>) patterns that will be matched
  132. * using the PHP `fnmatch` function. If any file or folder matches,
  133. * it will be excluded from the copy procedure. Omit or use false
  134. * to disable exclusions
  135. */
  136. function unpackage($folder, $destination, $recurse=0, $exclude=false) {
  137. $dryrun = $this->getOption('dry-run', false);
  138. $verbose = $this->getOption('verbose') || $dryrun;
  139. $force = $this->getOption('force', false);
  140. if (substr($destination, -1) !== '/')
  141. $destination .= '/';
  142. foreach (glob($folder, GLOB_BRACE|GLOB_NOSORT) as $file) {
  143. if ($this->exclude($exclude, $file))
  144. continue;
  145. if (is_file($file)) {
  146. $target = $destination . basename($file);
  147. $hash = $this->hashFile($file);
  148. if (!$force && is_file($target)
  149. && false === ($flag = $this->isChanged($file, $hash)))
  150. continue;
  151. if ($verbose) {
  152. $msg = $target;
  153. if (is_string($flag))
  154. $msg = "$msg ({$flag})";
  155. $this->stdout->write("$msg\n");
  156. }
  157. if ($dryrun)
  158. continue;
  159. if (!is_dir($destination))
  160. mkdir($destination, 0755, true);
  161. $this->copyFile($file, $target, $hash);
  162. }
  163. }
  164. if ($recurse) {
  165. $folders = glob(dirname($folder).'/'.basename($folder),
  166. GLOB_BRACE|GLOB_ONLYDIR|GLOB_NOSORT);
  167. foreach ($folders as $dir) {
  168. if (in_array(basename($dir), array('.','..')))
  169. continue;
  170. elseif ($this->exclude($exclude, $dir))
  171. continue;
  172. $this->unpackage(
  173. dirname($folder).'/'.basename($dir).'/'.basename($folder),
  174. $destination.basename($dir),
  175. $recurse - 1, $exclude);
  176. }
  177. }
  178. }
  179. function get_include_dir() {
  180. static $location;
  181. if (isset($location))
  182. return $location;
  183. $pipes = array();
  184. $php = proc_open('php', array(
  185. 0 => array('pipe', 'r'),
  186. 1 => array('pipe', 'w'),
  187. ), $pipes);
  188. fwrite($pipes[0], "<?php
  189. include '{$this->destination}/bootstrap.php';
  190. print INCLUDE_DIR;
  191. ");
  192. fclose($pipes[0]);
  193. $INCLUDE_DIR = fread($pipes[1], 8192);
  194. proc_close($php);
  195. return $location = rtrim($INCLUDE_DIR, '/').'/';
  196. }
  197. function bootstrap() {
  198. // Don't load config and frieds as that will likely crash if not yet
  199. // installed
  200. }
  201. function run($args, $options) {
  202. $this->destination = $args['install-path'];
  203. if (!is_dir($this->destination))
  204. if (!mkdir($this->destination, 0751, true))
  205. die("Destination path does not exist and cannot be created");
  206. # Determine if this is an upgrade, and if so, where the include/
  207. # folder is currently located
  208. $upgrade = file_exists("{$this->destination}/main.inc.php");
  209. # Locate the upload folder
  210. $upload = $this->source = $this->find_upload_folder();
  211. # Unpack the upload folder to the destination, except the include folder
  212. if ($upgrade)
  213. # Get the current value of the INCLUDE_DIR before overwriting
  214. # main.inc.php
  215. $include = $this->get_include_dir();
  216. $this->unpackage("$upload/{,.}*", $this->destination, -1, "*include");
  217. if (!$upgrade) {
  218. if ($this->getOption('include')) {
  219. $location = $this->getOption('include');
  220. if (!is_dir("$location/"))
  221. if (!mkdir("$location/", 0751, true))
  222. die("Unable to create folder for include/ files\n");
  223. $this->unpackage("$upload/include/{,.}*", $location, -1);
  224. $this->change_include_dir($location);
  225. }
  226. else
  227. $this->unpackage("$upload/include/{,.}*", "{$this->destination}/include", -1);
  228. }
  229. else {
  230. $this->unpackage("$upload/include/{,.}*", $include, -1);
  231. # Change the new main.inc.php to reflect the location of the
  232. # include/ directory
  233. $this->change_include_dir($include);
  234. }
  235. }
  236. }
  237. Module::register('unpack', 'Unpacker');