PageRenderTime 55ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/classes/fuel/fieldset.php

https://github.com/jay3/core
PHP | 621 lines | 477 code | 94 blank | 50 comment | 124 complexity | dd95ca52fd78c4dcf9a1352e20476174 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * NOVIUS OS - Web OS for digital communication
  4. *
  5. * @copyright 2011 Novius
  6. * @license GNU Affero General Public License v3 or (at your option) any later version
  7. * http://www.gnu.org/licenses/agpl-3.0.html
  8. * @link http://www.novius-os.org
  9. */
  10. class Fieldset extends \Fuel\Core\Fieldset
  11. {
  12. protected $append = array();
  13. protected $prepend = array();
  14. protected $config_used = array();
  15. protected $js_validation = false;
  16. protected $require_js = array();
  17. protected $instance = null;
  18. public function prepend($content)
  19. {
  20. $this->prepend[] = $content;
  21. }
  22. public function append($content)
  23. {
  24. $this->append[] = $content;
  25. }
  26. public function open($action = null)
  27. {
  28. $attributes = $this->get_config('form_attributes');
  29. if (empty($attributes['id'])) {
  30. $attributes['id'] = uniqid('form_');
  31. $this->set_config('form_attributes', $attributes);
  32. }
  33. if ($action and ($this->fieldset_tag == 'form' or empty($this->fieldset_tag))) {
  34. $attributes['action'] = $action;
  35. }
  36. $open = ($this->fieldset_tag == 'form' or empty($this->fieldset_tag))
  37. ? $this->form()->open($attributes).PHP_EOL
  38. : $this->form()->{$this->fieldset_tag.'_open'}($attributes);
  39. return $open;
  40. }
  41. public function build($action = null)
  42. {
  43. $build = parent::build($action);
  44. return $build.$this->build_append().$this->build_js_validation();
  45. }
  46. public function close()
  47. {
  48. $close = ($this->fieldset_tag == 'form' or empty($this->fieldset_tag))
  49. ? $this->form()->close().PHP_EOL
  50. : $this->form()->{$this->fieldset_tag.'_close'}();
  51. return $this->build_prepend().$close.$this->build_append().$this->build_js_validation();
  52. }
  53. public function build_hidden_fields()
  54. {
  55. $output = '';
  56. foreach ($this->field() as $field) {
  57. if (false != mb_strpos(get_class($field), 'Renderer_')) {
  58. continue;
  59. }
  60. if ($field->type == 'hidden') {
  61. $output .= $field->build();
  62. }
  63. }
  64. return $output;
  65. }
  66. public function build_prepend()
  67. {
  68. return $this->build_list($this->prepend);
  69. }
  70. public function build_append()
  71. {
  72. return $this->build_list($this->append);
  73. }
  74. public function build_list($list)
  75. {
  76. $append = array();
  77. foreach ($list as $a) {
  78. if (is_callable($a)) {
  79. $append[] = call_user_func($a, $this);
  80. } else {
  81. $append[] = $a;
  82. }
  83. }
  84. return implode('', $append);
  85. }
  86. public function build_js_validation()
  87. {
  88. $json = array();
  89. if ($this->js_validation) {
  90. foreach ($this->fields as $f) {
  91. $rules = $f->rules;
  92. if (empty($rules)) {
  93. continue;
  94. }
  95. foreach ($rules as $rule) {
  96. if (empty($rule)) {
  97. continue;
  98. }
  99. list($name, $args) = $rule;
  100. is_array($name) and $name = reset($name);
  101. list($js_name, $js_args) = $this->format_js_validation($name, $args);
  102. if (empty($js_name)) {
  103. continue;
  104. }
  105. $json['rules'][$f->name][$js_name] = $js_args;
  106. // Computes the error message, replacing :args placeholders with {n}
  107. $error = new \Validation_Error($f, '', array($name => ''), array());
  108. $error = $error->get_message();
  109. preg_match_all('`:param:(\d+)`u', $error, $m);
  110. foreach ($m[1] as $int) {
  111. $error = str_replace(':param:'.$int, '{' . ($int - 1).'}', $error);
  112. }
  113. $json['messages'][$f->name][$js_name] = $error;
  114. }
  115. }
  116. }
  117. $form_attributes = $this->get_config('form_attributes', array());
  118. return (string) \View::forge('form/fieldset_js', array(
  119. 'id' => $form_attributes['id'],
  120. 'rules' => \Format::forge()->to_json($json),
  121. 'require_js' => $this->require_js,
  122. ), false);
  123. }
  124. public function form_name($value)
  125. {
  126. if ($field = $this->field('form_name')) {
  127. return $field->value == $value;
  128. }
  129. $this->add('form_name', '', array('type' => 'hidden', 'value' => $value));
  130. }
  131. /**
  132. *
  133. * @param \Fuel\Core\Fieldset_Field $field A field instance
  134. * @return \Fuel\Core\Fieldset_Field
  135. */
  136. public function add_field(\Fuel\Core\Fieldset_Field $field)
  137. {
  138. $name = $field->name;
  139. if (empty($name)) {
  140. throw new \InvalidArgumentException('Cannot create field without name.');
  141. }
  142. // Check if it exists already, if so: return and give notice
  143. if ($existing = static::field($name)) {
  144. \Error::notice('Field with this name "'.$name.'" exists already, cannot be overwritten through add().');
  145. return $existing;
  146. }
  147. // Make sure fieldset is current
  148. if ($field->fieldset() != $this) {
  149. \Error::notice('A field added through add() must have the correct parent fieldset.');
  150. return false;
  151. }
  152. $this->fields[$name] = $field;
  153. return $field;
  154. }
  155. /**
  156. * Override default populate() to allow renderers populate themselves
  157. * @param array|object The whole input array
  158. * @param bool Also repopulate?
  159. * @return Fieldset this, to allow chaining
  160. */
  161. public function populate($input, $repopulate = false)
  162. {
  163. foreach ($this->fields as $f) {
  164. $class = mb_strtolower(\Inflector::denamespace(get_class($f)));
  165. if (mb_substr($class, 0, 8) == 'renderer' && isset($input->{$f->name})) {
  166. $f->populate($input);
  167. }
  168. }
  169. return parent::populate($input, $repopulate);
  170. }
  171. /**
  172. * Override default repopulate() to allow renderers populate themselves
  173. *
  174. * @param array|object input for initial population of fields, this is deprecated - you should use populate() instea
  175. * @return Fieldset this, to allow chaining
  176. */
  177. public function repopulate()
  178. {
  179. $input = mb_strtolower($this->form()->get_attribute('method', 'post')) == 'get' ? \Input::get() : \Input::post();
  180. foreach ($this->fields as $f) {
  181. if ($f->type == 'checkbox' && !isset($input[$f->name])) {
  182. $f->set_attribute('checked', null);
  183. }
  184. // Don't repopulate the CSRF field
  185. if ($f->name === \Config::get('security.csrf_token_key', 'fuel_csrf_token')) {
  186. continue;
  187. }
  188. if (mb_substr(mb_strtolower(\Inflector::denamespace(get_class($f))), 0, 8) == 'renderer') {
  189. // Renderers populates themselves
  190. $f->repopulate($input);
  191. }
  192. }
  193. parent::repopulate(); //return parent::populate($input);
  194. }
  195. /**
  196. * Get populated values
  197. *
  198. * @param string null to fetch an array of all
  199. * @return array|false returns false when field wasn't found
  200. */
  201. public function value($name = null)
  202. {
  203. if ($name === null) {
  204. $values = array();
  205. foreach ($this->fields as $f) {
  206. if ($f->type == 'checkbox' && $f->get_attribute('checked') == null) {
  207. $values[$f->name] = null;
  208. } else {
  209. $values[$f->name] = $f->value;
  210. }
  211. }
  212. return $values;
  213. }
  214. return $this->field($name)->value;
  215. }
  216. /**
  217. * Set a Model's properties as fields on a Fieldset, which will be created with the Model's
  218. * classname if none is provided.
  219. *
  220. * @param string
  221. * @param Fieldset|null
  222. * @return Fieldset
  223. */
  224. public function add_model_renderers($class, $instance = null, $options = array())
  225. {
  226. if (is_object($class)) {
  227. $instance = $class;
  228. $class = get_class($class);
  229. $options = $instance;
  230. }
  231. $properties = is_object($instance) ? $instance->properties() : $class::properties();
  232. $this->add_renderers($properties);
  233. $instance and $this->populate($instance);
  234. return $this;
  235. }
  236. public function add_renderers($properties, $options = array())
  237. {
  238. // Compatibility with 0.1 configuration (widgets have been renamed to renderers)
  239. foreach ($properties as &$property) {
  240. if (isset($property['widget'])) {
  241. \Log::deprecated('The widget key is deprecated ('.$property['widget'].'). Please use the renderer key and update class name.');
  242. $property['renderer'] = preg_replace('`^Nos(.+)Widget_(.+)$`', 'Nos$1Renderer_$2', $property['widget']);
  243. unset($property['widget']);
  244. }
  245. if (isset($property['widget_options'])) {
  246. \Log::deprecated('The widget_options key is deprecated. Please use the renderer_options key.');
  247. $property['renderer_options'] =& $property['widget_options'];
  248. unset($property['widget_options']);
  249. }
  250. }
  251. $this->config_used = $properties;
  252. foreach ($properties as $p => $settings) {
  253. if (!empty($options['action']) && isset($settings[$options['action']]) && false === $settings[$options['action']]) {
  254. continue;
  255. }
  256. $label = isset($settings['label']) ? $settings['label'] : $p;
  257. $attributes = isset($settings['form']) ? $settings['form'] : array();
  258. if (!empty($settings['renderer'])) {
  259. $class = $settings['renderer'];
  260. $attributes['renderer_options'] = \Arr::get($settings, 'renderer_options', array());
  261. $field = new $class($p, $label, $attributes, array(), $this);
  262. $this->add_field($field);
  263. } else {
  264. if (\Arr::get($attributes, 'type', '') == 'checkbox') {
  265. unset($attributes['empty']);
  266. }
  267. $field = $this->add($p, $label, $attributes);
  268. }
  269. if (isset($settings['template'])) {
  270. $field->set_template($settings['template']);
  271. }
  272. if (!empty($settings['validation'])) {
  273. foreach ($settings['validation'] as $rule => $args) {
  274. if (is_int($rule) and is_string($args)) {
  275. $args = array($args);
  276. } else {
  277. array_unshift($args, $rule);
  278. }
  279. call_user_func_array(array($field, 'add_rule'), $args);
  280. }
  281. }
  282. }
  283. }
  284. public function format_js_validation($name, $args)
  285. {
  286. static $i = 1;
  287. if ($name == 'required') {
  288. return array('required', true);
  289. }
  290. if ($name == 'min_length') {
  291. return array('minlength', $args[0]);
  292. }
  293. if ($name == 'max_length') {
  294. return array('maxlength', $args[0]);
  295. }
  296. if ($name == 'exact_length') {
  297. return array('length', $args[0]);
  298. }
  299. if ($name == 'match_field') {
  300. $field_id = $this->field($args[0])->get_attribute('id');
  301. if (empty($field_id)) {
  302. $field_id = 'field_id_'.$i++;
  303. $this->field($args[0])->set_attribute('id', $field_id);
  304. }
  305. return array('equalTo', '#'.$field_id);
  306. }
  307. if ($name == 'valid_email') {
  308. return array('email', true);
  309. }
  310. return false;
  311. return array($name, $args);
  312. }
  313. public function js_validation($require_js = array())
  314. {
  315. $this->js_validation = true;
  316. $this->require_js = array_merge($this->require_js, $require_js);
  317. }
  318. public static function build_from_config($config, $item = null, $options = array())
  319. {
  320. $instance = null;
  321. if (is_object($item)) {
  322. $instance = $item;
  323. empty($options['action']) && $options['action'] = 'edit';
  324. } elseif (is_string($item)) {
  325. $instance = null;
  326. empty($options['action']) && $options['action'] = 'add';
  327. } elseif (is_array($item)) {
  328. $options = $item;
  329. $instance = null;
  330. }
  331. $options['instance'] = $instance;
  332. $options = \Arr::merge(array(
  333. 'save' => \Input::method() == 'POST',
  334. ), $options);
  335. $fieldset = \Fieldset::forge(uniqid(), array(
  336. 'inline_errors' => true,
  337. 'auto_id' => true,
  338. 'required_mark' => '&nbsp;<span style="font-size: 1.5em; line-height: 1em; font-weight: bold">*</span>',
  339. 'error_template' => '{error_msg}',
  340. 'error_class' => 'error',
  341. 'form_template' => "\n\t\t{open}\n\t\t<table class=\"fieldset\">\n{fields}\n\t\t</table>\n\t\t{close}\n",
  342. ));
  343. if (!empty($options['form_name'])) {
  344. $fieldset->form_name($options['form_name']);
  345. }
  346. if (isset($instance)) {
  347. // Let behaviours do their job (publication for example)
  348. $instance->event('form_fieldset_fields', array(&$config));
  349. }
  350. $fieldset->add_renderers($config, $options);
  351. if (!empty($options['extend']) && is_callable($options['extend'])) {
  352. call_user_func($options['extend'], $fieldset);
  353. }
  354. if (isset($instance)) {
  355. $fieldset->populate_with_instance($instance);
  356. }
  357. if ($options['save'] && (empty($options['form_name']) || \Input::post('form_name') == $options['form_name'])) {
  358. $fieldset->repopulate();
  359. if ($fieldset->validation()->run($fieldset->value())) {
  360. $json = $fieldset->triggerComplete($item, $fieldset->validated(), $options);
  361. \Response::json($json);
  362. } else {
  363. \Response::json(array(
  364. 'error' => (string) current($fieldset->error()),
  365. 'config' => $config,
  366. ));
  367. }
  368. }
  369. return $fieldset;
  370. }
  371. public function triggerComplete($item, $data, $options = array())
  372. {
  373. $options['fieldset'] = $this;
  374. if (!empty($options['complete']) && is_callable($options['complete'])) {
  375. return call_user_func($options['complete'], $data, $item, $this->config_used, $options);
  376. } else {
  377. return static::defaultComplete($data, $item, $this->config_used, $options);
  378. }
  379. }
  380. public function populate_with_instance($instance = null, $generate_id = true)
  381. {
  382. $this->instance = $instance;
  383. if ($generate_id) {
  384. $uniqid = uniqid();
  385. // Generate a new ID for the form
  386. $form_attributes = $this->get_config('form_attributes', array());
  387. $form_attributes['id'] = 'form_id_'.$uniqid;
  388. list($application) = \Config::configFile($instance);
  389. $form_attributes['class'] = str_replace(array('\\', '_'), '-', strtolower(get_class($instance))).' '.$application;
  390. $this->set_config('auto_id_prefix', 'form'.$uniqid.'_');
  391. $this->set_config('form_attributes', $form_attributes);
  392. foreach ($this->fields as $field) {
  393. $id = $field->get_attribute('id');
  394. if (!empty($id)) {
  395. $field->set_attribute('id', '');
  396. }
  397. }
  398. }
  399. if (empty($instance)) {
  400. return;
  401. }
  402. $populate = array();
  403. foreach ($this->fields as $k => $f) {
  404. $populateCallback = \Arr::get($this->config_used, "$k.populate");
  405. if ($populateCallback && is_callable($populateCallback)) {
  406. $populate[$k] = call_user_func($populateCallback, $instance);
  407. continue;
  408. }
  409. // Don't populate password fields and submit
  410. if (in_array(\Arr::get($this->config_used, "$k.form.type"), array('password', 'submit'))) {
  411. continue;
  412. }
  413. // Don't populate some fields (for example, the context)
  414. if (\Arr::get($this->config_used, "$k.dont_populate", false) == true) {
  415. continue;
  416. }
  417. if (isset($instance->{$k})) {
  418. $populate[$k] = $instance->{$k};
  419. }
  420. }
  421. $this->populate($populate);
  422. }
  423. public static function defaultComplete($data, $item, $fields, $options)
  424. {
  425. if (isset($options['fieldset'])) {
  426. $fieldset = $options['fieldset'];
  427. } else {
  428. $fieldset = false;
  429. }
  430. if (!is_object($item)) {
  431. return;
  432. }
  433. if (empty($options['error'])) {
  434. $options['error'] = function (\Exception $e, $item, $data) {
  435. return array(
  436. 'error' => \Fuel::$env == \Fuel::DEVELOPMENT ? $e->getMessage() : __('Something went wrong. Please refresh your browser window and try again. Contact your developer or Novius OS if the problem persists. We apologise for the inconvenience caused.'),
  437. );
  438. };
  439. }
  440. $json_response = array();
  441. $pk = $item->primary_key();
  442. // Will trigger cascade_save for media and wysiwyg
  443. try {
  444. foreach ($fields as $name => $config) {
  445. if (!empty($config['renderer']) && in_array($config['renderer'], array('Nos\Renderer_Text', 'Nos\Renderer_Empty'))) {
  446. continue;
  447. }
  448. $type = \Arr::get($config, 'form.type', null);
  449. if ($type == 'submit') {
  450. continue;
  451. }
  452. if (isset($config['dont_save']) && $config['dont_save']) {
  453. continue;
  454. }
  455. if (!empty($config['renderer']) && !$options['fieldset']->fields[$name]->before_save($item, $data)) {
  456. continue;
  457. }
  458. if (!empty($config['before_save']) && is_callable($config['before_save'])) {
  459. call_user_func($config['before_save'], $item, $data);
  460. } else {
  461. if ($type == 'checkbox' && empty($data[$name])) {
  462. $item->$name = \Arr::get($config, 'form.empty', null);
  463. } elseif (isset($data[$name]) && !in_array($name, $pk)) {
  464. $item->$name = $data[$name];
  465. }
  466. }
  467. }
  468. // Let behaviours do their job (publication for example)
  469. $item->event('form_processing', array($data, &$json_response));
  470. if (!empty($options['before_save']) && is_callable($options['before_save'])) {
  471. call_user_func($options['before_save'], $item, $data);
  472. }
  473. if (!empty($options['success']) && is_callable($options['success'])) {
  474. $item->save();
  475. $json_user = call_user_func($options['success'], $item, $data);
  476. $json_response = \Arr::merge($json_response, $json_user);
  477. } else {
  478. $item->save();
  479. $json_response['notify'] = __('OK, it’s done.');
  480. }
  481. foreach ($fields as $name => $config) {
  482. if (!empty($config['renderer']) && in_array($config['renderer'], array('Nos\Renderer_Text', 'Nos\Renderer_Empty'))) {
  483. continue;
  484. }
  485. $type = \Arr::get($config, 'form.type', null);
  486. if ($type == 'submit') {
  487. continue;
  488. }
  489. if (!empty($config['success']) && is_callable($config['success'])) {
  490. $json_field = call_user_func($config['success'], $item, $data);
  491. $json_response = \Arr::merge($json_response, $json_field);
  492. }
  493. }
  494. } catch (Exception $e) {
  495. \Log::exception($e, 'Error while saving current item '.$item->id.'. ');
  496. if (is_callable($options['error'])) {
  497. $json_response = call_user_func($options['error'], $e, $item, $data);
  498. } else {
  499. $json_response['error'] = $e->getMessage();
  500. }
  501. }
  502. return $json_response;
  503. }
  504. protected function isExpert($field_name)
  505. {
  506. return !\Session::user()->user_expert && \Arr::get($this->config_used, $field_name.'.expert', false);
  507. }
  508. public function isRestricted($field_name)
  509. {
  510. if ($this->isExpert($field_name)) {
  511. return true;
  512. }
  513. $show_when = \Arr::get($this->config_used, $field_name.'.show_when', false);
  514. if ($show_when === false || !is_callable($show_when)) {
  515. return false;
  516. }
  517. return !call_user_func($show_when, $this->instance);
  518. }
  519. public function getInstance()
  520. {
  521. return $this->instance;
  522. }
  523. }