Start working on the new fields v2 system.
[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
56 // Usergroup access controls {{
57 const ACCESS_NONE = 0;
58 const ACCESS_READ = 1;
59 const ACCESS_WRITE = 2;
60 // }}
61
62 public function is_tag()
63 {
64 $title = $this->title;
65 return empty($title);
66 }
67 public function is_field()
68 {
69 $title = $this->title;
70 return !empty($title);
71 }
72
73 // Returns the access level that |user| has for this field for |bug|.
74 public function CheckAccess(User $user, $bug)
75 {
76 return self::ACCESS_READ | self::ACCESS_WRITE;
77 }
78
79 // Validates the value of a field. Returns a 2-Tuple<bool,mixed>. The
80 // first item is whether or not the value validated. The second item is the
81 // validated value, if any transformation took place.
82 public function Validate($value)
83 {
84 switch ($this->type) {
85 case self::TYPE_TEXT: return $this->_ValidateText($value);
86 case self::TYPE_BOOL: return $this->_ValidateBoolean($value);
87 case self::TYPE_LIST: return $this->_ValidateList($value);
88 case self::TYPE_DATE: return $this->_ValidateDate($value);
89 case self::TYPE_USER: return $this->_ValidateUser($value);
90 default: throw new FieldException('Unknown field type "' . $this->type . '"');
91 }
92 }
93
94 protected function _ValidateText($value)
95 {
96 $value = trim($value);
97
98 // Handle empty strings, including the default value.
99 if ($this->required && empty($value) && !$this->default_value) {
100 return array(FALSE, $value);
101 } else if ($this->default_value) {
102 return array(TRUE, $this->default_value);
103 }
104
105 // Validate using pattern.
106 if ($this->validator_pattern) {
107 $valid = preg_match("/{$this->validator_pattern}/", $value);
108 return array($valid !== FALSE && $valid > 0, $value);
109 }
110
111 // All other values are valid.
112 return array(TRUE, $value);
113 }
114
115 protected function _ValidateBoolean($value)
116 {
117 // Booleans are technically tri-state: true, false, and unset. The only
118 // time the default value can be used is in the unset state.
119 if ($value === NULL && $this->default_value !== NULL) {
120 return array(TRUE, $this->default_value);
121 }
122
123 // Parse booleans in a bunch of different ways.
124 $value = trim(strtolower($value));
125 if (intval($value[0]) == 1 || $value[0] === TRUE ||
126 $value[0] == 'y' || $value[0] == 't') {
127 return array(TRUE, TRUE);
128 }
129 // Everything else will assume false. Don't bother failing validation.
130 return array(TRUE, FALSE);
131 }
132
133 protected function _ValidateList($value)
134 {
135 // Handle empty values, including the default value.
136 if ($this->required && empty($value) && !$this->default_value) {
137 return array(FALSE, $value);
138 } else if ($this->default_value) {
139 return array(TRUE, $this->default_value);
140 }
141
142 // Otherwise, iterate over the possible values.
143 $options = $this->GetListOptions();
144 $value = trim($value);
145 foreach ($options as $option) {
146 if (strcasecmp($option, $value) == 0) {
147 // Return the proper case from the canonical option value.
148 return array(TRUE, $option);
149 }
150 }
151 return array(FALSE, $value);
152 }
153
154 protected function _ValidateDate($value)
155 {
156 // Handle the one default value (now).
157 if ($this->required && empty($value) && !$this->default_value) {
158 return array(FALSE, $value);
159 } else if ($this->default_value) {
160 return array(TRUE, time());
161 }
162
163 $time = strtotime($value);
164 if ($time === FALSE) {
165 return array(FALSE, $value);
166 } else {
167 return array(TRUE, $time);
168 }
169 }
170
171 protected function _ValidateUser($value)
172 {
173 // Handle the default value.
174 if ($this->required && empty($value) && !$this->default_value) {
175 return array(FALSE, $value);
176 } else if ($this->default_value) {
177 return array(TRUE, $this->default_value);
178 }
179
180 // Look the user up by alias to get the user ID.
181 $user = new User();
182 $user->set('displayname', $value);
183 $user->set_condition('displayname = :displayname');
184 try {
185 $user->FetchInto();
186 return array(TRUE, $user->userid);
187 } catch (\phalanx\data\ModelException $e) {
188 return array(FALSE, $value);
189 }
190 }
191
192 // If this Field is TYPE_LIST, this will return an array of options for
193 // the list. Note that bugs store values rather than indices, so comparison
194 // is case-insensitive string compare to determine if a value is a member
195 // of the set.
196 public function GetListOptions()
197 {
198 if ($this->type != self::TYPE_LIST) {
199 throw new FieldException('"' . $this->title . '" is not a list');
200 }
201 return explode("\n", $this->validator_pattern);
202 }
203
204 // Sets the valid options for the list. This will replace all current
205 // options. Note that bugs will retain their current values if an option is
206 // removed, as they store the actual value, rather than a reference to the
207 // value.
208 public function SetListOptions(Array $options)
209 {
210 if ($this->type != self::TYPE_LIST) {
211 throw new FieldException('"' . $this->title . '" is not a list');
212 }
213 $str_filter = '/[^a-z0-9_\-\.,]/i';
214 foreach ($options as $i => $option) {
215 $options[$i] = preg_replace($str_filter, '', $option);
216 }
217 $this->validator_pattern = implode("\n", $options);
218 }
219 }
220
221 class FieldException extends \Exception
222 {}