Forms.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <?php
  2. namespace Luracast\Restler\UI;
  3. use Luracast\Restler\CommentParser;
  4. use Luracast\Restler\Data\ApiMethodInfo;
  5. use Luracast\Restler\Data\Text;
  6. use Luracast\Restler\Data\ValidationInfo;
  7. use Luracast\Restler\Data\Validator;
  8. use Luracast\Restler\Defaults;
  9. use Luracast\Restler\Format\UploadFormat;
  10. use Luracast\Restler\Format\UrlEncodedFormat;
  11. use Luracast\Restler\iFilter;
  12. use Luracast\Restler\RestException;
  13. use Luracast\Restler\Restler;
  14. use Luracast\Restler\Routes;
  15. use Luracast\Restler\Scope;
  16. use Luracast\Restler\UI\Tags as T;
  17. use Luracast\Restler\User;
  18. use Luracast\Restler\Util;
  19. /**
  20. * Utility class for automatically generating forms for the given http method
  21. * and api url
  22. *
  23. * @category Framework
  24. * @package Restler
  25. * @author R.Arul Kumaran <arul@luracast.com>
  26. * @copyright 2010 Luracast
  27. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  28. * @link http://luracast.com/products/restler/
  29. *
  30. */
  31. class Forms implements iFilter
  32. {
  33. const FORM_KEY = 'form_key';
  34. public static $filterFormRequestsOnly = false;
  35. public static $excludedPaths = array();
  36. private static $style;
  37. /**
  38. * @var bool should we fill up the form using given data?
  39. */
  40. public static $preFill = true;
  41. /**
  42. * @var ValidationInfo
  43. */
  44. public static $validationInfo = null;
  45. protected static $inputTypes = array(
  46. 'hidden',
  47. 'password',
  48. 'button',
  49. 'image',
  50. 'file',
  51. 'reset',
  52. 'submit',
  53. 'search',
  54. 'checkbox',
  55. 'radio',
  56. 'email',
  57. 'text',
  58. 'color',
  59. 'date',
  60. 'datetime',
  61. 'datetime-local',
  62. 'email',
  63. 'month',
  64. 'number',
  65. 'range',
  66. 'search',
  67. 'tel',
  68. 'time',
  69. 'url',
  70. 'week',
  71. );
  72. protected static $fileUpload = false;
  73. private static $key = array();
  74. /**
  75. * @var ApiMethodInfo;
  76. */
  77. private static $info;
  78. public static function setStyles(HtmlForm $style)
  79. {
  80. static::$style = get_class($style);
  81. }
  82. /**
  83. * Get the form
  84. *
  85. * @param string $method http method to submit the form
  86. * @param string $action relative path from the web root. When set to null
  87. * it uses the current api method's path
  88. * @param bool $dataOnly if you want to render the form yourself use this
  89. * option
  90. * @param string $prefix used for adjusting the spacing in front of
  91. * form elements
  92. * @param string $indent used for adjusting indentation
  93. *
  94. * @return array|T
  95. *
  96. * @throws RestException
  97. */
  98. public static function get($method = 'POST', $action = null, $dataOnly = false, $prefix = '', $indent = ' ')
  99. {
  100. if (!static::$style) {
  101. static::$style = 'Luracast\\Restler\\UI\HtmlForm';
  102. }
  103. try {
  104. /** @var Restler $restler */
  105. $restler = Scope::get('Restler');
  106. if (is_null($action)) {
  107. $action = $restler->url;
  108. }
  109. $info = $restler->url == $action
  110. && Util::getRequestMethod() == $method
  111. ? $restler->apiMethodInfo
  112. : Routes::find(
  113. trim($action, '/'),
  114. $method,
  115. $restler->getRequestedApiVersion(),
  116. static::$preFill ||
  117. ($restler->requestMethod == $method &&
  118. $restler->url == $action)
  119. ? $restler->getRequestData()
  120. : array()
  121. );
  122. } catch (RestException $e) {
  123. //echo $e->getErrorMessage();
  124. $info = false;
  125. }
  126. if (!$info) {
  127. throw new RestException(500, 'invalid action path for form `' . $method . ' ' . $action . '`');
  128. }
  129. static::$info = $info;
  130. $m = $info->metadata;
  131. $r = static::fields($dataOnly);
  132. if ($method != 'GET' && $method != 'POST') {
  133. if (empty(Defaults::$httpMethodOverrideProperty)) {
  134. throw new RestException(
  135. 500,
  136. 'Forms require `Defaults::\$httpMethodOverrideProperty`' .
  137. "for supporting HTTP $method"
  138. );
  139. }
  140. if ($dataOnly) {
  141. $r[] = array(
  142. 'tag' => 'input',
  143. 'name' => Defaults::$httpMethodOverrideProperty,
  144. 'type' => 'hidden',
  145. 'value' => 'method',
  146. );
  147. } else {
  148. $r[] = T::input()
  149. ->name(Defaults::$httpMethodOverrideProperty)
  150. ->value($method)
  151. ->type('hidden');
  152. }
  153. $method = 'POST';
  154. }
  155. if (session_id() != '') {
  156. $form_key = static::key($method, $action);
  157. if ($dataOnly) {
  158. $r[] = array(
  159. 'tag' => 'input',
  160. 'name' => static::FORM_KEY,
  161. 'type' => 'hidden',
  162. 'value' => 'hidden',
  163. );
  164. } else {
  165. $key = T::input()
  166. ->name(static::FORM_KEY)
  167. ->type('hidden')
  168. ->value($form_key);
  169. $r[] = $key;
  170. }
  171. }
  172. $s = array(
  173. 'tag' => 'button',
  174. 'type' => 'submit',
  175. 'label' =>
  176. Util::nestedValue($m, 'return', CommentParser::$embeddedDataName, 'label')
  177. ?: 'Submit'
  178. );
  179. if (!$dataOnly) {
  180. $s = Emmet::make(static::style('submit', $m), $s);
  181. }
  182. $r[] = $s;
  183. $t = array(
  184. 'action' => $restler->getBaseUrl() . '/' . rtrim($action, '/'),
  185. 'method' => $method,
  186. );
  187. if (static::$fileUpload) {
  188. static::$fileUpload = false;
  189. $t['enctype'] = 'multipart/form-data';
  190. }
  191. if (isset($m[CommentParser::$embeddedDataName])) {
  192. $t += $m[CommentParser::$embeddedDataName];
  193. }
  194. if (!$dataOnly) {
  195. $t = Emmet::make(static::style('form', $m), $t);
  196. $t->prefix = $prefix;
  197. $t->indent = $indent;
  198. $t[] = $r;
  199. } else {
  200. $t['fields'] = $r;
  201. }
  202. return $t;
  203. }
  204. public static function style($name, array $metadata, $type = '')
  205. {
  206. if (isset($metadata[CommentParser::$embeddedDataName][$name])) {
  207. return $metadata[CommentParser::$embeddedDataName][$name];
  208. }
  209. $style = static::$style . '::' . $name;
  210. $typedStyle = $style . '_' . $type;
  211. if (defined($typedStyle)) {
  212. return constant($typedStyle);
  213. }
  214. if (defined($style)) {
  215. return constant($style);
  216. }
  217. return null;
  218. }
  219. public static function fields($dataOnly = false)
  220. {
  221. $m = static::$info->metadata;
  222. $params = $m['param'];
  223. $values = static::$info->parameters;
  224. $r = array();
  225. foreach ($params as $k => $p) {
  226. $value = Util::nestedValue($values, $k);
  227. if (
  228. is_scalar($value) ||
  229. ($p['type'] == 'array' && is_array($value) && $value == array_values($value)) ||
  230. is_object($value) && $p['type'] == get_class($value)
  231. ) {
  232. $p['value'] = $value;
  233. }
  234. static::$validationInfo = $v = new ValidationInfo($p);
  235. if ($v->from == 'path') {
  236. continue;
  237. }
  238. if (!empty($v->children)) {
  239. $t = Emmet::make(static::style('fieldset', $m), array('label' => $v->label));
  240. foreach ($v->children as $n => $c) {
  241. $value = Util::nestedValue($v->value, $n);
  242. if (
  243. is_scalar($value) ||
  244. ($c['type'] == 'array' && is_array($value) && $value == array_values($value)) ||
  245. is_object($value) && $c['type'] == get_class($value)
  246. ) {
  247. $c['value'] = $value;
  248. }
  249. static::$validationInfo = $vc = new ValidationInfo($c);
  250. if ($vc->from == 'path') {
  251. continue;
  252. }
  253. $vc->name = $v->name . '[' . $vc->name . ']';
  254. $t [] = static::field($vc, $dataOnly);
  255. }
  256. $r[] = $t;
  257. static::$validationInfo = null;
  258. } else {
  259. $f = static::field($v, $dataOnly);
  260. $r [] = $f;
  261. }
  262. static::$validationInfo = null;
  263. }
  264. return $r;
  265. }
  266. /**
  267. * @param ValidationInfo $p
  268. *
  269. * @param bool $dataOnly
  270. *
  271. * @return array|T
  272. */
  273. public static function field(ValidationInfo $p, $dataOnly = false)
  274. {
  275. if (is_string($p->value)) {
  276. //prevent XSS attacks
  277. $p->value = htmlspecialchars($p->value, ENT_QUOTES | ENT_HTML401, 'UTF-8');
  278. }
  279. $type = $p->field ?: static::guessFieldType($p);
  280. $tag = in_array($type, static::$inputTypes)
  281. ? 'input' : $type;
  282. $options = array();
  283. $name = $p->name;
  284. $multiple = null;
  285. if ($p->type == 'array' && $p->contentType != 'associative') {
  286. $name .= '[]';
  287. $multiple = true;
  288. }
  289. if ($p->choice) {
  290. foreach ($p->choice as $i => $choice) {
  291. $option = array('name' => $name, 'value' => $choice);
  292. $option['text'] = isset($p->rules['select'][$i])
  293. ? $p->rules['select'][$i]
  294. : $choice;
  295. if ($choice == $p->value) {
  296. $option['selected'] = true;
  297. }
  298. $options[] = $option;
  299. }
  300. } elseif ($p->type == 'boolean' || $p->type == 'bool') {
  301. if (Text::beginsWith($type, 'radio') || Text::beginsWith($type, 'select')) {
  302. $options[] = array(
  303. 'name' => $p->name,
  304. 'text' => ' Yes ',
  305. 'value' => 'true'
  306. );
  307. $options[] = array(
  308. 'name' => $p->name,
  309. 'text' => ' No ',
  310. 'value' => 'false'
  311. );
  312. if ($p->value || $p->default) {
  313. $options[0]['selected'] = true;
  314. }
  315. } else { //checkbox
  316. $r = array(
  317. 'tag' => $tag,
  318. 'name' => $name,
  319. 'type' => $type,
  320. 'label' => $p->label,
  321. 'value' => 'true',
  322. 'default' => $p->default,
  323. );
  324. $r['text'] = 'Yes';
  325. if ($p->default) {
  326. $r['selected'] = true;
  327. }
  328. if (isset($p->rules)) {
  329. $r += $p->rules;
  330. }
  331. }
  332. }
  333. if (empty($r)) {
  334. $r = array(
  335. 'tag' => $tag,
  336. 'name' => $name,
  337. 'type' => $type,
  338. 'label' => $p->label,
  339. 'value' => $p->value,
  340. 'default' => $p->default,
  341. 'options' => & $options,
  342. 'multiple' => $multiple,
  343. );
  344. if (isset($p->rules)) {
  345. $r += $p->rules;
  346. }
  347. }
  348. if ($type == 'file') {
  349. static::$fileUpload = true;
  350. if (empty($r['accept'])) {
  351. $r['accept'] = implode(', ', UploadFormat::$allowedMimeTypes);
  352. }
  353. }
  354. if (!empty(Validator::$exceptions[$name]) && static::$info->url == Scope::get('Restler')->url) {
  355. $r['error'] = 'has-error';
  356. $r['message'] = Validator::$exceptions[$p->name]->getMessage();
  357. }
  358. if (true === $p->required) {
  359. $r['required'] = 'required';
  360. }
  361. if (isset($p->rules['autofocus'])) {
  362. $r['autofocus'] = 'autofocus';
  363. }
  364. /*
  365. echo "<pre>";
  366. print_r($r);
  367. echo "</pre>";
  368. */
  369. if ($dataOnly) {
  370. return $r;
  371. }
  372. if (isset($p->rules['form'])) {
  373. return Emmet::make($p->rules['form'], $r);
  374. }
  375. $m = static::$info->metadata;
  376. $t = Emmet::make(static::style($type, $m, $p->type) ?: static::style($tag, $m, $p->type), $r);
  377. return $t;
  378. }
  379. protected static function guessFieldType(ValidationInfo $p, $type = 'type')
  380. {
  381. if (in_array($p->$type, static::$inputTypes)) {
  382. return $p->$type;
  383. }
  384. if ($p->choice) {
  385. return $p->type == 'array' ? 'checkbox' : 'select';
  386. }
  387. switch ($p->$type) {
  388. case 'boolean':
  389. return 'radio';
  390. case 'int':
  391. case 'number':
  392. case 'float':
  393. return 'number';
  394. case 'array':
  395. return static::guessFieldType($p, 'contentType');
  396. }
  397. if ($p->name == 'password') {
  398. return 'password';
  399. }
  400. return 'text';
  401. }
  402. /**
  403. * Get the form key
  404. *
  405. * @param string $method http method for form key
  406. * @param string $action relative path from the web root. When set to null
  407. * it uses the current api method's path
  408. *
  409. * @return string generated form key
  410. */
  411. public static function key($method = 'POST', $action = null)
  412. {
  413. if (is_null($action)) {
  414. $action = Scope::get('Restler')->url;
  415. }
  416. $target = "$method $action";
  417. if (empty(static::$key[$target])) {
  418. static::$key[$target] = md5($target . User::getIpAddress() . uniqid(mt_rand()));
  419. }
  420. $_SESSION[static::FORM_KEY] = static::$key;
  421. return static::$key[$target];
  422. }
  423. /**
  424. * Access verification method.
  425. *
  426. * API access will be denied when this method returns false
  427. *
  428. * @return boolean true when api access is allowed false otherwise
  429. *
  430. * @throws RestException 403 security violation
  431. */
  432. public function __isAllowed()
  433. {
  434. if (session_id() == '') {
  435. session_start();
  436. }
  437. /** @var Restler $restler */
  438. $restler = $this->restler;
  439. $url = $restler->url;
  440. foreach (static::$excludedPaths as $exclude) {
  441. if (empty($exclude)) {
  442. if ($url == $exclude) {
  443. return true;
  444. }
  445. } elseif (Text::beginsWith($url, $exclude)) {
  446. return true;
  447. }
  448. }
  449. $check = static::$filterFormRequestsOnly
  450. ? $restler->requestFormat instanceof UrlEncodedFormat || $restler->requestFormat instanceof UploadFormat
  451. : true;
  452. if (!empty($_POST) && $check) {
  453. if (
  454. isset($_POST[static::FORM_KEY]) &&
  455. ($target = Util::getRequestMethod() . ' ' . $restler->url) &&
  456. isset($_SESSION[static::FORM_KEY][$target]) &&
  457. $_POST[static::FORM_KEY] == $_SESSION[static::FORM_KEY][$target]
  458. ) {
  459. return true;
  460. }
  461. throw new RestException(403, 'Insecure form submission');
  462. }
  463. return true;
  464. }
  465. }