summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/forge/controllers/forge_demo.php83
-rw-r--r--modules/forge/i18n/en_US/forge.php7
-rw-r--r--modules/forge/i18n/es_ES/forge.php7
-rw-r--r--modules/forge/i18n/ru_RU/forge.php7
-rw-r--r--modules/forge/libraries/Forge.php313
-rw-r--r--modules/forge/libraries/Form_Checkbox.php83
-rw-r--r--modules/forge/libraries/Form_Checklist.php92
-rw-r--r--modules/forge/libraries/Form_Dateselect.php138
-rw-r--r--modules/forge/libraries/Form_Dropdown.php78
-rw-r--r--modules/forge/libraries/Form_Group.php89
-rw-r--r--modules/forge/libraries/Form_Hidden.php25
-rw-r--r--modules/forge/libraries/Form_Input.php555
-rw-r--r--modules/forge/libraries/Form_Password.php23
-rw-r--r--modules/forge/libraries/Form_Phonenumber.php98
-rw-r--r--modules/forge/libraries/Form_Radio.php22
-rw-r--r--modules/forge/libraries/Form_Submit.php41
-rw-r--r--modules/forge/libraries/Form_Textarea.php31
-rw-r--r--modules/forge/libraries/Form_Upload.php187
-rw-r--r--modules/forge/models/user_edit.php132
-rw-r--r--modules/forge/views/forge_template.php69
-rw-r--r--modules/mptt/libraries/MPTT.php1187
21 files changed, 3267 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
diff --git a/modules/mptt/libraries/MPTT.php b/modules/mptt/libraries/MPTT.php
new file mode 100644
index 00000000..84182a90
--- /dev/null
+++ b/modules/mptt/libraries/MPTT.php
@@ -0,0 +1,1187 @@
+<?php
+
+/**
+* File: libraries/MPTT.php
+*
+* An implementation of Joe Celko's Nested Sets as a Kohana model with ORM support.
+*
+*
+* Many thanks to
+* * Thunder who supplied a similar class for Code Igniter and gave permission to me
+* to release it under whatever license I deem necessary:
+* http://www.codeigniter.com/wiki/Nested_Sets/
+* * mpatek, his class was the initial start
+* http://bakery.cakephp.org/articles/view/modified-preorder-tree-traversal-component
+* * Propel, for inspiring some methods and the parent_id and scope stuff
+*
+* MPTT class
+* author - dlib
+* license - BSD
+*/
+
+class MPTT extends ORM {
+
+ public $children;
+
+ public $parent;
+
+ protected $left_column = 'lft';
+
+ protected $right_column = 'rgt';
+
+ protected $parent_column = 'parent_id';
+
+ protected $scope_column = 'scope';
+
+ protected $_scope = 1;
+
+ /**
+ * Patch through to ORM construct
+ *
+ * @return void
+ */
+ public function __construct($id = FALSE)
+ {
+ parent::__construct($id);
+
+ if (!empty($id))
+ {
+ //Set the right scope on new objects
+ $scope_column=$this->scope_column;
+ $this->set_scope($this->$scope_column);
+ $this->where='';
+ }
+
+
+ }
+
+ //////////////////////////////////////////
+ // Lock functions
+ //////////////////////////////////////////
+
+ /**
+ * Locks tree table
+ * This is a straight write lock - the database blocks until the previous lock is released
+ */
+ protected function lock_tree($aliases = array())
+ {
+ $sql = "LOCK TABLE " . $this->table . " WRITE";
+ return self::$db->query($sql);
+ }
+
+ /**
+ * Unlocks tree table
+ * Releases previous lock
+ */
+ protected function unlock_tree()
+ {
+ $sql = "UNLOCK TABLES";
+ return self::$db->query($sql);
+ }
+
+ /**
+ * Bit of an odd function since the node is supplied
+ * supply a node object and it will return a whole bunch of descendants
+ * @return tree object
+ * @param $root_node mixed
+
+ */
+ public function get_tree($root_node)
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ if(is_object($root_node))
+ {
+ $root=$root_node;
+ }
+ else
+ {
+ $root=$this->get_node($root_node);
+ }
+
+ $children=$this->new_node();
+ $children->where('`'.$lft_col.'`>'.$root->$lft_col.' AND `'.$rgt_col.'`<'.$root->$rgt_col);
+ $children->where($this->scope_column,$this->get_scope());
+ $children->orderby($lft_col,'ASC');
+ $children=$children->find_all();
+
+ if(count($children)>0)
+ {
+
+ $parent=$root;
+ $parent->children=array();
+
+ foreach($children as $child_data)
+ {
+
+ $child=$child_data;
+ $child->children=array();
+ $child->parent=$parent;
+ $child->scope=$this->get_scope();
+
+ while(!$this->_is_descendant_of($child, $parent))
+ {
+ $parent = $parent->parent;
+ }
+
+ $parent->children[]=$child;
+ $parent=$child;
+
+ }
+ }
+
+ return $root;
+ }
+ /*
+ * *****************
+ * Retrieval methods
+ * *****************
+ */
+ /**
+ * Current object will obtain its children
+ * @see http://trac.symfony-project.com/wiki/sfPropelActAsNestedSetBehaviorPlugin
+ * @see http://www.phpriot.com/articles/nested-trees-2/5
+ * @return
+ */
+ public function get_children($return=false)
+ {
+ if(!$this->has_descendants())
+ return false;
+
+ $parent_id=$this->id;
+
+ $this->children=array();
+ $children=$this->new_node();
+ $children->where($this->parent_column,$parent_id);
+ $children->where($this->scope_column,$this->get_scope());
+ $children->orderby($this->left_column);
+
+ foreach($children->find_all() as $child)
+ {
+ $child->parent=$this;
+
+ $this->children[]=$child;
+ }
+ if($return== true)
+ {
+ return $this->children;
+ }
+ return $this;
+ }
+
+ /**
+ * The current object will obtain all descendants
+ * @return ORM object
+ * @param $node_id Object
+ */
+ public function get_descendants()
+ {
+ if($this->has_descendants())
+ return $this->get_tree($this);
+ }
+ /**
+ * Return an array of all leaves in the entire tree
+ *
+ * @return array of ORM objects
+ */
+ public function get_leaves()
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ $leaves=$this->new_node();
+ $leaves->where($this->scope_column,$this->get_scope());
+ $leaves->where($rgt_col.' = '.$lft_col . ' + 1');
+
+ return $leaves->find_all();
+ }
+ /**
+ * Get the path leading up to the current node
+ * @return array with ORM objects
+ *
+ */
+ public function get_path()
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ $path=$this->new_node();
+ $path->where($this->scope_column,$this->get_scope());
+ $path->where($lft_col . ' <= '.$this->$lft_col . ' AND ' . $rgt_col . ' >=' .$this->$rgt_col . ' ORDER BY '.$lft_col);
+
+
+ return $path->find_all();
+
+ }
+
+ /**
+ * Returns the root node
+ * @return array $resultNode The node returned
+ */
+ function get_root()
+ {
+ return $this->get_node_where('`'.$this->left_column . '` = 1 ');
+ }
+ /**
+ * Returns a node by id
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function get_node($node_id)
+ {
+ $scope_column=$this->scope_column;
+ $class = get_class($this);
+ $node=new $class();
+
+ $node=$node->find($node_id,true);
+
+ return $node;
+ }
+ /**
+ * Returns a new empty node
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function new_node(){
+
+ $class = get_class($this);
+ $node=new $class();
+ return $node;
+ }
+ /**
+ * Returns one node with where condition
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function get_node_where($where)
+ {
+ $scope_column=$this->scope_column;
+ $class = get_class($this);
+ $node = new $class();
+ $node->where($where);
+ $node->where($this->scope_column,$this->get_scope());
+
+ $node=$node->find(false,false);
+ return $node;
+ }
+ /**
+ * Returns the first child node of the given parentNode
+ *
+ * @return array $resultNode The first child of the parent node supplied
+ */
+ function get_first_child()
+ {
+ $lft_col = $this->left_column;
+
+ return $this->get_node_where($this->left_column . " = " . ($this->$lft_col+1));
+ }
+
+ /**
+ * Returns the last child node of the given parentNode
+ *
+ * @return array $resultNode the last child of the parent node supplied
+ */
+ function get_last_child()
+ {
+ $rgt_col = $this->right_column;
+
+ return $this->get_node_where($this->right_column . " = " . ($this->$rgt_col-1));
+ }
+
+ /**
+ * Returns the node that is the immediately prior sibling of the given node
+ *
+ * @return array $resultNode The node returned
+ */
+ function get_prev_sibling()
+ {
+ $lft_col = $this->left_column;
+
+ return $this->get_node_where($this->right_column . " = " . ($this->$lft_col-1));
+ }
+
+ /**
+ * Returns the node that is the next sibling of the given node
+ *
+ * @return array $resultNode The node returned
+ */
+ function get_next_sibling()
+ {
+ $rgt_col = $this->right_column;
+
+ return $this->get_node_where($this->left_column . " = " . ($this->$rgt_col+1));
+ }
+ /**
+ * Returns the node that represents the parent of the given node
+ *
+ * @return array $resultNode the node returned
+ */
+ function get_parent()
+ {
+
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ $whereArg = " $leftcol < " . $this->$leftcol .
+ " AND $rightcol > " . $this->$rightcol .
+ " ORDER BY $rightcol ASC";
+ return $this->get_node_where($whereArg);
+ }
+/*
+ * *****************
+ * Modifying methods
+ * *****************
+ */
+ /**
+ * Adds the first entry to the table (only call once in an empty table) else corruption will follow
+ * @return $node an array of left and right values
+ */
+ function make_root()
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+
+ if(is_numeric($this->$lft_col) )
+ {
+ Log::add('error', 'Cannot make existing node root' );
+ //existing nodes cannot become root
+ return false;
+ }
+
+ $this->$lft_col=1;
+ $this->$rgt_col=2;
+ $this->$scp_col=$this->get_scope();
+
+ $this->save_node();
+
+ return $this;
+ }
+ /**
+ * Not yet implemented
+ *
+ */
+ function insert_as_parent_of($child_node)
+ {
+
+ }
+ /**
+ * inserts a the object node as the first child of the supplied parent node
+ * @param array $parentNode The node array of the parent to use
+ *
+ * @return
+ */
+ function insert_as_first_child_of($parent_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $this->$parent_column=$parent_node->id;
+
+ $this->$lft_col=$parent_node->$lft_col+1;
+ $this->$rgt_col=$parent_node->$lft_col+2;
+ //Get scope from current object (obsolete)
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Same as insertNewChild except the new node is added as the last child
+ * @param array $parentNode The node array of the parent to use
+ *
+ * @return
+ */
+ function insert_as_last_child_of($parent_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $this->$parent_column=$parent_node->id;
+
+ $this->$lft_col=$parent_node->$rgt_col;
+ $this->$rgt_col=$parent_node->$rgt_col+1;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Adds a new node to the left of the supplied focusNode
+ * @param array $focusNode The node to use as the position marker
+ *
+ * @return
+ */
+ function insert_as_prev_sibling_of($focus_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+ $scp_col=$this->scope_column;
+
+ //Set parent id (siblings have the same parent)
+ $this->$parent_column=$focus_node->$parent_column;
+
+ $this->$lft_col=$focus_node->$lft_col;
+ $this->$rgt_col=$focus_node->$lft_col+1;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Adds a new node to the right of the supplied focusNode
+ * @param array $focusNode The node to use as the position marker
+ *
+ * @return
+ */
+ function insert_as_next_sibling_of($focus_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+ $scp_col=$this->scope_column;
+
+ //Set parent id (siblings have the same parent)
+ $this->$parent_column=$focus_node->$parent_column;
+
+ $this->$lft_col=$focus_node->$rgt_col+1;
+ $this->$rgt_col=$focus_node->$rgt_col+2;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Why not, kill the entire tree
+ *
+ * @return
+ */
+ function delete_tree()
+ {
+ $where=array($this->scope_column=>$this->get_scope());
+
+ self::$db->delete($this->table, $where);
+ return;
+ }
+ /**
+ *
+ * overrides delete of ORM
+ */
+ public function delete()
+ {
+ return $this->delete_node();
+ }
+ /**
+ * Deletes the given node ((itself) from the tree table
+ * @param children boolean set to false to not delete children
+ * and move them up the tree
+ * @return boolean
+ */
+ function delete_node($children=true)
+ {
+
+ $table = $this->table;
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $leftanchor = $this->$leftcol;
+ $leftval = $this->$leftcol;
+ $rightval = $this->$rightcol;
+ $parent_column = $this->parent_column;
+ if($children==true)
+ {
+
+ $where=$leftcol . '>='.$leftval .'
+ AND '. $rightcol .' <= ' . $rightval . '
+ AND '. $this->scope_column .' = '.$this->get_scope() ;
+
+ $this->lock_tree();
+ //Delete node and children
+ self::$db->delete($this->table, $where);
+ //Modify other nodes to restore tree integrity
+ $this->modify_node($rightval+1, $leftval - $rightval - 1);
+ $this->unlock_tree();
+ }
+ else
+ {
+ if($this->is_root()){
+ Log::add('error', 'Cannot delete root node without deleting its children' );
+ return false;
+ }
+ $this->lock_tree();
+ //Get children before the parent is gone
+ //Set parent ids right again
+ $parent_node=$this->get_node($this->$parent_column);
+
+ $children=$this->get_children(true);
+
+ //Delete the node
+ $where=array('id'=>$this->id);
+ self::$db->delete($this->table, $where);
+
+ //First update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->left_column .'='.$this->left_column .'-1, '.
+ $this->right_column.' = '.$this->right_column . '-1
+ WHERE '. $this->left_column .' >= '.$this->$leftcol . '
+ AND '.$this->right_column .' <= '.$this->$rightcol .'
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Second update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->left_column .'='.$this->left_column .' -2
+ WHERE '. $this->right_column .' > '.$this->$rightcol . '-1
+ AND '.$this->left_column .' > '.$this->$rightcol .'-1
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Third update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->right_column .'='.$this->right_column .'-2
+ WHERE '. $this->right_column .' > '.$this->$rightcol . '-1
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Set the parent ids
+ if(is_array($children))
+ {
+ foreach($children as $child)
+ {
+ $child->$parent_column=$parent_node->id;
+ $child->save_node();
+ }
+ }
+ $this->unlock_tree();
+
+ }
+
+
+ return true;
+ }
+
+ /**
+ * Delete descendants but not node itself
+ *
+ * @return
+ */
+ function delete_descendants()
+ {
+ $this->lock_tree();
+ $this->get_children();
+ foreach($this->children as $child)
+ {
+ $child->delete_node();
+
+ }
+ $this->unlock_tree();
+ $this->children=array();
+ return true;
+ }
+ /**
+ * Deletes children but not descendants,
+ * descendants move up the tree
+ * @return
+ */
+ function delete_children()
+ {
+ $this->get_children();
+ foreach($this->children as $child)
+ {
+ $child->delete_node(false);
+
+ }
+ $this->unlock_tree();
+ $this->children=array();
+ return true;
+ }
+
+ // -------------------------------------------------------------------------
+ // MODIFY/REORGANISE TREE
+ //
+ // Methods to move nodes around the tree. Method names should be
+ // relatively self-explanatory! Hopefully ;)
+ //
+ // -------------------------------------------------------------------------
+
+ //inter-scope moves might be something for later
+ /**
+ * Moves the given node to make it the next sibling of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_next_sibling_of( $target)
+ {
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (siblings have the same parent)
+ $parent_id=$target->$parent_column;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$rgt_col+1);
+ }
+ return false;
+ }
+
+ /**
+ * Moves the given node to make it the prior sibling of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_prev_sibling_of( $target)
+ {
+ $lft_col = $this->left_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (siblings have the same parent)
+ $parent_id=$target->$parent_column;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$lft_col);
+ }
+ return false;
+
+ }
+
+ /**
+ * Moves the given node to make it the first child of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_first_child_of( $target)
+ {
+ $lft_col = $this->left_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $parent_id=$target->id;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$lft_col+1);
+ }
+ return false;
+ }
+
+ /**
+ * Moves the given node to make it the last child of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_last_child_of($target)
+ {
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $parent_id=$target->id;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree($target->$rgt_col);
+ }
+ return false;
+ }
+
+ /*
+ * *****************
+ * Check methods
+ * *****************
+ */
+ /**
+ * Returns true or false
+ * (in reality, it checks to see if the given left and
+ * right values _appear_ to be valid not necessarily that they _are_ valid)
+ *
+ * @return boolean
+ */
+ function is_valid_node()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return ($this->$leftcol < $this->$rightcol);
+ }
+
+ /**
+ * Tests whether the given node has an ancestor
+ * (effectively the opposite of isRoot yes|no)
+ *
+ * @return boolean
+ */
+ function has_parent()
+ {
+ return $this->is_valid_node($this->get_parent());
+ }
+
+ /**
+ * Tests whether the given node has a prior sibling or not
+ *
+ * @return boolean
+ */
+ function has_prev_sibling()
+ {
+ return $this->is_valid_node($this->get_prev_sibling());
+ }
+
+ /**
+ * Test to see if node has siblings after itself
+ *
+ * @return boolean
+ */
+ function has_next_sibling()
+ {
+ return $this->is_valid_node($this->get_next_sibling());
+ }
+
+ /**
+ * Test to see if node has children
+ *
+ * @return boolean
+ */
+ function has_descendants()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ return (($this->$rightcol - $this->$leftcol) > 1);
+ }
+
+ /**
+ * Test to see if the given node is also the root node
+ *
+ * @return boolean
+ */
+ function is_root()
+ {
+ $leftcol = $this->left_column;
+ return ($this->$leftcol == 1);
+ }
+
+ /**
+ * Test to see if the given node is a leaf node (ie has no children)
+ *
+ * @return boolean
+ */
+ function is_leaf()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ return (($this->$rightcol - $this->$leftcol) == 1);
+ }
+
+ /**
+ * Test to see if the node is a descendant of the given node
+ * @param array $control_node the node to use as the parent or ancestor
+ * @return boolean
+ */
+ function is_descendant_of($control_node)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return ( ($this->$leftcol > $control_node->$leftcol)
+ and ($this->$rightcol < $control_node->$rightcol)
+ );
+ }
+
+ /**
+ * Test to see if the node is a descendant of the given node
+ * @param array $control_node the node to use as the parent or ancestor
+ * @return boolean
+ */
+ function is_child_of($control_node)
+ {
+ $child_id=$this->id;
+ $parent_id=$control_node->id;
+
+ self::$db->select('count(*) as is_child');
+ self::$db->from($this->table);
+ self::$db->where('id',$child_id);
+ self::$db->where($this->parent_column,$parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $result=self::$db->get();
+
+ if ($row = $result->current())
+ {
+ return $row->is_child > 0;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Test to determine whether the node is infact also the $control_node (is A === B)
+ * @param array $control_node The node prototype to use for the comparison
+ * @return boolean
+ */
+ function is_equal_to($control_node)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return (($this->$leftcol==$control_node->$leftcol) and ($this->$rightcol==$control_node->$rightcol));
+ }
+
+ /**
+ * Combination method of is_descendant and is_equal
+ *
+ * @param array $controlNode The node prototype to use for the comparison
+ * @return boolean
+ */
+ function is_descendant_or_equal_to($controlNode)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return (($this->$leftcol>=$control_node->$leftcol) and ($this->$rightcol<=$control_node->$rightcol));
+ }
+ /*
+ * *****************
+ * Informational methods
+ * *****************
+ */
+ /**
+ * Returns the tree level for the given node (assuming root node is at level 0)
+ *
+ * @return integer The level of the supplied node
+ */
+ function get_level()
+ {
+ $leftval = (int) $this->$lft_col;
+ $rightval = (int) $this->$rgt_col;
+
+ self::$db->select('COUNT(*) AS level');
+ self::$db->from($this->table);
+ self::$db->where($this->left_column.' < '.$leftval);
+ self::$db->where($this->right_column.' < '.$rightval);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $query=self::$db->get();
+ if($query->count() > 0) {
+ $result = $query->current();
+ return (int) $result->level;
+ } else {
+ return 0;
+ }
+ }
+ /**
+ * Output number of descendants this node has
+ * @return integer
+ *
+ */
+ function get_number_of_descendants(){
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ // Generate WHERE
+
+ return (int) ( $this->$rgt_col - $this->$lft_col -1)/2;
+
+ }
+ /**
+ * Output number of children of this node
+ * @return integer
+ */
+ function get_number_of_children()
+ {
+
+ self::$db->select('count(*) as num_children');
+ self::$db->from($this->table);
+ self::$db->where($this->parent_column,$this->id);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $result=self::$db->get();
+
+ if($row = $result->current())
+ return (int) $row->num_children;
+
+ return -1;
+ }
+ /**
+ * Get current scope of the object
+ * @return integer
+ */
+ function get_scope()
+ {
+ return $this->_scope;
+ }
+ /**
+ * Set scope of current object, retrieved objects calls this in constructor
+ */
+ function set_scope($value)
+ {
+ $this->_scope=$value;
+ return $this;
+ }
+
+
+
+ /* *****************************************
+ * Print methods, more or less debug methods
+ * *****************************************
+ */
+ /**
+ * Debug tree
+ */
+ function debug_tree($tree, $disp_col, $ind = '')
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ echo $ind .'#'.$tree->id.' '.$tree->$lft_col.'- '.$tree->$disp_col .' p:'.$tree->$parent_column.' -'.$tree->$rgt_col.'<br>';
+ if(is_array($tree->children))
+ {
+ foreach($tree->children as $child)
+ {
+ $this->debug_tree($child,$disp_col,'....'.$ind);
+ }
+ }
+
+
+ }
+ /**
+ * Will rebuild tree according to the parent_id column
+ * Warning, the order of the tree might not exactly be maintained
+ * Might be slow for big trees
+ * Call this method only with the root_id and its left value.
+ * @return
+ * @param $parent_id Object
+ * @param $left Object
+ */
+ function rebuild_tree($parent_id, $left) {
+ $this->lock_tree();
+ // the right value of this node is the left value + 1
+ $right = $left+1;
+
+ // get all children of this node
+ self::$db->select('id');
+ self::$db->where($this->parent_column, $parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+ self::$db->from($this->table);
+ $result=self::$db->get();
+
+ foreach ($result as $row) {
+ // recursive execution of this function for each
+ // child of this node
+ // $right is the current right value, which is
+ // incremented by the rebuild_tree function
+ $right = $this->rebuild_tree($row->id, $right);
+ }
+
+ // we've got the left value, and now that we've processed
+ // the children of this node we also know the right value
+
+ self::$db->set($this->left_column, $left);
+ self::$db->set($this->right_column, $right);
+ self::$db->where('id',$parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+ self::$db->update($this->table);
+ // return the right value of this node + 1
+ return $right+1;
+
+ $this->unlock_tree();
+ }
+
+ /*
+ * Protected functions
+ *
+ */
+ /**
+ * check whether child is child of parent (internal)
+ *
+ * @return
+ * @param $child Object
+ * @param $parent Object
+ */
+ protected function _is_descendant_of($child, $parent)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ return ($child->$lft_col > $parent->$lft_col && $child->$rgt_col < $parent->$rgt_col);
+ }
+
+ /**
+ * The method that performs moving/renumbering operations
+ *
+ * @param array $targetValue Position integer to use as the target
+ * @return array $newpos The new left and right values of the node moved
+ * @access private
+ */
+ protected function move_sub_tree($targetValue)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ $sizeOfTree = $this->$rightcol - $this->$leftcol + 1;
+ $this->modify_node($targetValue, $sizeOfTree);
+
+
+ if($this->$leftcol >= $targetValue)
+ {
+ $this->$leftcol += $sizeOfTree;
+ $this->$rightcol += $sizeOfTree;
+ }
+
+ $newpos = $this->modify_node_range($this->$leftcol, $this->$rightcol, $targetValue - $this->$leftcol);
+
+ $this->modify_node($this->$rightcol+1, - $sizeOfTree);
+
+ if($this->$leftcol <= $targetValue)
+ {
+ $newpos[$this->left_column] -= $sizeOfTree;
+ $newpos[$this->right_column] -= $sizeOfTree;
+ }
+
+ return $newpos;
+ }
+ /**
+ * _modifyNodeRange
+ *
+ * @param $lowerbound integer value of lowerbound of range to move
+ * @param $upperbound integer value of upperbound of range to move
+ * @param $changeVal unsigned integer of change amount
+ * @access private
+ */
+
+ protected function modify_node_range($lowerbound, $upperbound, $changeVal)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $table = $this->table;
+ $scope_col = $this->scope_column;
+
+ $sql = "UPDATE $table
+ SET $leftcol = $leftcol + $changeVal
+ WHERE $leftcol >= $lowerbound
+ AND $leftcol <= $upperbound
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $sql = "UPDATE $table
+ SET $rightcol = $rightcol + $changeVal
+ WHERE $rightcol >= $lowerbound
+ AND $rightcol <= $upperbound
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $retArray = array(
+ $this->left_column => $lowerbound+$changeVal,
+ $this->right_column => $upperbound+$changeVal
+ );
+ return $retArray;
+ } // END: Method _modifyNodeRange
+
+ /**
+ * Update the parent id of this record, the ORM class handles
+ * it when the parent id didn't change
+ * @return
+ * @param $node Object
+ * @param $parent_id Object
+ */
+ protected function update_parent_id($parent_id)
+ {
+ $parent_column=$this->parent_column;
+ $this->$parent_column=$parent_id;
+ return $this->save_node();
+ }
+ /**
+ * _modifyNode
+ *
+ * Adds $changeVal to all left and right values that are greater than or
+ * equal to $node_int
+ *
+ * @param $node_int The value to start the shift from
+ * @param $changeVal unsigned integer value for change
+ * @access private
+ */
+ protected function modify_node($node_int, $changeVal)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $table = $this->table;
+ $scope_col = $this->scope_column;
+
+ $sql = "UPDATE $table " .
+ "SET $leftcol = $leftcol + $changeVal ".
+ "WHERE $leftcol >= $node_int
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $sql = "UPDATE $table " .
+ "SET $rightcol = $rightcol + $changeVal ".
+ "WHERE $rightcol >= $node_int
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ return true;
+ } // END: _modifyNode
+ /**
+ * save_node
+ *
+ * Inserts a new node into the tree, or saves the current one
+ *
+ * @return boolean True/False dependent upon the success of the operation
+ * @access private
+ */
+ protected function save_node( )
+ {
+ if ($this->save()) {
+ // Return true on success
+ return TRUE;
+ }
+
+ return false;
+ }
+}