diff options
Diffstat (limited to 'modules/forge')
20 files changed, 2080 insertions, 0 deletions
diff --git a/modules/forge/controllers/forge_demo.php b/modules/forge/controllers/forge_demo.php new file mode 100644 index 00000000..0521b939 --- /dev/null +++ b/modules/forge/controllers/forge_demo.php @@ -0,0 +1,83 @@ +<?php +/** + * Forge module demo controller. This controller should NOT be used in production. + * It is for demonstration purposes only! + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Forge_demo_Controller extends Controller { + + // Do not allow to run in production + const ALLOW_PRODUCTION = FALSE; + + public function index() + { + $profiler = new Profiler; + + $foods = array + ( + 'tacos' => array('tacos', FALSE), + 'burgers' => array('burgers', FALSE), + 'spaghetti' => array('spaghetti (checked)', TRUE), + 'cookies' => array('cookies (checked)', TRUE), + ); + + $form = new Forge(NULL, 'New User'); + + // Create each input, following this format: + // + // type($name)->attr(..)->attr(..); + // + $form->hidden('hideme')->value('hiddenz!'); + $form->input('email')->label(TRUE)->rules('required|valid_email'); + $form->input('username')->label(TRUE)->rules('required|length[5,32]'); + $form->password('password')->label(TRUE)->rules('required|length[5,32]'); + $form->password('confirm')->label(TRUE)->matches($form->password); + $form->checkbox('remember')->label('Remember Me'); + $form->checklist('foods')->label('Favorite Foods')->options($foods)->rules('required'); + $form->dropdown('state')->label('Home State')->options(locale_US::states())->rules('required'); + $form->dateselect('birthday')->label(TRUE)->minutes(15)->years(1950, date('Y')); + $form->submit('Save'); + + if ($form->validate()) + { + echo Kohana::debug($form->as_array()); + } + + echo $form->render(); + + // Using a custom template: + // echo $form->render('custom_view', TRUE); + // Inside the view access the inputs using $input_id->render(), ->label() etc + // + // To get the errors use $input_id_errors. + // Set the error format with $form->error_format('<div>{message}</div>'); + // Defaults to <p class="error">{message}</p> + // + // Examples: + // echo $username->render(); echo $password_errors; + } + + public function upload() + { + $profiler = new Profiler; + + $form = new Forge; + $form->input('hello')->label(TRUE); + $form->upload('file', TRUE)->label(TRUE)->rules('required|size[200KB]|allow[jpg,png,gif]'); + $form->submit('Upload'); + + if ($form->validate()) + { + echo Kohana::debug($form->as_array()); + } + + echo $form->render(); + } + +} // End Forge Demo Controller diff --git a/modules/forge/i18n/en_US/forge.php b/modules/forge/i18n/en_US/forge.php new file mode 100644 index 00000000..8bcc677c --- /dev/null +++ b/modules/forge/i18n/en_US/forge.php @@ -0,0 +1,7 @@ +<?php + +$lang = array +( + 'invalid_input' => 'Error loading %s: All inputs must be constructed with a name', + 'unknown_input' => 'Unable to find a Forge input class for: %s', +);
\ No newline at end of file diff --git a/modules/forge/i18n/es_ES/forge.php b/modules/forge/i18n/es_ES/forge.php new file mode 100644 index 00000000..71f403f5 --- /dev/null +++ b/modules/forge/i18n/es_ES/forge.php @@ -0,0 +1,7 @@ +<?php + +$lang = array +( + 'invalid_input' => 'Error cargando %s: todos los elementos input tienen que ser construidos con una etiqueta name', + 'unknown_input' => 'Imposible encontrar una clase de forge Forge para el elemento: %s', +);
\ No newline at end of file diff --git a/modules/forge/i18n/ru_RU/forge.php b/modules/forge/i18n/ru_RU/forge.php new file mode 100644 index 00000000..e9462ef5 --- /dev/null +++ b/modules/forge/i18n/ru_RU/forge.php @@ -0,0 +1,7 @@ +<?php + +$lang = array +( + 'invalid_input' => 'Ошибка при загрузке %s: Все поля ввода должны создаваться с именем', + 'unknown_input' => 'Не удалось найти класс Forge для поля ввода: %s', +);
\ No newline at end of file diff --git a/modules/forge/libraries/Forge.php b/modules/forge/libraries/Forge.php new file mode 100644 index 00000000..04b3640a --- /dev/null +++ b/modules/forge/libraries/Forge.php @@ -0,0 +1,313 @@ +<?php +/** + * FORGE (FORm GEneration) library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Forge_Core { + + // Template variables + protected $template = array + ( + 'title' => '', + 'class' => '', + 'open' => '', + 'close' => '', + ); + + // Form attributes + protected $attr = array(); + + // Form inputs and hidden inputs + public $inputs = array(); + public $hidden = array(); + + // Error message format, only used with custom templates + public $error_format = '<p class="error">{message}</p>'; + public $newline_char = "\n"; + + /** + * Form constructor. Sets the form action, title, method, and attributes. + * + * @return void + */ + public function __construct($action = NULL, $title = '', $method = NULL, $attr = array()) + { + // Set form attributes + $this->attr['action'] = $action; + $this->attr['method'] = empty($method) ? 'post' : $method; + + // Set template variables + $this->template['title'] = $title; + + // Empty attributes sets the class to "form" + empty($attr) and $attr = array('class' => 'form'); + + // String attributes is the class name + is_string($attr) and $attr = array('class' => $attr); + + // Extend the template with the attributes + $this->attr += $attr; + } + + /** + * Magic __get method. Returns the specified form element. + * + * @param string unique input name + * @return object + */ + public function __get($key) + { + if (isset($this->inputs[$key])) + { + return $this->inputs[$key]; + } + elseif (isset($this->hidden[$key])) + { + return $this->hidden[$key]; + } + } + + /** + * Magic __call method. Creates a new form element object. + * + * @throws Kohana_Exception + * @param string input type + * @param string input name + * @return object + */ + public function __call($method, $args) + { + // Class name + $input = 'Form_'.ucfirst($method); + + // Create the input + switch (count($args)) + { + case 1: + $input = new $input($args[0]); + break; + case 2: + $input = new $input($args[0], $args[1]); + break; + default: + throw new Kohana_Exception('forge.invalid_input', $input); + } + + if ( ! ($input instanceof Form_Input) AND ! ($input instanceof Forge)) + throw new Kohana_Exception('forge.unknown_input', get_class($input)); + + $input->method = $this->attr['method']; + + if ($name = $input->name) + { + // Assign by name + if ($method == 'hidden') + { + $this->hidden[$name] = $input; + } + else + { + $this->inputs[$name] = $input; + } + } + else + { + // No name, these are unretrievable + $this->inputs[] = $input; + } + + return $input; + } + + /** + * Set a form attribute. This method is chainable. + * + * @param string|array attribute name, or an array of attributes + * @param string attribute value + * @return object + */ + public function set_attr($key, $val = NULL) + { + if (is_array($key)) + { + // Merge the new attributes with the old ones + $this->attr = array_merge($this->attr, $key); + } + else + { + // Set the new attribute + $this->attr[$key] = $val; + } + + return $this; + } + + /** + * Validates the form by running each inputs validation rules. + * + * @return bool + */ + public function validate() + { + $status = TRUE; + + $inputs = array_merge($this->hidden, $this->inputs); + + foreach ($inputs as $input) + { + if ($input->validate() == FALSE) + { + $status = FALSE; + } + } + + return $status; + } + + /** + * Returns the form as an array of input names and values. + * + * @return array + */ + public function as_array() + { + $data = array(); + foreach (array_merge($this->hidden, $this->inputs) as $input) + { + if (is_object($input->name)) // It's a Forge_Group object (hopefully) + { + foreach ($input->inputs as $group_input) + { + if ($name = $group_input->name) + { + $data[$name] = $group_input->value; + } + } + } + else if (is_array($input->inputs)) + { + foreach ($input->inputs as $group_input) + { + if ($name = $group_input->name) + { + $data[$name] = $group_input->value; + } + } + } + else if ($name = $input->name) // It's a normal input + { + // Return only named inputs + $data[$name] = $input->value; + } + } + return $data; + } + + /** + * Changes the error message format. Your message formatting must + * contain a {message} placeholder. + * + * @throws Kohana_Exception + * @param string new message format + * @return void + */ + public function error_format($string = '') + { + if (strpos((string) $string, '{message}') === FALSE) + throw new Kohana_Exception('validation.error_format'); + + $this->error_format = $string; + } + + /** + * Creates the form HTML + * + * @param string form view template name + * @param boolean use a custom view + * @return string + */ + public function render($template = 'forge_template', $custom = FALSE) + { + // Load template + $form = new View($template); + + if ($custom) + { + // Using a custom view + + $data = array(); + foreach (array_merge($this->hidden, $this->inputs) as $input) + { + $data[$input->name] = $input; + + // Groups will never have errors, so skip them + if ($input instanceof Form_Group) + continue; + + // Compile the error messages for this input + $messages = ''; + $errors = $input->error_messages(); + if (is_array($errors) AND ! empty($errors)) + { + foreach ($errors as $error) + { + // Replace the message with the error in the html error string + $messages .= str_replace('{message}', $error, $this->error_format).$this->newline_char; + } + } + + $data[$input->name.'_errors'] = $messages; + } + + $form->set($data); + } + else + { + // Using a template view + + $form->set($this->template); + $hidden = array(); + if ( ! empty($this->hidden)) + { + foreach ($this->hidden as $input) + { + $hidden[$input->name] = $input->value; + } + } + + $form_type = 'open'; + // See if we need a multipart form + foreach ($this->inputs as $input) + { + if ($input instanceof Form_Upload) + { + $form_type = 'open_multipart'; + } + } + + // Set the form open and close + $form->open = form::$form_type(arr::remove('action', $this->attr), $this->attr, $hidden); + $form->close = form::close(); + + // Set the inputs + $form->inputs = $this->inputs; + } + + return $form; + } + + /** + * Returns the form HTML + */ + public function __toString() + { + return (string) $this->render(); + } + +} // End Forge
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Checkbox.php b/modules/forge/libraries/Form_Checkbox.php new file mode 100644 index 00000000..c015e437 --- /dev/null +++ b/modules/forge/libraries/Form_Checkbox.php @@ -0,0 +1,83 @@ +<?php +/** + * FORGE checkbox input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Checkbox_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'checkbox', + 'class' => 'checkbox', + 'value' => '1', + 'checked' => FALSE, + ); + + protected $protect = array('type'); + + public function __get($key) + { + if ($key == 'value') + { + // Return the value if the checkbox is checked + return $this->data['checked'] ? $this->data['value'] : NULL; + } + + return parent::__get($key); + } + + public function label($val = NULL) + { + if ($val === NULL) + { + // Do not display labels for checkboxes, labels wrap checkboxes + return ''; + } + else + { + $this->data['label'] = ($val === TRUE) ? utf8::ucwords(inflector::humanize($this->name)) : $val; + return $this; + } + } + + protected function html_element() + { + // Import the data + $data = $this->data; + + if (empty($data['checked'])) + { + // Not checked + unset($data['checked']); + } + else + { + // Is checked + $data['checked'] = 'checked'; + } + + if ($label = arr::remove('label', $data)) + { + // There must be one space before the text + $label = ' '.ltrim($label); + } + + return '<label>'.form::input($data).$label.'</label>'; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + // Makes the box checked if the value from POST is the same as the current value + $this->data['checked'] = ($this->input_value($this->name) == $this->data['value']); + } + +} // End Form Checkbox
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Checklist.php b/modules/forge/libraries/Form_Checklist.php new file mode 100644 index 00000000..6b1df490 --- /dev/null +++ b/modules/forge/libraries/Form_Checklist.php @@ -0,0 +1,92 @@ +<?php +/** + * FORGE checklist input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Checklist_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'type' => 'checkbox', + 'class' => 'checklist', + 'options' => array(), + ); + + protected $protect = array('name', 'type'); + + public function __construct($name) + { + $this->data['name'] = $name; + } + + public function __get($key) + { + if ($key == 'value') + { + // Return the currently checked values + $array = array(); + foreach ($this->data['options'] as $id => $opt) + { + // Return the options that are checked + ($opt[1] === TRUE) and $array[] = $id; + } + return $array; + } + + return parent::__get($key); + } + + public function render() + { + // Import base data + $base_data = $this->data; + + // Make it an array + $base_data['name'] .= '[]'; + + // Newline + $nl = "\n"; + + $checklist = '<ul class="'.arr::remove('class', $base_data).'">'.$nl; + foreach (arr::remove('options', $base_data) as $val => $opt) + { + // New set of input data + $data = $base_data; + + // Get the title and checked status + list ($title, $checked) = $opt; + + // Set the name, value, and checked status + $data['value'] = $val; + $data['checked'] = $checked; + + $checklist .= '<li><label>'.form::checkbox($data).' '.$title.'</label></li>'.$nl; + } + $checklist .= '</ul>'; + + return $checklist; + } + + protected function load_value() + { + foreach ($this->data['options'] as $val => $checked) + { + if ($input = $this->input_value($this->data['name'])) + { + $this->data['options'][$val][1] = in_array($val, $input); + } + else + { + $this->data['options'][$val][1] = FALSE; + } + } + } + +} // End Form Checklist
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Dateselect.php b/modules/forge/libraries/Form_Dateselect.php new file mode 100644 index 00000000..d531b3c8 --- /dev/null +++ b/modules/forge/libraries/Form_Dateselect.php @@ -0,0 +1,138 @@ +<?php +/** + * FORGE dateselect input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Dateselect_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'dropdown', + ); + + protected $protect = array('type'); + + // Precision for the parts, you can use @ to insert a literal @ symbol + protected $parts = array + ( + 'month' => array(), + 'day' => array(1), + 'year' => array(), + ' @ ', + 'hour' => array(), + ':', + 'minute' => array(5), + 'am_pm' => array(), + ); + + public function __construct($name) + { + // Set name + $this->data['name'] = $name; + + // Default to the current time + $this->data['value'] = time(); + } + + public function __call($method, $args) + { + if (isset($this->parts[substr($method, 0, -1)])) + { + // Set options for date generation + $this->parts[substr($method, 0, -1)] = $args; + return $this; + } + + return parent::__call($method, $args); + } + + public function html_element() + { + // Import base data + $data = $this->data; + + // Get the options and default selection + $time = $this->time_array(arr::remove('value', $data)); + + // No labels or values + unset($data['label']); + + $input = ''; + foreach ($this->parts as $type => $val) + { + if (is_int($type)) + { + // Just add the separators + $input .= $val; + continue; + } + + // Set this input name + $data['name'] = $this->data['name'].'['.$type.']'; + + // Set the selected option + $selected = $time[$type]; + + if ($type == 'am_pm') + { + // Options are static + $options = array('AM' => 'AM', 'PM' => 'PM'); + } + else + { + // minute(s), hour(s), etc + $type .= 's'; + + // Use the date helper to generate the options + $options = empty($val) ? date::$type() : call_user_func_array(array('date', $type), $val); + } + + $input .= form::dropdown($data, $options, $selected); + } + + return $input; + } + + protected function time_array($timestamp) + { + $time = array_combine + ( + array('month', 'day', 'year', 'hour', 'minute', 'am_pm'), + explode('--', date('n--j--Y--g--i--A', $timestamp)) + ); + + // Minutes should always be in 5 minute increments + $time['minute'] = num::round($time['minute'], current($this->parts['minute'])); + + return $time; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $time = $this->input_value($this->name); + + // Make sure all the required inputs keys are set + $time += $this->time_array(time()); + + $this->data['value'] = mktime + ( + date::adjust($time['hour'], $time['am_pm']), + $time['minute'], + 0, + $time['month'], + $time['day'], + $time['year'] + ); + } + +} // End Form Dateselect
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Dropdown.php b/modules/forge/libraries/Form_Dropdown.php new file mode 100644 index 00000000..ac810299 --- /dev/null +++ b/modules/forge/libraries/Form_Dropdown.php @@ -0,0 +1,78 @@ +<?php +/** + * FORGE dropdown input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Dropdown_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'dropdown', + ); + + protected $protect = array('type'); + + public function __get($key) + { + if ($key == 'value') + { + return $this->selected; + } + + return parent::__get($key); + } + + public function html_element() + { + // Import base data + $base_data = $this->data; + + unset($base_data['label']); + + // Get the options and default selection + $options = arr::remove('options', $base_data); + $selected = arr::remove('selected', $base_data); + + return form::dropdown($base_data, $options, $selected); + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $this->data['selected'] = $this->input_value($this->name); + } + + public function validate() + { + // Validation has already run + if (is_bool($this->is_valid)) + return $this->is_valid; + + if ($this->input_value() == FALSE) + { + // No data to validate + return $this->is_valid = FALSE; + } + + // Load the submitted value + $this->load_value(); + + if ( ! array_key_exists($this->value, $this->data['options'])) + { + // Value does not exist in the options + return $this->is_valid = FALSE; + } + + return parent::validate(); + } + +} // End Form Dropdown
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Group.php b/modules/forge/libraries/Form_Group.php new file mode 100644 index 00000000..0c6dd100 --- /dev/null +++ b/modules/forge/libraries/Form_Group.php @@ -0,0 +1,89 @@ +<?php +/** + * FORGE group library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Group_Core extends Forge { + + protected $data = array + ( + 'type' => 'group', + 'name' => '', + 'class' => 'group', + 'label' => '', + 'message' => '' + ); + + // Input method + public $method; + + public function __construct($name = NULL, $class = 'group') + { + $this->data['name'] = $name; + $this->data['class'] = $class; + + // Set dummy data so we don't get errors + $this->attr['action'] = ''; + $this->attr['method'] = 'post'; + } + + public function __get($key) + { + if ($key == 'type' || $key == 'name') + { + return $this->data[$key]; + } + return parent::__get($key); + } + + public function __set($key, $val) + { + if ($key == 'method') + { + $this->attr['method'] = $val; + } + $this->$key = $val; + } + + public function label($val = NULL) + { + if ($val === NULL) + { + if ($label = $this->data['label']) + { + return $this->data['label']; + } + } + else + { + $this->data['label'] = ($val === TRUE) ? ucwords(inflector::humanize($this->data['name'])) : $val; + return $this; + } + } + + public function message($val = NULL) + { + if ($val === NULL) + { + return $this->data['message']; + } + else + { + $this->data['message'] = $val; + return $this; + } + } + + public function render() + { + // No Sir, we don't want any html today thank you + return; + } + +} // End Form Group
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Hidden.php b/modules/forge/libraries/Form_Hidden.php new file mode 100644 index 00000000..4dcbcc27 --- /dev/null +++ b/modules/forge/libraries/Form_Hidden.php @@ -0,0 +1,25 @@ +<?php +/** + * FORGE hidden input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Hidden_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'value' => '', + ); + + public function render() + { + return form::hidden($this->data['name'], $this->data['value']); + } + +} // End Form Hidden
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Input.php b/modules/forge/libraries/Form_Input.php new file mode 100644 index 00000000..7dfc974d --- /dev/null +++ b/modules/forge/libraries/Form_Input.php @@ -0,0 +1,555 @@ +<?php +/** + * FORGE base input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Input_Core { + + // Input method + public $method; + + // Element data + protected $data = array + ( + 'type' => 'text', + 'class' => 'textbox', + 'value' => '' + ); + + // Protected data keys + protected $protect = array(); + + // Validation rules, matches, and callbacks + protected $rules = array(); + protected $matches = array(); + protected $callbacks = array(); + + // Validation check + protected $is_valid; + + // Errors + protected $errors = array(); + protected $error_messages = array(); + + /** + * Sets the input element name. + */ + public function __construct($name) + { + $this->data['name'] = $name; + } + + /** + * Sets form attributes, or return rules. + */ + public function __call($method, $args) + { + if ($method == 'rules') + { + if (empty($args)) + return $this->rules; + + // Set rules and action + $rules = $args[0]; + $action = substr($rules, 0, 1); + + if (in_array($action, array('-', '+', '='))) + { + // Remove the action from the rules + $rules = substr($rules, 1); + } + else + { + // Default action is append + $action = ''; + } + + $this->add_rules(explode('|', $rules), $action); + } + elseif ($method == 'name') + { + // Do nothing. The name should stay static once it is set. + } + else + { + $this->data[$method] = $args[0]; + } + + return $this; + } + + /** + * Returns form attributes. + * + * @param string attribute name + * @return string + */ + public function __get($key) + { + if (isset($this->data[$key])) + { + return $this->data[$key]; + } + } + + /** + * Sets a form element that this element must match the value of. + * + * @chainable + * @param object another Forge input + * @return object + */ + public function matches($input) + { + if ( ! in_array($input, $this->matches, TRUE)) + { + $this->matches[] = $input; + } + + return $this; + } + + /** + * Sets a callback method as a rule for this input. + * + * @chainable + * @param callback + * @return object + */ + public function callback($callback) + { + if ( ! in_array($callback, $this->callbacks, TRUE)) + { + $this->callbacks[] = $callback; + } + + return $this; + } + + /** + * Sets or returns the input label. + * + * @chainable + * @param string label to set + * @return string|object + */ + public function label($val = NULL) + { + if ($val === NULL) + { + if (isset($this->data['name']) AND isset($this->data['label'])) + { + return form::label($this->data['name'], $this->data['label']); + } + return FALSE; + } + else + { + $this->data['label'] = ($val === TRUE) ? utf8::ucwords(inflector::humanize($this->name)) : $val; + return $this; + } + } + + /** + * Set or return the error message. + * + * @chainable + * @param string error message + * @return strong|object + */ + public function message($val = NULL) + { + if ($val === NULL) + { + if (isset($this->data['message'])) + return $this->data['message']; + } + else + { + $this->data['message'] = $val; + return $this; + } + } + + /** + * Runs validation and returns the element HTML. + * + * @return string + */ + public function render() + { + // Make sure validation runs + $this->validate(); + + return $this->html_element(); + } + + /** + * Returns the form input HTML. + * + * @return string + */ + protected function html_element() + { + $data = $this->data; + + unset($data['label']); + unset($data['message']); + + return form::input($data); + } + + /** + * Replace, remove, or append rules. + * + * @param array rules to change + * @param string action to use: replace, remove, append + */ + protected function add_rules( array $rules, $action) + { + if ($action === '=') + { + // Just replace the rules + $this->rules = $rules; + return; + } + + foreach ($rules as $rule) + { + if ($action === '-') + { + if (($key = array_search($rule, $this->rules)) !== FALSE) + { + // Remove the rule + unset($this->rules[$key]); + } + } + else + { + if ( ! in_array($rule, $this->rules)) + { + if ($action == '+') + { + array_unshift($this->rules, $rule); + } + else + { + $this->rules[] = $rule; + } + } + } + } + } + + /** + * Add an error to the input. + * + * @chainable + * @return object + */ + public function add_error($key, $val) + { + if ( ! isset($this->errors[$key])) + { + $this->errors[$key] = $val; + } + + return $this; + } + + /** + * Set or return the error messages. + * + * @chainable + * @param string|array failed validation function, or an array of messages + * @param string error message + * @return object|array + */ + public function error_messages($func = NULL, $message = NULL) + { + // Set custom error messages + if ( ! empty($func)) + { + if (is_array($func)) + { + // Replace all + $this->error_messages = $func; + } + else + { + if (empty($message)) + { + // Single error, replaces all others + $this->error_messages = $func; + } + else + { + // Add custom error + $this->error_messages[$func] = $message; + } + } + return $this; + } + + // Make sure validation runs + is_null($this->is_valid) and $this->validate(); + + // Return single error + if ( ! is_array($this->error_messages) AND ! empty($this->errors)) + return array($this->error_messages); + + $messages = array(); + foreach ($this->errors as $func => $args) + { + if (is_string($args)) + { + $error = $args; + } + else + { + // Force args to be an array + $args = is_array($args) ? $args : array(); + + // Add the label or name to the beginning of the args + array_unshift($args, $this->label ? utf8::strtolower($this->label) : $this->name); + + if (isset($this->error_messages[$func])) + { + // Use custom error message + $error = vsprintf($this->error_messages[$func], $args); + } + else + { + // Get the proper i18n entry, very hacky but it works + switch ($func) + { + case 'valid_url': + case 'valid_email': + case 'valid_ip': + // Fetch an i18n error message + $error = Kohana::lang('validation.'.$func, $args); + break; + case substr($func, 0, 6) === 'valid_': + // Strip 'valid_' from func name + $func = (substr($func, 0, 6) === 'valid_') ? substr($func, 6) : $func; + case 'alpha': + case 'alpha_dash': + case 'digit': + case 'numeric': + // i18n strings have to be inserted into valid_type + $args[] = Kohana::lang('validation.'.$func); + $error = Kohana::lang('validation.valid_type', $args); + break; + default: + $error = Kohana::lang('validation.'.$func, $args); + } + } + } + + // Add error to list + $messages[] = $error; + } + + return $messages; + } + + /** + * Get the global input value. + * + * @return string|bool + */ + protected function input_value($name = array()) + { + // Get the Input instance + $input = Input::instance(); + + // Fetch the method for this object + $method = $this->method; + + return $input->$method($name, NULL); + } + + /** + * Load the value of the input, if form data is present. + * + * @return void + */ + protected function load_value() + { + if (is_bool($this->is_valid)) + return; + + if ($name = $this->name) + { + // Load POSTed value, but only for named inputs + $this->data['value'] = $this->input_value($name); + } + + if (is_string($this->data['value'])) + { + // Trim string values + $this->data['value'] = trim($this->data['value']); + } + } + + /** + * Validate this input based on the set rules. + * + * @return bool + */ + public function validate() + { + // Validation has already run + if (is_bool($this->is_valid)) + return $this->is_valid; + + // No data to validate + if ($this->input_value() == FALSE) + return $this->is_valid = FALSE; + + // Load the submitted value + $this->load_value(); + + // No rules to validate + if (count($this->rules) == 0 AND count($this->matches) == 0 AND count($this->callbacks) == 0) + return $this->is_valid = TRUE; + + if ( ! empty($this->rules)) + { + foreach ($this->rules as $rule) + { + if (($offset = strpos($rule, '[')) !== FALSE) + { + // Get the args + $args = preg_split('/, ?/', trim(substr($rule, $offset), '[]')); + + // Remove the args from the rule + $rule = substr($rule, 0, $offset); + } + + if (substr($rule, 0, 6) === 'valid_' AND method_exists('valid', substr($rule, 6))) + { + $func = substr($rule, 6); + + if ($this->value AND ! valid::$func($this->value)) + { + $this->errors[$rule] = TRUE; + } + } + elseif (method_exists($this, 'rule_'.$rule)) + { + // The rule function is always prefixed with rule_ + $rule = 'rule_'.$rule; + + if (isset($args)) + { + // Manually call up to 2 args for speed + switch (count($args)) + { + case 1: + $this->$rule($args[0]); + break; + case 2: + $this->$rule($args[0], $args[1]); + break; + default: + call_user_func_array(array($this, $rule), $args); + break; + } + } + else + { + // Just call the rule + $this->$rule(); + } + + // Prevent args from being re-used + unset($args); + } + else + { + throw new Kohana_Exception('validation.invalid_rule', $rule); + } + + // Stop when an error occurs + if ( ! empty($this->errors)) + break; + } + } + + if ( ! empty($this->matches)) + { + foreach ($this->matches as $input) + { + if ($this->value != $input->value) + { + // Field does not match + $this->errors['matches'] = array($input->label ? utf8::strtolower($input->label) : $input->name); + break; + } + } + } + + if ( ! empty($this->callbacks)) + { + foreach ($this->callbacks as $callback) + { + call_user_func($callback, $this); + + // Stop when an error occurs + if ( ! empty($this->errors)) + break; + } + } + + // If there are errors, validation failed + return $this->is_valid = empty($this->errors); + } + + /** + * Validate required. + */ + protected function rule_required() + { + if ($this->value === '' OR $this->value === NULL) + { + $this->errors['required'] = TRUE; + } + } + + /** + * Validate length. + */ + protected function rule_length($min, $max = NULL) + { + // Get the length, return if zero + if (($length = utf8::strlen($this->value)) === 0) + return; + + if ($max == NULL) + { + if ($length != $min) + { + $this->errors['exact_length'] = array($min); + } + } + else + { + if ($length < $min) + { + $this->errors['min_length'] = array($min); + } + elseif ($length > $max) + { + $this->errors['max_length'] = array($max); + } + } + } + +} // End Form Input
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Password.php b/modules/forge/libraries/Form_Password.php new file mode 100644 index 00000000..ac4dd8ae --- /dev/null +++ b/modules/forge/libraries/Form_Password.php @@ -0,0 +1,23 @@ +<?php +/** + * FORGE password input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Password_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'password', + 'class' => 'password', + 'value' => '', + ); + + protected $protect = array('type'); + +} // End Form Password
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Phonenumber.php b/modules/forge/libraries/Form_Phonenumber.php new file mode 100644 index 00000000..e30f47b1 --- /dev/null +++ b/modules/forge/libraries/Form_Phonenumber.php @@ -0,0 +1,98 @@ +<?php +/** + * FORGE phone number input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Phonenumber_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'phone_number', + ); + + protected $protect = array('type'); + + // Precision for the parts, you can use @ to insert a literal @ symbol + protected $parts = array + ( + 'area_code' => '', + 'exchange' => '', + 'last_four' => '', + ); + + public function __construct($name) + { + // Set name + $this->data['name'] = $name; + } + + public function __call($method, $args) + { + if (isset($this->parts[substr($method, 0, -1)])) + { + // Set options for date generation + $this->parts[substr($method, 0, -1)] = $args; + return $this; + } + + return parent::__call($method, $args); + } + + public function html_element() + { + // Import base data + $data = $this->data; + + $input = ''; + foreach ($this->parts as $type => $val) + { + isset($data['value']) OR $data['value'] = ''; + $temp = $data; + $temp['name'] = $this->data['name'].'['.$type.']'; + $offset = (strlen($data['value']) == 10) ? 0 : 3; + switch ($type) + { + case 'area_code': + if (strlen($data['value']) == 10) + { + $temp['value'] = substr($data['value'], 0, 3); + } + else + $temp['value'] = ''; + $temp['class'] = 'area_code'; + $input .= form::input(array_merge(array('value' => $val), $temp)).'-'; + break; + case 'exchange': + $temp['value'] = substr($data['value'], (3-$offset), 3); + $temp['class'] = 'exchange'; + $input .= form::input(array_merge(array('value' => $val), $temp)).'-'; + break; + case 'last_four': + $temp['value'] = substr($data['value'], (6-$offset), 4); + $temp['class'] = 'last_four'; + $input .= form::input(array_merge(array('value' => $val), $temp)); + break; + } + + } + + return $input; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $data = $this->input_value($this->name, $this->data['name']); + + $this->data['value'] = $data['area_code'].$data['exchange'].$data['last_four']; + } +} // End Form Phonenumber
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Radio.php b/modules/forge/libraries/Form_Radio.php new file mode 100644 index 00000000..f88632f2 --- /dev/null +++ b/modules/forge/libraries/Form_Radio.php @@ -0,0 +1,22 @@ +<?php +/** + * FORGE radio input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Radio_Core extends Form_Checkbox { + + protected $data = array + ( + 'type' => 'radio', + 'class' => 'radio', + 'value' => '1', + 'checked' => FALSE, + ); + +} // End Form_Radio
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Submit.php b/modules/forge/libraries/Form_Submit.php new file mode 100644 index 00000000..527580c9 --- /dev/null +++ b/modules/forge/libraries/Form_Submit.php @@ -0,0 +1,41 @@ +<?php +/** + * FORGE submit input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Submit_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'submit', + 'class' => 'submit' + ); + + protected $protect = array('type'); + + public function __construct($value) + { + $this->data['value'] = $value; + } + + public function render() + { + $data = $this->data; + unset($data['label']); + + return form::button($data); + } + + public function validate() + { + // Submit buttons do not need to be validated + return $this->is_valid = TRUE; + } + +} // End Form Submit
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Textarea.php b/modules/forge/libraries/Form_Textarea.php new file mode 100644 index 00000000..f6d28fd0 --- /dev/null +++ b/modules/forge/libraries/Form_Textarea.php @@ -0,0 +1,31 @@ +<?php +/** + * FORGE textarea input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Textarea_Core extends Form_Input { + + protected $data = array + ( + 'class' => 'textarea', + 'value' => '', + ); + + protected $protect = array('type'); + + protected function html_element() + { + $data = $this->data; + + unset($data['label']); + + return form::textarea($data); + } + +} // End Form Textarea
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Upload.php b/modules/forge/libraries/Form_Upload.php new file mode 100644 index 00000000..dce8816e --- /dev/null +++ b/modules/forge/libraries/Form_Upload.php @@ -0,0 +1,187 @@ +<?php +/** + * FORGE upload input library. + * + * $Id$ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Upload_Core extends Form_Input { + + protected $data = array + ( + 'class' => 'upload', + 'value' => '', + ); + + protected $protect = array('type', 'label', 'value'); + + // Upload data + protected $upload; + + // Upload directory and filename + protected $directory; + protected $filename; + + public function __construct($name, $filename = FALSE) + { + parent::__construct($name); + + if ( ! empty($_FILES[$name])) + { + if (empty($_FILES[$name]['tmp_name']) OR is_uploaded_file($_FILES[$name]['tmp_name'])) + { + // Cache the upload data in this object + $this->upload = $_FILES[$name]; + + // Hack to allow file-only inputs, where no POST data is present + $_POST[$name] = $this->upload['name']; + + // Set the filename + $this->filename = empty($filename) ? FALSE : $filename; + } + else + { + // Attempt to delete the invalid file + is_writable($_FILES[$name]['tmp_name']) and unlink($_FILES[$name]['tmp_name']); + + // Invalid file upload, possible hacking attempt + unset($_FILES[$name]); + } + } + } + + /** + * Sets the upload directory. + * + * @param string upload directory + * @return void + */ + public function directory($dir = NULL) + { + // Use the global upload directory by default + empty($dir) and $dir = Kohana::config('upload.directory'); + + // Make the path asbolute and normalize it + $directory = str_replace('\\', '/', realpath($dir)).'/'; + + // Make sure the upload director is valid and writable + if ($directory === '/' OR ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('upload.not_writable', $dir); + + $this->directory = $directory; + } + + public function validate() + { + // The upload directory must always be set + empty($this->directory) and $this->directory(); + + // By default, there is no uploaded file + $filename = ''; + + if ($status = parent::validate() AND $this->upload['error'] === UPLOAD_ERR_OK) + { + // Set the filename to the original name + $filename = $this->upload['name']; + + if (Kohana::config('upload.remove_spaces')) + { + // Remove spaces, due to global upload configuration + $filename = preg_replace('/\s+/', '_', $this->data['value']); + } + + if (file_exists($filepath = $this->directory.$filename)) + { + if ($this->filename !== TRUE OR ! is_writable($filepath)) + { + // Prefix the file so that the filename is unique + $filepath = $this->directory.'uploadfile-'.uniqid(time()).'-'.$this->upload['name']; + } + } + + // Move the uploaded file to the upload directory + move_uploaded_file($this->upload['tmp_name'], $filepath); + } + + if ( ! empty($_POST[$this->data['name']])) + { + // Reset the POST value to the new filename + $this->data['value'] = $_POST[$this->data['name']] = empty($filepath) ? '' : $filepath; + } + + return $status; + } + + protected function rule_required() + { + if (empty($this->upload) OR $this->upload['error'] === UPLOAD_ERR_NO_FILE) + { + $this->errors['required'] = TRUE; + } + } + + public function rule_allow() + { + if (empty($this->upload['tmp_name']) OR count($types = func_get_args()) == 0) + return; + + if (($mime = file::mime($this->upload['tmp_name'])) === FALSE) + { + // Trust the browser + $mime = $this->upload['type']; + } + + // Allow nothing by default + $allow = FALSE; + + foreach ($types as $type) + { + // Load the mime types + $type = Kohana::config('mimes.'.$type); + + if (is_array($type) AND in_array($mime, $type)) + { + // Type is valid + $allow = TRUE; + break; + } + } + + if ($allow === FALSE) + { + $this->errors['invalid_type'] = TRUE; + } + } + + public function rule_size($size) + { + // Skip the field if it is empty + if (empty($this->upload) OR $this->upload['error'] === UPLOAD_ERR_NO_FILE) + return; + + $bytes = (int) $size; + + switch (substr($size, -2)) + { + case 'GB': $bytes *= 1024; + case 'MB': $bytes *= 1024; + case 'KB': $bytes *= 1024; + default: break; + } + + if (empty($this->upload['size']) OR $this->upload['size'] > $bytes) + { + $this->errors['max_size'] = array($size); + } + } + + protected function html_element() + { + return form::upload($this->data); + } + +} // End Form Upload diff --git a/modules/forge/models/user_edit.php b/modules/forge/models/user_edit.php new file mode 100644 index 00000000..c8cf9594 --- /dev/null +++ b/modules/forge/models/user_edit.php @@ -0,0 +1,132 @@ +<?php + +class User_Edit_Model extends User_Model { + + // Overload the class + protected $class = 'user'; + + // Forge instance + protected $form; + + public function __construct($action, $title, $id = FALSE) + { + // Load the user + parent::__construct($id); + + // Create the form + $this->form = new Forge($action, $title); + + $this->form->input('username')->label(TRUE)->rules('required|length[5,32]')->value($this->object->username); + $this->form->input('email')->label(TRUE)->rules('required|length[5,127]|valid_email')->value($this->object->email); + $this->form->password('password')->label(TRUE)->rules('length[5,64]'); + $this->form->password('confirm')->label(TRUE)->matches($this->form->password); + + // Make sure that the username does not already exist + $this->form->username->callback(array($this, 'is_existing_user')); + + if ($this->object->id == 0) + { + // Password fields are required for new users + $this->form->password->rules('+required'); + } + + // // Find all roles + // $roles = new Role_Model; + // $roles = $roles->find(ALL); + // + // $options = array(); + // foreach ($roles as $role) + // { + // // Add each role to the options + // $options[$role->name] = isset($this->roles[$role->id]); + // } + // + // // Create a checklist of roles + // $this->form->checklist('roles')->options($options)->label(TRUE); + + // Add the save button + $this->form->submit('Save'); + } + + public function is_existing_user($input) + { + if ($this->object->username == $input->value) + return TRUE; + + if (self::$db->count_records($this->table, array('username' => $input->value)) > 0) + { + $input->add_error(__FUNCTION__, 'The username <strong>'.$input->value.'</strong> is already in use.'); + return FALSE; + } + + return TRUE; + } + + public function save() + { + if ($this->form->validate() AND $data = $this->form->as_array()) + { + if (empty($data['password'])) + { + // Remove the empty password so it's not reset + unset($data['password'], $data['confirm']); + } + + // Need to set this before saving + $new_user = ($this->object->id == 0); + + // Remove the roles from data + isset($data['roles']) and $roles = arr::remove('roles', $data); + + foreach ($data as $field => $val) + { + // Set object data from the form + $this->$field = $val; + } + + if ($status = parent::save()) + { + // if ($new_user) + // { + // foreach ($roles as $role) + // { + // // Add the user roles + // $this->add_role($role); + // } + // } + // else + // { + // foreach (array_diff($this->roles, $roles) as $role) + // { + // // Remove roles that were deactivated + // $this->remove_role($role); + // } + // + // foreach (array_diff($roles, $this->roles) as $role) + // { + // // Add new roles + // $this->add_role($role); + // } + // } + } + + // Return the save status + return $status; + } + + return FALSE; + } + + public function render() + { + // Proxy to form html + return $this->form->render(); + } + + public function __toString() + { + // Proxy to form html + return $this->form->render(); + } + +} // End User Edit Model
\ No newline at end of file diff --git a/modules/forge/views/forge_template.php b/modules/forge/views/forge_template.php new file mode 100644 index 00000000..d71b16f7 --- /dev/null +++ b/modules/forge/views/forge_template.php @@ -0,0 +1,69 @@ +<?php echo $open; ?> +<table class="<?php echo $class ?>"> +<?php if ($title != ''): ?> +<caption><?php echo $title ?></caption> +<?php endif ?> +<?php +foreach ($inputs as $input): + +$sub_inputs = array(); +if ($input->type == 'group'): + $sub_inputs = $input->inputs; + +?> +<tr> +<th colspan="2"><?php echo $input->label() ?></th> +</tr> +<?php + + if ($message = $input->message()): + +?> +<tr> +<td colspan="2"><p class="group_message"><?php echo $message ?></p></td> +</tr> +<?php + + endif; + +else: + $sub_inputs = array($input); +endif; + +foreach ($sub_inputs as $input): + +?> +<tr> +<th><?php echo $input->label() ?></th> +<td> +<?php + +echo $input->render(); + +if ($message = $input->message()): + +?> +<p class="message"><?php echo $message ?></p> +<?php + +endif; + +foreach ($input->error_messages() as $error): + +?> +<p class="error"><?php echo $error ?></p> +<?php + +endforeach; + +?> +</td> +</tr> +<?php + +endforeach; + +endforeach; +?> +</table> +<?php echo $close ?>
\ No newline at end of file |