PageRenderTime 36ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Lampcms/Forms/Form.php

http://github.com/snytkine/LampCMS
PHP | 816 lines | 271 code | 136 blank | 409 comment | 33 complexity | 68cae925986b1bd097a70d33e60cec11 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\Forms;
  52. use Lampcms\Request;
  53. use Lampcms\Registry;
  54. use Lampcms\LampcmsObject;
  55. /**
  56. * This is base class for various web forms
  57. * It is responsible for rendering a form
  58. * using a tpl... template file, possibly
  59. * setting error message in the form,
  60. * It may also translate element titles and captions
  61. * as well as error messages.
  62. *
  63. * @author Dmitri Snytkine
  64. *
  65. */
  66. class Form extends LampcmsObject
  67. {
  68. /**
  69. * Use CSRF token
  70. * by default it will set value of token
  71. * in new form and will automatically
  72. * validate value of submitted token
  73. * if form is submitted (Request was POST)
  74. *
  75. * @var bool
  76. */
  77. protected $useToken = true;
  78. /**
  79. * Array of field names used in current form
  80. * This should be set in sub-class that represents
  81. * concrete form
  82. *
  83. * This is optional and if set, then
  84. * keys are field names, values are... not sure yet,
  85. * could be objects or arrays containing validator
  86. * callback functions
  87. *
  88. * It is helpful to know which field names we have
  89. * or going to have in the form.
  90. *
  91. * First it can help if we need to pre-populate
  92. * field values in case of error during validation,
  93. * we can just get already submitted values from Request
  94. *
  95. * @var array
  96. */
  97. protected $aFields = array();
  98. /**
  99. * Array of validator callback functions
  100. * keys are field names, values are anonymous functions
  101. * that take field name as param
  102. *
  103. * @var array
  104. */
  105. protected $aValidators = array();
  106. /**
  107. * Name of form template file
  108. * The name of actual template should be
  109. * set in sub-class
  110. *
  111. * Templates must have these placeholders:
  112. * filedName, fieldName_e for setting
  113. * error specific to form field
  114. *
  115. * and also 'formError'
  116. * with corresponding html in template: <div class="form_error">%%</div>
  117. * for setting form error
  118. * via javascript %% should correspond to 'formError'
  119. * position in vars array, for example %19$s
  120. *
  121. *
  122. * @var string
  123. */
  124. protected $template;
  125. /**
  126. * Array of template vars
  127. * This is usually a copy from template
  128. * we get it via tplXXXX::getVars()
  129. * then we can work with it like
  130. * translating field values,
  131. * pre-populating fields if form has already been
  132. * submitted but contains errors in which case we want
  133. * to show user errors but also preserve already
  134. * submitted data in the form
  135. *
  136. * @var array
  137. */
  138. protected $aVars;
  139. /**
  140. * Flag indicates that form has been
  141. * submitted via POST
  142. *
  143. * @var bool
  144. */
  145. protected $bSubmitted = false;
  146. /**
  147. * Array of uploaded files
  148. * Basically a copy of $_FILES array that php
  149. * provides
  150. *
  151. * @var array
  152. */
  153. protected $aUploads = array();
  154. /**
  155. * Array of form field errors
  156. * keys should be form field names + '_e', values
  157. * are array of error messages. This way a single form field
  158. * can have more than one validation error message
  159. *
  160. * Before form template is parsed, this array is checked
  161. * and if not empty it is merged with $aVars array, then
  162. * merged array is used in parse() of template
  163. *
  164. * @var array
  165. */
  166. protected $aErrors = array();
  167. /**
  168. * Translation object
  169. *
  170. * @var Object of type Lampcms\I18n\Translator
  171. */
  172. protected $Tr;
  173. public function __construct(Registry $Registry, $useToken = true)
  174. {
  175. $this->Registry = $Registry;
  176. $this->Tr = $Registry->Tr;
  177. $this->useToken = $useToken;
  178. $tpl = $this->template;
  179. d('tpl: ' . $tpl);
  180. if (isset($tpl)) {
  181. $this->aVars = $tpl::getVars();
  182. d('$this->aVars: ' . \json_encode($this->aVars));
  183. }
  184. if (Request::isPost()) {
  185. $this->bSubmitted = true;
  186. if (true === $useToken) {
  187. self::validateToken($Registry);
  188. }
  189. $this->aUploads = $_FILES;
  190. d('$this->aUploads: ' . \json_encode($this->aUploads));
  191. } else {
  192. $this->addToken();
  193. }
  194. $this->init();
  195. }
  196. /**
  197. * Translator method
  198. * It's customary in many projects to
  199. * use the single underscore
  200. * symbol for translation function.
  201. *
  202. * @param string $string string to translate
  203. * @param array $vars optional array of replacement vars for
  204. * translation
  205. *
  206. * @return string translated string
  207. */
  208. protected function _($string, array $vars = null)
  209. {
  210. return $this->Tr->get($string, $vars);
  211. }
  212. /**
  213. * Check to see if form has been submitted
  214. *
  215. * @return bool true if form submitted, false
  216. * if not submitted
  217. */
  218. public function isSubmitted()
  219. {
  220. return $this->bSubmitted;
  221. }
  222. public function enableToken()
  223. {
  224. $this->useToken = true;
  225. return $this;
  226. }
  227. public function disableToken()
  228. {
  229. $this->useToken = false;
  230. d('$this->useToken: ' . $this->useToken);
  231. return $this;
  232. }
  233. public function addValidator($field, $func)
  234. {
  235. if (!is_callable($func)) {
  236. throw new \InvalidArgumentException('second param passed to addValidator must be a callable funcion. Was: ' . var_export($func, true));
  237. }
  238. $aFields = $this->getFields();
  239. if (!in_array($field, $aFields)) {
  240. throw new \Lampcms\DevException('Field ' . $field . ' does not exist in form. Cannot set validator for non-existent field aFields: ' . print_r($aFields, 1));
  241. }
  242. $this->aValidators[$field] = $func;
  243. }
  244. /**
  245. * Run custom validators
  246. * Validators can be added via addValidator() method
  247. * OR a sub class can implement a doValidate() method
  248. * which may contain all necessary validation methods and
  249. * must set errors via setError()
  250. *
  251. *
  252. * @throws \Lampcms\DevException
  253. * @return bool true if there are no validation errors,
  254. */
  255. public function validate()
  256. {
  257. if (!empty($this->aValidators)) {
  258. foreach ($this->aValidators as $field => $func) {
  259. if (!is_callable($func)) {
  260. throw new \Lampcms\DevException('not callable');
  261. }
  262. $val = $this->getSubmittedValue($field);
  263. if (true !== $res = $func($val)) {
  264. $this->setError($field, $res);
  265. }
  266. }
  267. }
  268. $this->doValidate();
  269. return $this->isValid();
  270. }
  271. /**
  272. * Method that invokes form
  273. * validation
  274. *
  275. * Concrete form class can implement its own
  276. * to do custom validation
  277. */
  278. protected function doValidate()
  279. {
  280. }
  281. /**
  282. * Get values of submitted form fields
  283. * Returned values are sanitized by filter_var
  284. * and other custom sanitization we have in Request object
  285. *
  286. * @return array keys are form fields, values are submitted
  287. * values
  288. */
  289. public function getSubmittedValues()
  290. {
  291. $aFields = $this->getFields();
  292. $a = $this->Registry->Request->getArray();
  293. d('$aFields: ' . \print_r($aFields, 1) . ' Request->getArray(): ' . \print_r($a, 1) . ' POST: ' . \print_r($_POST, 1));
  294. /**
  295. * Order of array_intersect_key is very important!
  296. */
  297. $ret = \array_intersect_key($a, \array_flip($aFields));
  298. d('submitted values: ' . \print_r($ret, 1));
  299. return $ret;
  300. }
  301. /**
  302. *
  303. * Get value of certain form field
  304. *
  305. * @param string $field
  306. *
  307. * @throws \Lampcms\DevException if $field does not exist in form
  308. *
  309. * @return string value of submitted field
  310. */
  311. public function getSubmittedValue($field)
  312. {
  313. if (!$this->fieldExists($field)) {
  314. throw new \Lampcms\DevException('Field ' . $field . ' does not exist');
  315. }
  316. return $this->Registry->Request->get($field);
  317. }
  318. /**
  319. * Get path to uploaded file
  320. * The file is first copied to tmp directory
  321. *
  322. * @param string $field
  323. *
  324. * @throws \Lampcms\DevException if move_uploaded_file operation
  325. * fails
  326. *
  327. * @return mixed null | false | string full path to new temporary location
  328. * of the uploaded file null if there is no uploaded file with this
  329. * element name of false if there was a problem with upload
  330. */
  331. public function getUploadedFile($field)
  332. {
  333. d('looking for uploaded file: ' . $field);
  334. if (!$this->fieldExists($field)) {
  335. throw new \Lampcms\DevException('field ' . $field . ' does not exist');
  336. }
  337. if (!array_key_exists($field, $this->aUploads)) {
  338. d('no such file in uploads: ' . $field);
  339. return null;
  340. }
  341. if (!is_array($this->aUploads[$field]) || (0 == $this->aUploads[$field]['size']) || empty($this->aUploads[$field]['tmp_name']) || ('none' == $this->aUploads[$field]['tmp_name'])) {
  342. d('file ' . $field . ' was not uploaded');
  343. return null;
  344. }
  345. /**
  346. * If upload was made but there was an error...
  347. * if 'error' code then
  348. * set element error? throw exception?
  349. * what to return?
  350. * element Error vs Form Error?
  351. * element for file upload input may be hidden by css style
  352. * like in case of avatar upload it is hidden initially
  353. * so it's better to set form error!
  354. *
  355. */
  356. if (UPLOAD_ERR_OK !== $errCode = $this->aUploads[$field]['error']) {
  357. e('Upload of file ' . $field . ' failed with error ' . $this->aUploads[$field]['error']);
  358. if (UPLOAD_ERR_FORM_SIZE === $errCode) {
  359. e('Uploaded file exceeds maximum allowed size');
  360. } elseif (UPLOAD_ERR_INI_SIZE === $errCode) {
  361. e('Uploaded file exceeds maximum upload size');
  362. }
  363. return false;
  364. }
  365. $temp_file = \tempnam(\sys_get_temp_dir(), 'uploaded');
  366. d('$temp_file: ' . $temp_file);
  367. if (false === \move_uploaded_file($this->aUploads[$field]['tmp_name'], $temp_file)) {
  368. d('No go with move_uploaded_file to ' . $temp_file . ' $this->aUploads: ' . print_r($this->aUploads, 1));
  369. throw new \Lampcms\DevException('Unable to copy uploaded file');
  370. }
  371. d('new file path: ' . $temp_file);
  372. return $temp_file;
  373. }
  374. /**
  375. * Getter for $this->aUploads
  376. *
  377. * @return array raw array of $this->aUploads which is
  378. * the copy of the $_FILES Array
  379. */
  380. public function getUploadedFiles()
  381. {
  382. return $this->aUploads;
  383. }
  384. /**
  385. * Check if form had any uploaded files
  386. *
  387. * @return bool true if there are any uploaded files
  388. */
  389. public function hasUploads()
  390. {
  391. return (count($this->aUploads) > 0);
  392. }
  393. /**
  394. *
  395. * Check if certain form field exists in form object
  396. *
  397. * @param string $field
  398. *
  399. * @return bool
  400. */
  401. protected function fieldExists($field)
  402. {
  403. $aFields = $this->getFields();
  404. return in_array($field, $aFields);
  405. }
  406. /**
  407. *
  408. * Enter description here ...
  409. */
  410. public function getFields()
  411. {
  412. $aFields = (!empty($this->aFields)) ? \array_keys($this->aFields) : \array_keys($this->aVars);
  413. return $aFields;
  414. }
  415. /**
  416. * Sub-class may implement init() method
  417. * in order to initialize form vars.
  418. * For example to translate some of the vars into
  419. * current language
  420. *
  421. * @return object $this
  422. */
  423. protected function init()
  424. {
  425. return $this;
  426. }
  427. /**
  428. * Sets error message
  429. * for the form field
  430. *
  431. * We don't check to see if field name exists
  432. * and we don't check if field_e key exists
  433. * in template vars. If it does not then
  434. * setting of error will not fail but will have
  435. * absolutely no meaning since errors will not be shown
  436. * on form.
  437. *
  438. *
  439. * @param string $field
  440. * @param string $message
  441. *
  442. * @return \Lampcms\Forms\Form
  443. */
  444. public function setError($field, $message)
  445. {
  446. if (Request::isAjax()) {
  447. \Lampcms\Responder::sendJSON(array('formElementError' => array($field => $message)));
  448. } else {
  449. $this->aErrors[$field . '_e'][] = $message;
  450. }
  451. return $this;
  452. }
  453. /**
  454. * Set error message for the form as a whole.
  455. * This error message is not specific to any form field,
  456. * it usually appears on top of form as a general error message
  457. *
  458. * For example: You must wait 5 minutes between posting
  459. * This is not due to any element error, just a general error
  460. * message.
  461. *
  462. * The form template MUST have 'formError' variable in it!
  463. *
  464. * @param string $errMessage
  465. *
  466. * @return \Lampcms\Forms\Form
  467. */
  468. public function setFormError($errMessage)
  469. {
  470. if (Request::isAjax()) {
  471. \Lampcms\Responder::sendJSON(array('formError' => $errMessage));
  472. } else {
  473. $this->aErrors['formError'][] = $errMessage;
  474. }
  475. return $this;
  476. }
  477. /**
  478. *
  479. * Set variable (any variable that
  480. * is present in form's template
  481. *
  482. * @param string $name
  483. * @param string $value
  484. *
  485. * @throws \InvalidArgumentException
  486. *
  487. * @return object $this
  488. */
  489. public function setVar($name, $value)
  490. {
  491. if (!array_key_exists($name, $this->aVars)) {
  492. throw new \InvalidArgumentException('Var ' . $name . ' does not exist in this form\'s template aVars: ' . print_r($this->aVars, 1));
  493. }
  494. $this->aVars[$name] = $value;
  495. return $this;
  496. }
  497. /**
  498. *
  499. * Magic setter
  500. *
  501. * @param string $name
  502. * @param string $val
  503. */
  504. public function __set($name, $val)
  505. {
  506. $this->setVar($name, $val);
  507. }
  508. /**
  509. * Getter for $this->aErrors
  510. *
  511. * @return array
  512. */
  513. public function getErrors()
  514. {
  515. return $this->aErrors;
  516. }
  517. /**
  518. * Parse form template using vars/values we set
  519. * also if aErrors not empty, merge it with aVars
  520. *
  521. * @param bool $useSubmittedVars if set to false then
  522. * will not update $this->aVars to the values of submitted
  523. * values and will reuse the vars that were set initially.
  524. * This is useful when form was submitted but then some error
  525. * occurred in a script that was parsing the form.
  526. * In that case
  527. * we often need to setFormError and then use values in form
  528. * than were there initially, no using any of the submitted values.
  529. *
  530. * @return string html parsed form template
  531. */
  532. public function getForm($useSubmittedVars = true)
  533. {
  534. d('$this->aVars: ' . \json_debug($this->aVars));
  535. if ($useSubmittedVars) {
  536. $this->prepareVars();
  537. }
  538. $this->addErrors();
  539. $tpl = $this->template;
  540. /**
  541. * Observer can
  542. * do setVar() on a passed form object
  543. * and add another element to aVars just before
  544. * form is rendered
  545. *
  546. */
  547. $this->Registry->Dispatcher->post($this, 'onBeforeFormRender');
  548. return $tpl::parse($this->aVars);
  549. }
  550. /**
  551. *
  552. * @return object $this
  553. */
  554. protected function prepareVars()
  555. {
  556. if ($this->bSubmitted) {
  557. $a = $this->Registry->Request->getArray();
  558. d('a from request: ' . print_r($a, 1));
  559. d('$this->aVars : ' . print_r($this->aVars, 1));
  560. $this->aVars = \array_merge($this->aVars, $a);
  561. }
  562. return $this;
  563. }
  564. /**
  565. * It makes sense to call this method ONLY after
  566. * you validated the form values yourself and
  567. * set errors via setError() method
  568. *
  569. * @return bool true if no errors has been set,
  570. * false otherwise
  571. *
  572. */
  573. public function isValid()
  574. {
  575. return 0 === count($this->aErrors);
  576. }
  577. /**
  578. * If aErrors not empty then merge aVars with aErrors
  579. *
  580. * @return object $this
  581. */
  582. protected function addErrors()
  583. {
  584. if (!empty($this->aErrors)) {
  585. $this->aVars = array_merge($this->aVars, $this->flattenErrors());
  586. d('$this->aVars: ' . \print_r($this->aVars, 1));
  587. }
  588. return $this;
  589. }
  590. /**
  591. * Turn array of errors into string
  592. * in which each element from array becomes
  593. * an <li> html element
  594. *
  595. * @return array where keys are field names + _e
  596. * and values are strings contained in <ul> tag
  597. *
  598. */
  599. protected function flattenErrors()
  600. {
  601. $ret = array();
  602. foreach ($this->aErrors as $field => $aErrors) {
  603. $ret[$field] = '<ul>';
  604. foreach ($aErrors as $error) {
  605. $ret[$field] .= '<li>' . $error . '</li>';
  606. }
  607. $ret[$field] .= '</ul>';
  608. }
  609. d('$ret: ' . \print_r($ret, 1));
  610. return $ret;
  611. }
  612. /**
  613. * Generate unique ID and store in session
  614. * The page will have the meta tag 'version'
  615. * with the value of this token
  616. * it will be used by ajax based forms when submitting
  617. * form via ajax
  618. *
  619. *
  620. * @return string value of form token
  621. * for this class.
  622. */
  623. public static function generateToken()
  624. {
  625. if (!\array_key_exists('secret', $_SESSION)) {
  626. $token = \uniqid(\mt_rand());
  627. $_SESSION['secret'] = \hash('md5', $token);
  628. }
  629. return $_SESSION['secret'];
  630. }
  631. /**
  632. * Add value of 'token' to form's aVars
  633. *
  634. * @return object $this
  635. */
  636. protected function addToken()
  637. {
  638. if ($this->useToken) {
  639. $this->aVars['token'] = static::generateToken();
  640. }
  641. return $this;
  642. }
  643. /**
  644. * Validate submitted 'token' value
  645. * against generateToken()
  646. * they must match OR throw TokenException
  647. *
  648. * Must be static because we use this sometimes
  649. * from outside this object.
  650. *
  651. * @param Registry $Registry
  652. *
  653. * @throws \Lampcms\TokenException
  654. * @return bool true on success
  655. */
  656. public static function validateToken(Registry $Registry)
  657. {
  658. if (empty($_SESSION['secret'])) {
  659. throw new \Lampcms\TokenException('@@Form token not found in session@@');
  660. }
  661. $token = $Registry->Request['token'];
  662. d('submitted form token: ' . $token);
  663. if ($token !== $_SESSION['secret']) {
  664. throw new \Lampcms\TokenException('@@Invalid security token. You need to reload this page in browser and try submitting this form again@@');
  665. }
  666. return true;
  667. }
  668. /**
  669. * Not going to use translation here
  670. * We now translating all vars on page render
  671. * from inside output buffer
  672. *
  673. * @return object $this
  674. */
  675. protected function translateVars()
  676. {
  677. d('cp');
  678. return $this;
  679. }
  680. }