PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Lampcms/String.php

https://github.com/snytkine/LampCMS
PHP | 801 lines | 272 code | 136 blank | 393 comment | 20 complexity | cdb15535ffdbf4681213320696bcaa6d MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. *
  4. * License, TERMS and CONDITIONS
  5. *
  6. * This software is licensed under the GNU LESSER GENERAL PUBLIC LICENSE (LGPL) version 3
  7. * Please read the license here : http://www.gnu.org/licenses/lgpl-3.0.txt
  8. *
  9. * Redistribution and use in source and binary forms, with or without
  10. * modification, are permitted provided that the following conditions are met:
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. * 2. Redistributions in binary form must reproduce the above copyright
  14. * notice, this list of conditions and the following disclaimer in the
  15. * documentation and/or other materials provided with the distribution.
  16. * 3. The name of the author may not be used to endorse or promote products
  17. * derived from this software without specific prior written permission.
  18. *
  19. * ATTRIBUTION REQUIRED
  20. * 4. All web pages generated by the use of this software, or at least
  21. * the page that lists the recent questions (usually home page) must include
  22. * a link to the http://www.lampcms.com and text of the link must indicate that
  23. * the website's Questions/Answers functionality is powered by lampcms.com
  24. * An example of acceptable link would be "Powered by <a href="http://www.lampcms.com">LampCMS</a>"
  25. * The location of the link is not important, it can be in the footer of the page
  26. * but it must not be hidden by style attributes
  27. *
  28. * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
  29. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  30. * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  31. * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
  32. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  33. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  34. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  35. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  36. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  37. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  38. *
  39. * This product includes GeoLite data created by MaxMind,
  40. * available from http://www.maxmind.com/
  41. *
  42. *
  43. * @author Dmitri Snytkine <cms@lampcms.com>
  44. * @copyright 2005-2012 (or current year) Dmitri Snytkine
  45. * @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE (LGPL) version 3
  46. * @link http://www.lampcms.com Lampcms.com project
  47. * @version Release: @package_version@
  48. *
  49. *
  50. */
  51. namespace Lampcms;
  52. /**
  53. * Object representation of string
  54. *
  55. * @author Dmitri Snytkine
  56. *
  57. */
  58. class String extends LampcmsObject implements \Serializable
  59. {
  60. /**
  61. * The string that this object represents
  62. *
  63. * @var string
  64. */
  65. protected $string;
  66. /**
  67. * This mode indicates how
  68. * we return the result
  69. * when the result is a new string
  70. *
  71. * The default is 'immutable' mode
  72. * which means that a new object of this class
  73. * is created for the new string and returned
  74. *
  75. * Another option is to set it
  76. * to 'StringBuilder' which simulates the Java StringBuilder object
  77. * in which case when string changes, only the
  78. * instance of $this->string is changing (object is not immutable in
  79. * this case) and $this is returned
  80. *
  81. * The StringBuilder is more efficient because it does not
  82. * create a new object every time the string changes
  83. *
  84. * Default is 'immutable'
  85. *
  86. * @var string
  87. */
  88. protected $returnMode = 'default';
  89. /**
  90. * Constructor
  91. *
  92. * @param string $string
  93. * @param string $returnMode if set to StringBuilder will set the StringBuilder return mode
  94. *
  95. * @throws \InvalidArgumentException
  96. */
  97. public function __construct($string, $returnMode = 'default')
  98. {
  99. if (!\is_string($string) && !\is_int($string) && !\is_object($string)) {
  100. $err = '$string must be a string of int. Was: ' . gettype($string);
  101. e($err);
  102. throw new \InvalidArgumentException($err);
  103. }
  104. if (\is_object($string)) {
  105. if (!($string instanceof \Lampcms\String)) {
  106. $err = '$string must be instance of \Lampcms\String class';
  107. e($err);
  108. throw new \InvalidArgumentException($err);
  109. }
  110. $string = $string->valueOf();
  111. }
  112. d('cp');
  113. $this->string = (string)$string;
  114. $this->returnMode = $returnMode;
  115. }
  116. /**
  117. * Setter for returnMode
  118. * Should be default or StringBuilder
  119. * but basically if it's any string other
  120. * that StringBuilder, it will set the return
  121. * mode to default, making this an immutable object
  122. *
  123. * @param $mode
  124. *
  125. * @throws \InvalidArgumentException
  126. * @return \Lampcms\object $this
  127. */
  128. public function setReturnMode($mode)
  129. {
  130. if (!is_string($mode)) {
  131. throw new \InvalidArgumentException('$mode must be a string');
  132. }
  133. $this->returnMode = $mode;
  134. return $this;
  135. }
  136. /**
  137. * Getter for returnMode
  138. *
  139. * @return string
  140. */
  141. public function getReturnMode()
  142. {
  143. return $this->returnMode;
  144. }
  145. /**
  146. * Factory method but cannot call it factory
  147. * due to some bug in certain php versions
  148. * that raise error if static method signature
  149. * is not the same as in parent class
  150. *
  151. * @param string $string
  152. *
  153. * @return \Lampcms\String
  154. */
  155. public static function stringFactory($string)
  156. {
  157. $o = new \Lampcms\String($string);
  158. return $o;
  159. }
  160. /**
  161. * @todo unfinished
  162. *
  163. */
  164. public function __clone()
  165. {
  166. }
  167. /**
  168. * (non-PHPdoc)
  169. *
  170. * @see Lampcms.LampcmsObject::__toString()
  171. */
  172. public function __toString()
  173. {
  174. return $this->string;
  175. }
  176. /**
  177. *
  178. * @return string value of $this->string
  179. */
  180. public function valueOf()
  181. {
  182. return $this->string;
  183. }
  184. /**
  185. * (non-PHPdoc)
  186. *
  187. * @see Lampcms.LampcmsObject::hashCode()
  188. */
  189. public function hashCode()
  190. {
  191. return $this->getMd5();
  192. }
  193. /**
  194. * Tests to see if the string
  195. * contains html
  196. *
  197. * @return bool true if string contains html tags, false otherwise
  198. */
  199. public function isHtml()
  200. {
  201. return (\strlen(\strip_tags($this->string)) !== \strlen($this->string));
  202. }
  203. /**
  204. * (non-PHPdoc)
  205. *
  206. * @see Serializable::serialize()
  207. */
  208. public function serialize()
  209. {
  210. return \serialize(array('s' => $this->string, 'm' => $this->returnMode));
  211. }
  212. /**
  213. * (non-PHPdoc)
  214. *
  215. * @see Serializable::unserialize()
  216. */
  217. public function unserialize($serialized)
  218. {
  219. $a = \unserialize($serialized);
  220. $this->string = $a['s'];
  221. $this->returnMode = $a['m'];
  222. }
  223. /**
  224. * Returns number of lines in a string
  225. * This is utf-8 safe,
  226. * so no need to override in utf8 string class
  227. *
  228. * @return int number of lines
  229. */
  230. public function getLinesCount()
  231. {
  232. $a = \explode("\n", trim($this->string, "\n"));
  233. return count($a);
  234. }
  235. /**
  236. * Counts number of words in a string
  237. *
  238. * @internal param string $str
  239. *
  240. * @return int number of words in a string
  241. */
  242. public function getWordsCount()
  243. {
  244. return \str_word_count($this->string, 0, '0123456789-');
  245. }
  246. /**
  247. * Return number of sentences in a string
  248. * sentence is spotted by the end-of-sentence
  249. * punctuation mark: ., !, or ?
  250. * that is NOT followed by a word.
  251. *
  252. * @internal param $str
  253. *
  254. * @return int number of sentences in a string
  255. */
  256. public function getSentencesCount()
  257. {
  258. return \preg_match_all('/(?:[\w])([\.!?])(?!\w)/m', $this->string, $match);
  259. }
  260. /**
  261. * Get string length in bytes
  262. * It is important to understand that
  263. * for normal ASCII string this is also the number
  264. * of chars in string, but for utf-8 this may be
  265. * different. That's why this method would be
  266. * overridden in Utf8String class
  267. *
  268. * @return int length of string in bytes
  269. *
  270. */
  271. public function length()
  272. {
  273. return \strlen($this->string);
  274. }
  275. /**
  276. *
  277. * @return md5 hash of $this->string
  278. */
  279. public function getMd5()
  280. {
  281. return \md5($this->string);
  282. }
  283. /**
  284. *
  285. * @return crc32 value of this string
  286. */
  287. public function getCrc32()
  288. {
  289. return \crc32($this->string);
  290. }
  291. /**
  292. * Remove (mask) some chars from email address
  293. * so that it becomes not valid for email harvesters
  294. * and can be displayed on the web page
  295. *
  296. * @param object of this type
  297. * @return object
  298. */
  299. public function obfuscateEmail()
  300. {
  301. $str = \preg_replace('/([a-zA-Z0-9_\.]{2,})(@)/Ume', "substr('\\1', 0, rand((floor(strlen('\\1') / 2)), (floor(strlen('\\1') / 2) + 1))).'### @'", $this->string);
  302. return $this->handleReturn($str);
  303. }
  304. /**
  305. * Strip tags but preserve white spaces
  306. *
  307. * @return object of this class representing
  308. * new string
  309. */
  310. public function asPlainText()
  311. {
  312. if (!$this->isHtml()) {
  313. return $this->handleReturn($this->string);
  314. }
  315. /**
  316. * Remove all the < brackets with space
  317. * so that when tags are stripped we will
  318. * not lose any spaces
  319. */
  320. $text = \str_replace('<', ' <', $this->string);
  321. $text = \strip_tags($text);
  322. $text = \preg_replace('/[\n\r\t]+/', ' ', $text);
  323. $text = \preg_replace('!\s+!', ' ', $text);
  324. return $this->handleReturn(\trim($text));
  325. }
  326. public function stripTags(array $aAllowed = null)
  327. {
  328. $ret = \strip_tags($this->string, $aAllowed);
  329. return $this->handleReturn($ret);
  330. }
  331. /**
  332. * Generates random alphanumeric string
  333. * of predetermined length
  334. *
  335. * @param int|\Lampcms\intered $len
  336. *
  337. * @return string a string of random letters and numbers.
  338. */
  339. public static function makeRandomString($len = 30)
  340. {
  341. $strAlphanum = 'abcdefghijklmnopqrstuvwqyz0123456789';
  342. $len = (int)$len;
  343. $aAlphanum = \str_split($strAlphanum);
  344. $strRes = '';
  345. for ($i = 0; $i < $len; $i += 1) {
  346. $key = \mt_rand(0, 35);
  347. $char = $aAlphanum[$key];
  348. $strRes .= (1 === \mt_rand(0, 1) && !\is_numeric($char)) ? \strtoupper($char) : $char;
  349. }
  350. return $strRes;
  351. }
  352. /**
  353. * Make random string that will be used
  354. * as value of 'sid' cookie
  355. * The value is generated based on current microtime
  356. * so the string always starts with current microtime,
  357. * then the letter a and remainder is a random string
  358. * with the length necessary to make the total length
  359. * equal to $len param
  360. *
  361. * @param int $len the total length of generated sid
  362. *
  363. * @return string random string of $len chars
  364. * the string always starts with microtime value
  365. * then char 'a' and then random string
  366. * This way we can always extract the microtime
  367. * from it and find out when user first got this cookie
  368. * meaning we don't even need to set a separate 'first visit'
  369. * cookie!
  370. *
  371. */
  372. public static function makeSid($len = 48)
  373. {
  374. $prefix = \microtime(true) . 'a';
  375. $rs = self::makeRandomString($len - \strlen($prefix));
  376. return $prefix . $rs;
  377. }
  378. /**
  379. * Returns sha256 hashed password
  380. * using LAMPCMS_SALT from settings as salt
  381. *
  382. * @param string $pwd password to hash
  383. *
  384. * @return string md5 hash of VERSION + $pwd
  385. */
  386. public static function hashPassword($pwd)
  387. {
  388. $salt = LAMPCMS_SALT;
  389. return \hash('sha256', $salt . $pwd);
  390. }
  391. /**
  392. * Create random number on 6 to 8 digits long
  393. * This is useful to generate initial password
  394. * for a new user
  395. *
  396. * This method will also make sure that password
  397. * has at least one number character in it in order
  398. * to pass password validation which requires the password
  399. * to have letters and numbers
  400. *
  401. * @param int $minLen
  402. * @param int $maxLen
  403. *
  404. * @return string a randomly generate password
  405. */
  406. public static function makePasswd($minLen = 6, $maxLen = 8)
  407. {
  408. $len = \mt_rand($minLen, $maxLen);
  409. d('len: ' . $len);
  410. $pwd = self::makeRandomString($len);
  411. /**
  412. * if result string does not have at least one number
  413. * then append one digit to the end of string
  414. * this is so that the password will pass
  415. * the validator during login process,
  416. * which requires the password to have at least one digit
  417. */
  418. if (0 === \preg_match('/\d/', $pwd)) {
  419. $digit = \mt_rand(0, 9);
  420. $pwd .= $digit;
  421. /**
  422. * Now that we added an extra char
  423. * to end of string, we must remove
  424. * the first char to keep the string to be
  425. * not over the maxLen value
  426. *
  427. * But only if it exceeded $maxLen
  428. */
  429. if (strlen($pwd) > $maxLen) {
  430. $pwd = \substr($pwd, 1);
  431. }
  432. }
  433. return $pwd;
  434. }
  435. /**
  436. * Wraps the string inside this html (or xml) tag
  437. *
  438. * @param string $tag defaults to <pre>
  439. * MUST specify tag without the < > brackets, just a tag name
  440. * for example 'div'
  441. *
  442. * @return object of this class (new object or $this)
  443. */
  444. public function wrapInTag($tag = 'pre')
  445. {
  446. $string = '<' . $tag . '>' . $this->string . '</' . $tag . '>';
  447. return $this->handleReturn($string);
  448. }
  449. /**
  450. * Handles the return function
  451. * depending on returnMode it will either
  452. * modify the value of $this->string and return $this
  453. * OR will create a new object of this class for the
  454. * new string and return that new object
  455. * Either way an object of this class is returned
  456. *
  457. * @param $string
  458. *
  459. * @return object of this class
  460. */
  461. protected function handleReturn($string)
  462. {
  463. if ('StringBuilder' !== $this->returnMode) {
  464. $o = new static($string, $this->returnMode);
  465. return $o;
  466. }
  467. $this->string = $string;
  468. return $this;
  469. }
  470. /**
  471. * A simpler implementation of linkify
  472. * does not do truncating of link text
  473. * but seems to work better for certain links
  474. *
  475. * @important DO NOT use on HTML String!
  476. * for html string use HTMLStringParser::linkify()
  477. *
  478. * @return object of this class
  479. */
  480. public function linkify()
  481. {
  482. if ($this->isHtml()) {
  483. e('not cool to linkify this string because it is an HTML string Use \Lampcms\String\HTMLStringParser::linkify() for HTML strings');
  484. }
  485. $text = $this->string;
  486. $text = \preg_replace("/(^|[\n ])([\w]*?)((ht|f)tp(s)?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is", "$1$2<a href=\"$3\" rel=\"nofollow\">$3</a>", $text);
  487. $text = \preg_replace("/(^|[\n ])([\w]*?)((www|ftp)\.[^ \,\"\t\n\r<]*)/is", "$1$2<a href=\"http://$3\" rel=\"nofollow\">$3</a>", $text);
  488. return $this->handleReturn($text);
  489. }
  490. /**
  491. * Cut string of text to be not more
  492. * than $max chars but makes sure it cuts
  493. * only on word boundary
  494. *
  495. * @param int $max
  496. * @param string $link
  497. *
  498. * @return object of this class
  499. */
  500. public function truncate($max, $link = '')
  501. {
  502. $words = \preg_split("/[\s]+/", $this->string);
  503. $newstring = '';
  504. $numwords = 0;
  505. foreach ($words as $word) {
  506. if ((\strlen($newstring) + 1 + \strlen($word)) < $max) {
  507. $newstring .= ' ' . $word;
  508. ++$numwords;
  509. } else {
  510. break;
  511. }
  512. }
  513. if ($numwords < count($words)) {
  514. $newstring .= '... ' . $link;
  515. }
  516. return $this->handleReturn(\trim($newstring));
  517. }
  518. /**
  519. * Change the & to $amp;
  520. * but only if the & is not part of already encoded
  521. * html entity, including &amp;
  522. * This is most likely utf8 safe because the pattern
  523. * only contains valid ascii chars
  524. * and just in case, we also use the /u switch with
  525. * will make the replace fail if input contains invalid utf8 chars
  526. *
  527. * @return object of this class
  528. */
  529. public function escapeAmp()
  530. {
  531. $newstring = \preg_replace('/&(?!([#]{0,1})([a-zA-Z0-9]{2,9});)/u', '&amp;', $this->string);
  532. return $this->handleReturn($newstring);
  533. }
  534. /**
  535. * Replace non alphanumerics with underscores,
  536. * limit to 65 chars
  537. *
  538. * @param int $limit
  539. *
  540. * @return object of this class
  541. */
  542. public function makeLinkTitle($limit = 65)
  543. {
  544. /**
  545. * Remove 'a', 'the', 'an', 'i', 'you', 'we', 'it', 'is', 'are'
  546. */
  547. $aFiltered = array(
  548. '/ a /i',
  549. '/ the /i',
  550. '/ an /i',
  551. '/ i /i',
  552. '/ am /i',
  553. '/ you /i',
  554. '/ we /i',
  555. '/ it /i',
  556. '/ is /i',
  557. '/ are /i',
  558. '/ of /i',
  559. '/ i\'m /i',
  560. '/ it\'s /i',
  561. '/ of /i',
  562. '/ of /i',
  563. '/ my /i'
  564. );
  565. $str = \preg_replace($aFiltered, ' ', $this->string);
  566. /**
  567. * All non-alpha numeric chars will become dashes
  568. */
  569. $str = \preg_replace('/([^a-zA-Z0-9\-_])/', '-', $str);
  570. $str = \preg_replace('/(-){2,}/', '-', $str);
  571. /**
  572. * Remove the Re: type prefix
  573. * because it's not adding any value to
  574. * SEO-friendly string
  575. */
  576. $str = \preg_replace('/^re-/i', '', $str);
  577. /**
  578. * Replace anything that looks like -_ or _- with
  579. * just an underscore
  580. */
  581. $str = \str_replace(array('-_', '_-'), '_', $str);
  582. /**
  583. * If for some reason the string just became
  584. * an empty string
  585. * like maybe all chars were non-alpha numeric, so
  586. * there we all removed, then we don't want
  587. * to have an empty string as subject, we'll
  588. * just say 'topic'
  589. */
  590. if (empty($str) || ('-' === $str) || ('_' === $str)) {
  591. $str = 'Question';
  592. }
  593. /**
  594. * At this point of result string
  595. * is shorter than the limit, no further
  596. * processing is necessary, just return it
  597. */
  598. if (strlen($str) <= $limit) {
  599. $str = \trim($str, ' _-');
  600. return $this->handleReturn($str);
  601. }
  602. /**
  603. * Find the right-most occurrence of dash
  604. */
  605. $lastPos = \strrpos($str, '-', ($limit - strlen($str)));
  606. /**
  607. * If last occurrence of dash not found,
  608. * then we will cut off the string at the $limit length
  609. * This is a rare case when a string did not
  610. * have any non alphanumeric chars - like when
  611. * it was a continues string of over 100 chars
  612. */
  613. $lastPos = (false !== $lastPos) ? $lastPos : $limit;
  614. $ret = \substr($str, 0, $lastPos);
  615. return $this->handleReturn($ret);
  616. }
  617. /**
  618. * Prepare email for more comfortable type
  619. *
  620. * @param string $strAddress email address
  621. *
  622. * @param string $strFirstName first name
  623. * @param string $strLastName last name
  624. *
  625. * @return string email address string complete with first name, last name and email address
  626. */
  627. public static function prepareEmail($strAddress, $strFirstName = '', $strLastName = '')
  628. {
  629. $fn_ln = \trim(\trim($strFirstName) . ' ' . \trim($strLastName));
  630. $filtered = \htmlspecialchars($fn_ln);
  631. $name = ('' !== $fn_ln) ? '"' . $filtered . '"' : '';
  632. $recipient = ('' !== $name) ? $name . ' <' . $strAddress . '>' : $strAddress;
  633. return $recipient;
  634. }
  635. public function toLowerCase()
  636. {
  637. $s = \strtolower($this->string);
  638. return $this->handleReturn($s);
  639. }
  640. public function toUpperCase()
  641. {
  642. $s = \strtoupper($this->string);
  643. return $this->handleReturn($s);
  644. }
  645. public function isEmpty()
  646. {
  647. return (0 === $this->length());
  648. }
  649. public function substr($start, $len = null)
  650. {
  651. $s = \substr($this->string, $start, $len);
  652. return $this->handleReturn($s);
  653. }
  654. /**
  655. * Apply trim() to this string with no
  656. * extra params passed to trim, which is UTF-8 safe
  657. *
  658. * @return object of this class
  659. */
  660. public function trim()
  661. {
  662. $s = \trim($this->string);
  663. return $this->handleReturn($s);
  664. }
  665. }