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

/lib/Jelix/IniFile/Modifier.php

https://github.com/gmarrot/jelix
PHP | 713 lines | 496 code | 61 blank | 156 comment | 201 complexity | 1a5c7f80da1daf1610d2630dac2481bd MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * @author Laurent Jouanneau
  4. * @copyright 2008-2014 Laurent Jouanneau
  5. * @link http://jelix.org
  6. * @licence http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public Licence, see LICENCE file
  7. */
  8. namespace Jelix\IniFile;
  9. /**
  10. * utility class to modify an ini file by preserving comments, whitespace..
  11. * It follows same behaviors of parse_ini_file, except when there are quotes
  12. * inside values. it doesn't support quotes inside values, because parse_ini_file
  13. * is totally bugged, depending cases.
  14. */
  15. class Modifier {
  16. /**
  17. * @const integer token type for whitespaces
  18. */
  19. const TK_WS = 0;
  20. /**
  21. * @const integer token type for a comment
  22. */
  23. const TK_COMMENT = 1;
  24. /**
  25. * @const integer token type for a section header
  26. */
  27. const TK_SECTION = 2;
  28. /**
  29. * @const integer token type for a simple value
  30. */
  31. const TK_VALUE = 3;
  32. /**
  33. * @const integer token type for a value of an array item
  34. */
  35. const TK_ARR_VALUE = 4;
  36. /**
  37. * each item of this array contains data for a section. the key of the item
  38. * is the section name. There is a section with the key "0", and which contains
  39. * data for options which are not in a section.
  40. * each value of the items is an array of tokens. A token is an array with
  41. * some values. first value is the token type (see TK_* constants), and other
  42. * values depends of the token type:
  43. * - TK_WS: content of whitespaces
  44. * - TK_COMMENT: the comment
  45. * - TK_SECTION: the section name
  46. * - TK_VALUE: the name, and the value
  47. * - TK_ARRAY_VALUE: the name, the value, and the key
  48. * @var array
  49. */
  50. protected $content = array();
  51. /**
  52. * @var string the filename of the ini file
  53. */
  54. protected $filename = '';
  55. /**
  56. * @var boolean true if the content has been modified
  57. */
  58. protected $modified = false;
  59. /**
  60. * load the given ini file
  61. * @param string $filename the file to load
  62. */
  63. function __construct($filename) {
  64. if (!file_exists($filename) || !is_file($filename))
  65. // because the class is used also by installers, we don't have any
  66. // modules in this case, so impossible to use jException
  67. throw new \Exception ('(23)The file '.$filename.' doesn\'t exist', 23);
  68. $this->filename = $filename;
  69. $this->parse(preg_split("/(\r\n|\n|\r)/", file_get_contents($filename)));
  70. }
  71. /**
  72. * @return string the file name
  73. */
  74. function getFileName() {
  75. return $this->filename;
  76. }
  77. /**
  78. * parsed the lines of the ini file
  79. */
  80. protected function parse($lines) {
  81. $this->content = array(0=>array());
  82. $currentSection = 0;
  83. $multiline = false;
  84. $currentValue = null;
  85. $arrayContents = array();
  86. foreach ($lines as $num => $line) {
  87. if ($multiline) {
  88. if (preg_match('/^(.*)"\s*$/', $line, $m)) {
  89. $currentValue[2] .= $m[1];
  90. $multiline = false;
  91. $this->content[$currentSection][] = $currentValue;
  92. }
  93. else {
  94. $currentValue[2] .= $line."\n";
  95. }
  96. }
  97. else if (preg_match('/^\s*([a-z0-9_.-]+)(\[\])?\s*=\s*(")?([^"]*)(")?(\s*)/i', $line, $m)) {
  98. list($all, $name, $foundkey, $firstquote, $value ,$secondquote,$lastspace) = $m;
  99. if ($foundkey !='') {
  100. if (isset($arrayContents[$currentSection][$name])) {
  101. $key = count($arrayContents[$currentSection][$name]);
  102. }
  103. else {
  104. $key = 0;
  105. }
  106. $currentValue = array(self::TK_ARR_VALUE, $name, $value, $key);
  107. $arrayContents[$currentSection][$name][$key] = $value;
  108. }
  109. else {
  110. $currentValue = array(self::TK_VALUE, $name, $value);
  111. }
  112. if ($firstquote == '"' && $secondquote == '') {
  113. $multiline = true;
  114. $currentValue[2].="\n";
  115. }
  116. else {
  117. if ($firstquote == '' && $secondquote == '') {
  118. $currentValue[2] = trim($value);
  119. }
  120. $this->content[$currentSection][] = $currentValue;
  121. }
  122. }
  123. else if (preg_match('/^(\s*;.*)$/',$line, $m)) {
  124. $this->content[$currentSection][] = array(self::TK_COMMENT, $m[1]);
  125. }
  126. else if (preg_match('/^(\s*\[([a-z0-9_.\-@:]+)\]\s*)/i', $line, $m)) {
  127. $currentSection = $m[2];
  128. $this->content[$currentSection] = array(
  129. array(self::TK_SECTION, $m[1]),
  130. );
  131. }
  132. else {
  133. $this->content[$currentSection][] = array(self::TK_WS, $line);
  134. }
  135. }
  136. }
  137. /**
  138. * modify an option in the ini file. If the option doesn't exist,
  139. * it is created.
  140. * @param string $name the name of the option to modify
  141. * @param string $value the new value
  142. * @param string $section the section where to set the item. 0 is the global section
  143. * @param integer $key for option which is an item of array, the key in the array. '' to just add a value in the array
  144. */
  145. public function setValue($name, $value, $section=0, $key=null) {
  146. $foundValue=false;
  147. $lastKey = -1; // last key in an array value
  148. if (isset($this->content[$section])) {
  149. // boolean to erase array values if the new value is not a new item for the array
  150. $deleteMode = false;
  151. foreach ($this->content[$section] as $k =>$item) {
  152. if ($deleteMode) {
  153. if ($item[0] == self::TK_ARR_VALUE && $item[1] == $name) {
  154. $this->content[$section][$k] = array(self::TK_WS, '--');
  155. }
  156. continue;
  157. }
  158. // if the item is not a value or an array value, or not the same name
  159. if (($item[0] != self::TK_VALUE && $item[0] != self::TK_ARR_VALUE)
  160. || $item[1] != $name) {
  161. continue;
  162. }
  163. // if it is an array value, and if the key doesn't correspond
  164. if ($item[0] == self::TK_ARR_VALUE && $key !== null) {
  165. if ($item[3] != $key || $key === '') {
  166. $lastKey = $item[3];
  167. continue;
  168. }
  169. }
  170. if ($key !== null) {
  171. // we add the value as an array value
  172. if ($key === '')
  173. $key = 0;
  174. $this->content[$section][$k] = array(self::TK_ARR_VALUE, $name,$value, $key);
  175. }
  176. else {
  177. // we store the value
  178. $this->content[$section][$k] = array(self::TK_VALUE, $name, $value);
  179. if ($item[0] == self::TK_ARR_VALUE) {
  180. // the previous value was an array value, so we erase other array values
  181. $deleteMode = true;
  182. $foundValue = true;
  183. continue;
  184. }
  185. }
  186. $foundValue=true;
  187. break;
  188. }
  189. }
  190. else {
  191. $this->content[$section] = array(array(self::TK_SECTION, '['.$section.']'));
  192. }
  193. if (!$foundValue) {
  194. if ($key === null) {
  195. $this->content[$section][]= array(self::TK_VALUE, $name, $value);
  196. }
  197. else {
  198. if ($lastKey != -1) {
  199. $lastKey++;
  200. }
  201. else {
  202. $lastKey = 0;
  203. }
  204. $this->content[$section][]= array(self::TK_ARR_VALUE, $name, $value, $lastKey);
  205. }
  206. }
  207. $this->modified = true;
  208. }
  209. /**
  210. * modify several options in the ini file.
  211. * @param array $value associated array with key=>value
  212. * @param string $section the section where to set the item. 0 is the global section
  213. */
  214. public function setValues($values, $section=0) {
  215. foreach ($values as $name=>$val) {
  216. if (is_array($val)) {
  217. // let's ignore key values, we don't want them
  218. $i = 0;
  219. foreach ($val as $arval) {
  220. $this->setValue($name, $arval, $section, $i++);
  221. }
  222. }
  223. else {
  224. $this->setValue($name, $val, $section);
  225. }
  226. }
  227. }
  228. /**
  229. * remove an option in the ini file. It can remove an entire section if you give
  230. * an empty value as $name, and a $section name
  231. * @param string $name the name of the option to remove, or null to remove an entire section
  232. * @param string $section the section where to remove the value, or the section to remove
  233. * @param integer $key for option which is an item of array, the key in the array
  234. * @since 1.2
  235. */
  236. public function removeValue($name, $section=0, $key=null, $removePreviousComment = true) {
  237. $foundValue=false;
  238. if ($section === 0 && $name == '')
  239. return;
  240. if ($name == '') {
  241. if ($section === 0 || !isset($this->content[$section]))
  242. return;
  243. if ($removePreviousComment) {
  244. // retrieve the previous section
  245. $previousSection = -1;
  246. foreach($this->content as $s=>$c) {
  247. if ($s === $section) {
  248. break;
  249. }
  250. else {
  251. $previousSection = $s;
  252. }
  253. }
  254. if ($previousSection != -1) {
  255. //retrieve the last comment
  256. $s = $this->content[$previousSection];
  257. end($s);
  258. $tok = current($s);
  259. while($tok !== false) {
  260. if ($tok[0] != self::TK_WS && $tok[0] != self::TK_COMMENT) {
  261. break;
  262. }
  263. if ($tok[0] == self::TK_COMMENT && strpos($tok[1], '<?') === false) {
  264. $this->content[$previousSection][key($s)] = array(self::TK_WS, '--');
  265. }
  266. $tok = prev($s);
  267. }
  268. }
  269. }
  270. unset($this->content[$section]);
  271. $this->modified = true;
  272. return;
  273. }
  274. if (isset($this->content[$section])) {
  275. // boolean to erase array values if the option to remove is an array
  276. $deleteMode = false;
  277. $previousComment = array();
  278. foreach ($this->content[$section] as $k =>$item) {
  279. if ($deleteMode) {
  280. if ($item[0] == self::TK_ARR_VALUE && $item[1] == $name)
  281. $this->content[$section][$k] = array(self::TK_WS, '--');
  282. continue;
  283. }
  284. if ($item[0] == self::TK_COMMENT) {
  285. if ($removePreviousComment)
  286. $previousComment[] = $k;
  287. continue;
  288. }
  289. if ($item[0] == self::TK_WS) {
  290. if ($removePreviousComment)
  291. $previousComment[] = $k;
  292. continue;
  293. }
  294. // if the item is not a value or an array value, or not the same name
  295. if ($item[1] != $name) {
  296. $previousComment = array();
  297. continue;
  298. }
  299. // if it is an array value, and if the key doesn't correspond
  300. if ($item[0] == self::TK_ARR_VALUE && $key !== null) {
  301. if($item[3] != $key) {
  302. $previousComment = array();
  303. continue;
  304. }
  305. }
  306. if (count($previousComment)) {
  307. $kc = array_pop($previousComment);
  308. while ($kc !== null && $this->content[$section][$kc][0] == self::TK_WS) {
  309. $kc = array_pop($previousComment);
  310. }
  311. while ($kc !== null && $this->content[$section][$kc][0] == self::TK_COMMENT) {
  312. if(strpos($this->content[$section][$kc][1], "<?") === false) {
  313. $this->content[$section][$kc] = array(self::TK_WS, '--');
  314. }
  315. $kc = array_pop($previousComment);
  316. }
  317. }
  318. if ($key !== null) {
  319. // we remove the value from the array
  320. $this->content[$section][$k] = array(self::TK_WS, '--');
  321. } else {
  322. // we remove the value
  323. $this->content[$section][$k] = array(self::TK_WS, '--');
  324. if ($item[0] == self::TK_ARR_VALUE) {
  325. // the previous value was an array value, so we erase other array values
  326. $deleteMode = true;
  327. $foundValue = true;
  328. continue;
  329. }
  330. }
  331. $foundValue=true;
  332. break;
  333. }
  334. }
  335. $this->modified = true;
  336. }
  337. /**
  338. * return the value of an option in the ini file. If the option doesn't exist,
  339. * it returns null.
  340. * @param string $name the name of the option to retrieve
  341. * @param string $section the section where the option is. 0 is the global section
  342. * @param integer $key for option which is an item of array, the key in the array
  343. * @return mixed the value
  344. */
  345. public function getValue($name, $section=0, $key=null) {
  346. if(!isset($this->content[$section])) {
  347. return null;
  348. }
  349. $arrayValue = array();
  350. $isArray = false;
  351. foreach ($this->content[$section] as $k =>$item) {
  352. if (($item[0] != self::TK_VALUE && $item[0] != self::TK_ARR_VALUE)
  353. || $item[1] != $name)
  354. continue;
  355. if ($item[0] == self::TK_ARR_VALUE) {
  356. if ($key !== null) {
  357. if($item[3] != $key)
  358. continue;
  359. }
  360. else {
  361. $isArray = true;
  362. $arrayValue[] = $item[2];
  363. continue;
  364. }
  365. }
  366. if (preg_match('/^-?[0-9]$/', $item[2])) {
  367. return intval($item[2]);
  368. }
  369. else if (preg_match('/^-?[0-9\.]$/', $item[2])) {
  370. return floatval($item[2]);
  371. }
  372. else if (strtolower($item[2]) === 'true' || strtolower($item[2]) === 'on') {
  373. return true;
  374. }
  375. else if (strtolower($item[2]) === 'false' || strtolower($item[2]) === 'off') {
  376. return false;
  377. }
  378. return $item[2];
  379. }
  380. if ($isArray)
  381. return $arrayValue;
  382. return null;
  383. }
  384. /**
  385. * return all values of a section in the ini file.
  386. * @param string $section the section from wich we want values. 0 is the global section
  387. * @return array the list of values, $key=>$value
  388. */
  389. public function getValues($section=0) {
  390. if(!isset($this->content[$section])) {
  391. return array();
  392. }
  393. $values = array();
  394. foreach ($this->content[$section] as $k =>$item) {
  395. if ($item[0] != self::TK_VALUE && $item[0] != self::TK_ARR_VALUE)
  396. continue;
  397. if (preg_match('/^-?[0-9]$/', $item[2])) {
  398. $val = intval($item[2]);
  399. }
  400. else if (preg_match('/^-?[0-9\.]$/', $item[2])) {
  401. $val = floatval($item[2]);
  402. }
  403. else if (strtolower($item[2]) === 'true' || strtolower($item[2]) === 'on') {
  404. $val = true;
  405. }
  406. else if (strtolower($item[2]) === 'false' || strtolower($item[2]) === 'off') {
  407. $val = false;
  408. }
  409. else
  410. $val = $item[2];
  411. if ($item[0] == self::TK_VALUE) {
  412. $values[$item[1]] = $val;
  413. }
  414. else {
  415. $values[$item[1]][$item[3]] = $val;
  416. }
  417. }
  418. return $values;
  419. }
  420. /**
  421. * save the ini file
  422. */
  423. public function save() {
  424. if ($this->modified) {
  425. if (false === @file_put_contents($this->filename, $this->generateIni()))
  426. throw new \Exception("Impossible to write into ".$this->filename);
  427. $this->modified = false;
  428. }
  429. }
  430. /**
  431. * save the content in an new ini file
  432. * @param string $filename the name of the file
  433. */
  434. public function saveAs($filename) {
  435. file_put_contents($filename, $this->generateIni());
  436. }
  437. /**
  438. * says if the ini content has been modified
  439. * @return boolean
  440. * @since 1.2
  441. */
  442. public function isModified() {
  443. return $this->modified;
  444. }
  445. /**
  446. * says if there is a section with the given name
  447. * @since 1.2
  448. */
  449. public function isSection($name) {
  450. return isset($this->content[$name]);
  451. }
  452. /**
  453. * return the list of section names
  454. * @return array
  455. * @since 1.2
  456. */
  457. public function getSectionList() {
  458. $list = array_keys($this->content);
  459. array_shift($list); // remove the global section
  460. return $list;
  461. }
  462. protected function generateIni() {
  463. $content = '';
  464. foreach($this->content as $sectionname=>$section) {
  465. foreach($section as $item) {
  466. switch($item[0]) {
  467. case self::TK_SECTION:
  468. if($item[1] != '0')
  469. $content.=$item[1]."\n";
  470. break;
  471. case self::TK_WS:
  472. if ($item[1]=='--')
  473. break;
  474. case self::TK_COMMENT:
  475. $content.=$item[1]."\n";
  476. break;
  477. case self::TK_VALUE:
  478. $content.=$item[1].'='.$this->getIniValue($item[2])."\n";
  479. break;
  480. case self::TK_ARR_VALUE:
  481. $content.=$item[1].'[]='.$this->getIniValue($item[2])."\n";
  482. break;
  483. }
  484. }
  485. }
  486. return $content;
  487. }
  488. protected function getIniValue($value) {
  489. if ($value === '' || is_numeric(trim($value)) || (preg_match("/^[\w-.]*$/", $value) && strpos("\n",$value) === false) ) {
  490. return $value;
  491. }else if($value === false) {
  492. $value="0";
  493. }else if($value === true) {
  494. $value="1";
  495. }else {
  496. $value='"'.$value.'"';
  497. }
  498. return $value;
  499. }
  500. /**
  501. * import values of an ini file into the current ini content.
  502. * If a section prefix is given, all section of the given ini file will be
  503. * renamed with the prefix plus "_". The global (unamed) section will be the section
  504. * named with the value of prefix. If the section prefix is not given, the existing
  505. * sections and given section with the same name will be merged.
  506. * @param Jelix\IniFile\Modifier $ini an ini file modifier to merge with the current
  507. * @param string $sectionPrefix the prefix to add to the section prefix
  508. * @param string $separator the separator to add between the prefix and the old name
  509. * of the section
  510. * @since 1.2
  511. */
  512. public function import(Modifier $ini, $sectionPrefix = '', $separator = '_') {
  513. foreach($ini->content as $section=>$values) {
  514. if ($sectionPrefix) {
  515. if ($section == "0") {
  516. $realSection = $sectionPrefix;
  517. }
  518. else {
  519. $realSection = $sectionPrefix.$separator.$section;
  520. }
  521. }
  522. else $realSection = $section;
  523. if (isset($this->content[$realSection])) {
  524. // let's merge the current and the given section
  525. $this->mergeValues($values, $realSection);
  526. }
  527. else {
  528. if ($values[0][0] == self::TK_SECTION)
  529. $values[0][1] = '['.$realSection.']';
  530. else {
  531. array_unshift($values, array(self::TK_SECTION, '['.$realSection.']'));
  532. }
  533. $this->content[$realSection] = $values;
  534. $this->modified = true;
  535. }
  536. }
  537. }
  538. /**
  539. * move values of a section into an other section and remove the section
  540. * @return boolean true if the merge is a success
  541. */
  542. public function mergeSection($sectionSource, $sectionTarget) {
  543. if (!isset($this->content[$sectionTarget]))
  544. return $this->renameSection($sectionSource, $sectionTarget);
  545. if (!isset($this->content[$sectionSource]))
  546. return false;
  547. $this->mergeValues($this->content[$sectionSource], $sectionTarget);
  548. if ($sectionSource == "0")
  549. $this->content[$sectionSource] = array();
  550. else
  551. unset($this->content[$sectionSource]);
  552. $this->modified = true;
  553. return true;
  554. }
  555. protected function mergeValues($values, $sectionTarget) {
  556. $previousItems = array();
  557. // if options already exists, just change their values.
  558. // if options don't exist, add them to the section, with
  559. // comments and whitespace
  560. foreach ($values as $k=>$item) {
  561. switch($item[0]) {
  562. case self::TK_SECTION:
  563. break;
  564. case self::TK_WS:
  565. if ($item[1]=='--')
  566. break;
  567. case self::TK_COMMENT:
  568. $previousItems [] = $item;
  569. break;
  570. case self::TK_VALUE:
  571. case self::TK_ARR_VALUE:
  572. $found = false;
  573. $lastNonValues = -1;
  574. foreach ($this->content[$sectionTarget] as $j =>$item2) {
  575. if ($item2[0] != self::TK_VALUE && $item2[0] != self::TK_ARR_VALUE) {
  576. if ($lastNonValues == -1 && $item2[0] != self::TK_SECTION)
  577. $lastNonValues = $j;
  578. continue;
  579. }
  580. if ($item2[1] != $item[1]) {
  581. $lastNonValues = -1;
  582. continue;
  583. }
  584. $found = true;
  585. $this->content[$sectionTarget][$j][2] = $item[2];
  586. $this->modified = true;
  587. break;
  588. }
  589. if (!$found) {
  590. $atTheEnd = false;
  591. $previousItems[] = $item;
  592. if ($lastNonValues > 0) {
  593. $previousItems = array_splice($this->content[$sectionTarget], $lastNonValues, $j, $previousItems);
  594. }
  595. $this->content[$sectionTarget] = array_merge($this->content[$sectionTarget], $previousItems);
  596. $this->modified = true;
  597. }
  598. $previousItems = array();
  599. break;
  600. }
  601. }
  602. }
  603. /**
  604. * rename a value
  605. *
  606. */
  607. public function renameValue($name, $newName, $section=0) {
  608. if (!isset($this->content[$section]))
  609. return false;
  610. foreach ($this->content[$section] as $k =>$item) {
  611. if ($item[0] != self::TK_VALUE && $item[0] != self::TK_ARR_VALUE) {
  612. continue;
  613. }
  614. if ($item[1] != $name) {
  615. continue;
  616. }
  617. $this->content[$section][$k][1] = $newName;
  618. $this->modified = true;
  619. break;
  620. }
  621. return true;
  622. }
  623. /**
  624. * rename a section
  625. */
  626. public function renameSection($oldName, $newName) {
  627. if (!isset($this->content[$oldName]))
  628. return false;
  629. if (isset($this->content[$newName])) {
  630. return $this->mergeSection($oldName, $newName);
  631. }
  632. $newcontent = array();
  633. foreach($this->content as $section=>$values) {
  634. if ((string)$oldName == (string)$section) {
  635. if ($section == "0") {
  636. $newcontent[0] = array();
  637. }
  638. if ($values[0][0] == self::TK_SECTION)
  639. $values[0][1] = '['.$newName.']';
  640. else {
  641. array_unshift($values, array(self::TK_SECTION, '['.$newName.']'));
  642. }
  643. $newcontent[$newName] = $values;
  644. }
  645. else
  646. $newcontent [$section] = $values;
  647. }
  648. $this->content = $newcontent;
  649. $this->modified = true;
  650. return true;
  651. }
  652. }