IniReader.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. */
  8. namespace Piwik\Ini;
  9. /**
  10. * Reads INI configuration.
  11. */
  12. class IniReader
  13. {
  14. /**
  15. * @var bool
  16. */
  17. private $useNativeFunction;
  18. public function __construct()
  19. {
  20. $this->useNativeFunction = function_exists('parse_ini_string');
  21. }
  22. /**
  23. * Reads a INI configuration file and returns it as an array.
  24. *
  25. * The array returned is multidimensional, indexed by section names:
  26. *
  27. * ```
  28. * array(
  29. * 'Section 1' => array(
  30. * 'value1' => 'hello',
  31. * 'value2' => 'world',
  32. * ),
  33. * 'Section 2' => array(
  34. * 'value3' => 'foo',
  35. * )
  36. * );
  37. * ```
  38. *
  39. * @param string $filename The file to read.
  40. * @throws IniReadingException
  41. * @return array
  42. */
  43. public function readFile($filename)
  44. {
  45. $ini = $this->getContentOfIniFile($filename);
  46. return $this->readString($ini);
  47. }
  48. /**
  49. * Reads a INI configuration string and returns it as an array.
  50. *
  51. * The array returned is multidimensional, indexed by section names:
  52. *
  53. * ```
  54. * array(
  55. * 'Section 1' => array(
  56. * 'value1' => 'hello',
  57. * 'value2' => 'world',
  58. * ),
  59. * 'Section 2' => array(
  60. * 'value3' => 'foo',
  61. * )
  62. * );
  63. * ```
  64. *
  65. * @param string $ini String containing INI configuration.
  66. * @throws IniReadingException
  67. * @return array
  68. */
  69. public function readString($ini)
  70. {
  71. // On PHP 5.3.3 an empty line return is needed at the end
  72. // See http://3v4l.org/jD1Lh
  73. $ini .= "\n";
  74. if ($this->useNativeFunction) {
  75. $array = $this->readWithNativeFunction($ini);
  76. } else {
  77. $array = $this->readWithAlternativeImplementation($ini);
  78. }
  79. return $array;
  80. }
  81. /**
  82. * @param string $ini
  83. * @throws IniReadingException
  84. * @return array
  85. */
  86. private function readWithNativeFunction($ini)
  87. {
  88. $array = @parse_ini_string($ini, true);
  89. if ($array === false) {
  90. $e = error_get_last();
  91. throw new IniReadingException('Syntax error in INI configuration: ' . $e['message']);
  92. }
  93. // We cannot use INI_SCANNER_RAW by default because it is buggy under PHP 5.3.14 and 5.4.4
  94. // http://3v4l.org/m24cT
  95. $rawValues = @parse_ini_string($ini, true, INI_SCANNER_RAW);
  96. $array = $this->decode($array, $rawValues);
  97. return $array;
  98. }
  99. private function getContentOfIniFile($filename)
  100. {
  101. if (!file_exists($filename) || !is_readable($filename)) {
  102. throw new IniReadingException(sprintf("The file %s doesn't exist or is not readable", $filename));
  103. }
  104. $content = $this->getFileContent($filename);
  105. if ($content === false) {
  106. throw new IniReadingException(sprintf('Impossible to read the file %s', $filename));
  107. }
  108. return $content;
  109. }
  110. /**
  111. * Reads ini comments for each key.
  112. *
  113. * The array returned is multidimensional, indexed by section names:
  114. *
  115. * ```
  116. * array(
  117. * 'Section 1' => array(
  118. * 'key1' => 'comment 1',
  119. * 'key2' => 'comment 2',
  120. * ),
  121. * 'Section 2' => array(
  122. * 'key3' => 'comment 3',
  123. * )
  124. * );
  125. * ```
  126. *
  127. * @param string $filename The path to a file.
  128. * @throws IniReadingException
  129. * @return array
  130. */
  131. public function readComments($filename)
  132. {
  133. $ini = $this->getContentOfIniFile($filename);
  134. $ini = $this->splitIniContentIntoLines($ini);
  135. $descriptions = array();
  136. $section = '';
  137. $lastComment = '';
  138. foreach ($ini as $line) {
  139. $line = trim($line);
  140. if (strpos($line, '[') === 0) {
  141. $tmp = explode(']', $line);
  142. $section = trim(substr($tmp[0], 1));
  143. $descriptions[$section] = array();
  144. $lastComment = '';
  145. continue;
  146. }
  147. if (!preg_match('/^[a-zA-Z0-9[]/', $line)) {
  148. if (strpos($line, ';') === 0) {
  149. $line = trim(substr($line, 1));
  150. }
  151. // comment
  152. $lastComment .= $line . "\n";
  153. continue;
  154. }
  155. list($key, $value) = explode('=', $line, 2);
  156. $key = trim($key);
  157. if (strpos($key, '[]') === strlen($key) - 2) {
  158. $key = substr($key, 0, -2);
  159. }
  160. if (empty($descriptions[$section][$key])) {
  161. $descriptions[$section][$key] = $lastComment;
  162. }
  163. $lastComment = '';
  164. }
  165. return $descriptions;
  166. }
  167. private function splitIniContentIntoLines($ini)
  168. {
  169. if (is_string($ini)) {
  170. $ini = explode("\n", str_replace("\r", "\n", $ini));
  171. }
  172. return $ini;
  173. }
  174. /**
  175. * Reimplementation in case `parse_ini_file()` is disabled.
  176. *
  177. * @author Andrew Sohn <asohn (at) aircanopy (dot) net>
  178. * @author anthon (dot) pang (at) gmail (dot) com
  179. *
  180. * @param string $ini
  181. * @return array
  182. */
  183. private function readWithAlternativeImplementation($ini)
  184. {
  185. $ini = $this->splitIniContentIntoLines($ini);
  186. if (count($ini) == 0) {
  187. return array();
  188. }
  189. $sections = array();
  190. $values = array();
  191. $result = array();
  192. $globals = array();
  193. $i = 0;
  194. foreach ($ini as $line) {
  195. $line = trim($line);
  196. $line = str_replace("\t", " ", $line);
  197. // Comments
  198. if (!preg_match('/^[a-zA-Z0-9[]/', $line)) {
  199. continue;
  200. }
  201. // Sections
  202. if ($line{0} == '[') {
  203. $tmp = explode(']', $line);
  204. $sections[] = trim(substr($tmp[0], 1));
  205. $i++;
  206. continue;
  207. }
  208. // Key-value pair
  209. list($key, $value) = explode('=', $line, 2);
  210. $key = trim($key);
  211. $value = trim($value);
  212. if (strstr($value, ";")) {
  213. $tmp = explode(';', $value);
  214. if (count($tmp) == 2) {
  215. if ((($value{0} != '"') && ($value{0} != "'")) ||
  216. preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) ||
  217. preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value)
  218. ) {
  219. $value = $tmp[0];
  220. }
  221. } else {
  222. if ($value{0} == '"') {
  223. $value = preg_replace('/^"(.*)".*/', '$1', $value);
  224. } elseif ($value{0} == "'") {
  225. $value = preg_replace("/^'(.*)'.*/", '$1', $value);
  226. } else {
  227. $value = $tmp[0];
  228. }
  229. }
  230. }
  231. $value = trim($value);
  232. // Special keywords
  233. if ($value === 'true' || $value === 'yes' || $value === 'on') {
  234. $value = true;
  235. } elseif ($value === 'false' || $value === 'no' || $value === 'off') {
  236. $value = false;
  237. } elseif ($value === '' || $value === 'null') {
  238. $value = null;
  239. }
  240. if (is_string($value)) {
  241. $value = trim($value, "'\"");
  242. }
  243. if ($i == 0) {
  244. if (substr($key, -2) == '[]') {
  245. $globals[substr($key, 0, -2)][] = $value;
  246. } else {
  247. $globals[$key] = $value;
  248. }
  249. } else {
  250. if (substr($key, -2) == '[]') {
  251. $values[$i - 1][substr($key, 0, -2)][] = $value;
  252. } else {
  253. $values[$i - 1][$key] = $value;
  254. }
  255. }
  256. }
  257. for ($j = 0; $j < $i; $j++) {
  258. if (isset($values[$j])) {
  259. $result[$sections[$j]] = $values[$j];
  260. } else {
  261. $result[$sections[$j]] = array();
  262. }
  263. }
  264. $finalResult = $result + $globals;
  265. return $this->decode($finalResult, $finalResult);
  266. }
  267. /**
  268. * @param string $filename
  269. * @return bool|string Returns false if failure.
  270. */
  271. private function getFileContent($filename)
  272. {
  273. if (function_exists('file_get_contents')) {
  274. return file_get_contents($filename);
  275. } elseif (function_exists('file')) {
  276. $ini = file($filename);
  277. if ($ini !== false) {
  278. return implode("\n", $ini);
  279. }
  280. } elseif (function_exists('fopen') && function_exists('fread')) {
  281. $handle = fopen($filename, 'r');
  282. if (!$handle) {
  283. return false;
  284. }
  285. $ini = fread($handle, filesize($filename));
  286. fclose($handle);
  287. return $ini;
  288. }
  289. return false;
  290. }
  291. /**
  292. * We have to decode values manually because parse_ini_file() has a poor implementation.
  293. *
  294. * @param mixed $value The array decoded by `parse_ini_file`
  295. * @param mixed $rawValue The same array but with raw strings, so that we can re-decode manually
  296. * and override the poor job of `parse_ini_file`
  297. * @return mixed
  298. */
  299. private function decode($value, $rawValue)
  300. {
  301. if (is_array($value)) {
  302. foreach ($value as $i => &$subValue) {
  303. $subValue = $this->decode($subValue, $rawValue[$i]);
  304. }
  305. return $value;
  306. }
  307. if (! is_string($value)) {
  308. return $value;
  309. }
  310. $value = $this->decodeBoolean($value, $rawValue);
  311. $value = $this->decodeNull($value, $rawValue);
  312. if (is_numeric($value) && $this->noLossWhenCastToInt($value)) {
  313. return $value + 0;
  314. }
  315. return $value;
  316. }
  317. private function decodeBoolean($value, $rawValue)
  318. {
  319. if ($value === '1' && ($rawValue === 'true' || $rawValue === 'yes' || $rawValue === 'on')) {
  320. return true;
  321. }
  322. if ($value === '' && ($rawValue === 'false' || $rawValue === 'no' || $rawValue === 'off')) {
  323. return false;
  324. }
  325. return $value;
  326. }
  327. private function decodeNull($value, $rawValue)
  328. {
  329. if ($value === '' && $rawValue === 'null') {
  330. return null;
  331. }
  332. return $value;
  333. }
  334. private function noLossWhenCastToInt($value)
  335. {
  336. return (string) ($value + 0) === $value;
  337. }
  338. }