Start working on the admin field editor UI.
[bugdar.git] / includes / model_field.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Bugdar
5 || # Copyright (c)2002-2013 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 namespace bugdar;
23
24 require_once HOPLITE_ROOT . '/data/model.php';
25 require_once './includes/api_user.php';
26
27 class Field extends \hoplite\data\Model
28 {
29 // Model properties.
30 protected $table_prefix = TABLE_PREFIX;
31 protected $table = 'field';
32 protected $primary_key = 'title';
33 protected $condition = 'title = :title';
34
35 // Struct properties.
36 protected $fields = array(
37 'title',
38 'description',
39 'type', // See constants below.
40 'validator_pattern', // Stores list options and string regex.
41 'required',
42 'default_value', // String. Or TRUE for TYPE_DATE to mean today.
43 'can_search',
44 'color_foreground',
45 'color_background'
46 );
47
48 // Types of fields {{
49 const TYPE_TEXT = 'text';
50 const TYPE_BOOL = 'boolean';
51 const TYPE_LIST = 'list';
52 const TYPE_DATE = 'date';
53 const TYPE_USER = 'user';
54 // }}
55 public static $field_types = [
56 self::TYPE_TEXT,
57 self::TYPE_BOOL,
58 self::TYPE_LIST,
59 self::TYPE_DATE,
60 self::TYPE_USER
61 ];
62
63 // Usergroup access controls {{
64 const ACCESS_NONE = 0;
65 const ACCESS_READ = 1;
66 const ACCESS_WRITE = 2;
67 // }}
68
69 public function is_tag()
70 {
71 $title = $this->title;
72 return empty($title);
73 }
74 public function is_field()
75 {
76 $title = $this->title;
77 return !empty($title);
78 }
79
80 // Returns the access level that |user| has for this field for |bug|.
81 public function CheckAccess(User $user, $bug)
82 {
83 return self::ACCESS_READ | self::ACCESS_WRITE;
84 }
85
86 // Validates the value of a field. Returns a 2-Tuple<bool,mixed>. The
87 // first item is whether or not the value validated. The second item is the
88 // validated value, if any transformation took place.
89 public function Validate($value)
90 {
91 switch ($this->type) {
92 case self::TYPE_TEXT: return $this->_ValidateText($value);
93 case self::TYPE_BOOL: return $this->_ValidateBoolean($value);
94 case self::TYPE_LIST: return $this->_ValidateList($value);
95 case self::TYPE_DATE: return $this->_ValidateDate($value);
96 case self::TYPE_USER: return $this->_ValidateUser($value);
97 default: throw new FieldException('Unknown field type "' . $this->type . '"');
98 }
99 }
100
101 protected function _ValidateText($value)
102 {
103 $value = trim($value);
104
105 // Handle empty strings, including the default value.
106 if ($this->required && empty($value) && !$this->default_value) {
107 return array(FALSE, $value);
108 } else if ($this->default_value) {
109 return array(TRUE, $this->default_value);
110 }
111
112 // Validate using pattern.
113 if ($this->validator_pattern) {
114 $valid = preg_match("/{$this->validator_pattern}/", $value);
115 return array($valid !== FALSE && $valid > 0, $value);
116 }
117
118 // All other values are valid.
119 return array(TRUE, $value);
120 }
121
122 protected function _ValidateBoolean($value)
123 {
124 // Booleans are technically tri-state: true, false, and unset. The only
125 // time the default value can be used is in the unset state.
126 if ($value === NULL && $this->default_value !== NULL) {
127 return array(TRUE, $this->default_value);
128 }
129
130 // Parse booleans in a bunch of different ways.
131 $value = trim(strtolower($value));
132 if (intval($value[0]) == 1 || $value[0] === TRUE ||
133 $value[0] == 'y' || $value[0] == 't') {
134 return array(TRUE, TRUE);
135 }
136 // Everything else will assume false. Don't bother failing validation.
137 return array(TRUE, FALSE);
138 }
139
140 protected function _ValidateList($value)
141 {
142 // Handle empty values, including the default value.
143 if ($this->required && empty($value) && !$this->default_value) {
144 return array(FALSE, $value);
145 } else if ($this->default_value) {
146 return array(TRUE, $this->default_value);
147 }
148
149 // Otherwise, iterate over the possible values.
150 $options = $this->GetListOptions();
151 $value = trim($value);
152 foreach ($options as $option) {
153 if (strcasecmp($option, $value) == 0) {
154 // Return the proper case from the canonical option value.
155 return array(TRUE, $option);
156 }
157 }
158 return array(FALSE, $value);
159 }
160
161 protected function _ValidateDate($value)
162 {
163 // Handle the one default value (now).
164 if ($this->required && empty($value) && !$this->default_value) {
165 return array(FALSE, $value);
166 } else if ($this->default_value) {
167 return array(TRUE, time());
168 }
169
170 $time = strtotime($value);
171 if ($time === FALSE) {
172 return array(FALSE, $value);
173 } else {
174 return array(TRUE, $time);
175 }
176 }
177
178 protected function _ValidateUser($value)
179 {
180 // Handle the default value.
181 if ($this->required && empty($value) && !$this->default_value) {
182 return array(FALSE, $value);
183 } else if ($this->default_value) {
184 return array(TRUE, $this->default_value);
185 }
186
187 // Look the user up by alias to get the user ID.
188 $user = new User();
189 $user->set('displayname', $value);
190 $user->set_condition('displayname = :displayname');
191 try {
192 $user->FetchInto();
193 return array(TRUE, $user->userid);
194 } catch (\phalanx\data\ModelException $e) {
195 return array(FALSE, $value);
196 }
197 }
198
199 // If this Field is TYPE_LIST, this will return an array of options for
200 // the list. Note that bugs store values rather than indices, so comparison
201 // is case-insensitive string compare to determine if a value is a member
202 // of the set.
203 public function GetListOptions()
204 {
205 if ($this->type != self::TYPE_LIST) {
206 throw new FieldException('"' . $this->title . '" is not a list');
207 }
208 return explode("\n", $this->validator_pattern);
209 }
210
211 // Sets the valid options for the list. This will replace all current
212 // options. Note that bugs will retain their current values if an option is
213 // removed, as they store the actual value, rather than a reference to the
214 // value.
215 public function SetListOptions(Array $options)
216 {
217 if ($this->type != self::TYPE_LIST) {
218 throw new FieldException('"' . $this->title . '" is not a list');
219 }
220 $str_filter = '/[^a-z0-9_\-\.,]/i';
221 foreach ($options as $i => $option) {
222 $options[$i] = preg_replace($str_filter, '', $option);
223 }
224 $this->validator_pattern = implode("\n", $options);
225 }
226 }
227
228 class FieldException extends \Exception
229 {}