@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 384 lines 9.8 kB view raw
1<?php 2 3final class AphrontFormDateControlValue extends Phobject { 4 5 private $valueDate; 6 private $valueTime; 7 private $valueEnabled; 8 9 private $viewer; 10 private $zone; 11 private $optional; 12 13 public function getValueDate() { 14 return $this->valueDate; 15 } 16 17 public function getValueTime() { 18 return $this->valueTime; 19 } 20 21 public function isValid() { 22 if ($this->isDisabled()) { 23 return true; 24 } 25 return ($this->getEpoch() !== null); 26 } 27 28 public function isEmpty() { 29 if ($this->valueDate) { 30 return false; 31 } 32 33 if ($this->valueTime) { 34 return false; 35 } 36 37 return true; 38 } 39 40 public function isDisabled() { 41 return ($this->optional && !$this->valueEnabled); 42 } 43 44 public function setEnabled($enabled) { 45 $this->valueEnabled = $enabled; 46 return $this; 47 } 48 49 public function setOptional($optional) { 50 $this->optional = $optional; 51 return $this; 52 } 53 54 public function getOptional() { 55 return $this->optional; 56 } 57 58 public function getViewer() { 59 return $this->viewer; 60 } 61 62 public static function newFromRequest(AphrontRequest $request, $key) { 63 $value = new AphrontFormDateControlValue(); 64 $value->viewer = $request->getViewer(); 65 66 $date = $request->getStr($key.'_d', ''); 67 $time = $request->getStr($key.'_t', ''); 68 69 // If we have the individual parts, we read them preferentially. If we do 70 // not, try to read the key as a raw value. This makes it so that HTTP 71 // prefilling is overwritten by the control value if the user changes it. 72 if (!strlen($date) && !strlen($time)) { 73 $date = $request->getStr($key); 74 $time = null; 75 } 76 77 $value->valueDate = $date; 78 $value->valueTime = $time; 79 80 $formatted = $value->getFormattedDateFromDate( 81 $value->valueDate, 82 $value->valueTime); 83 84 if ($formatted) { 85 list($value->valueDate, $value->valueTime) = $formatted; 86 } 87 88 $value->valueEnabled = $request->getStr($key.'_e'); 89 return $value; 90 } 91 92 public static function newFromEpoch(PhabricatorUser $viewer, $epoch) { 93 $value = new AphrontFormDateControlValue(); 94 $value->viewer = $viewer; 95 96 if (!$epoch) { 97 return $value; 98 } 99 100 $readable = $value->formatTime($epoch, 'Y!m!d!g:i A'); 101 $readable = explode('!', $readable, 4); 102 103 $year = $readable[0]; 104 $month = $readable[1]; 105 $day = $readable[2]; 106 $time = $readable[3]; 107 108 list($value->valueDate, $value->valueTime) = 109 $value->getFormattedDateFromParts( 110 $year, 111 $month, 112 $day, 113 $time); 114 115 return $value; 116 } 117 118 public static function newFromDictionary( 119 PhabricatorUser $viewer, 120 array $dictionary) { 121 $value = new AphrontFormDateControlValue(); 122 $value->viewer = $viewer; 123 124 $value->valueDate = idx($dictionary, 'd'); 125 $value->valueTime = idx($dictionary, 't'); 126 127 $formatted = $value->getFormattedDateFromDate( 128 $value->valueDate, 129 $value->valueTime); 130 131 if ($formatted) { 132 list($value->valueDate, $value->valueTime) = $formatted; 133 } 134 135 $value->valueEnabled = idx($dictionary, 'e'); 136 137 return $value; 138 } 139 140 public static function newFromWild(PhabricatorUser $viewer, $wild) { 141 if (is_array($wild)) { 142 return self::newFromDictionary($viewer, $wild); 143 } else if (is_numeric($wild)) { 144 return self::newFromEpoch($viewer, $wild); 145 } else { 146 throw new Exception( 147 pht( 148 'Unable to construct a date value from value of type "%s".', 149 gettype($wild))); 150 } 151 } 152 153 public function getDictionary() { 154 return array( 155 'd' => $this->valueDate, 156 't' => $this->valueTime, 157 'e' => $this->valueEnabled, 158 ); 159 } 160 161 public function getValueAsFormat($format) { 162 return phabricator_format_local_time( 163 $this->getEpoch(), 164 $this->viewer, 165 $format); 166 } 167 168 private function formatTime($epoch, $format) { 169 $date = phorge_localize_time($epoch, $this->viewer); 170 if (!$date) { 171 return ''; 172 } 173 // Call DateTime->format directly (bypassing PhutilTranslator) 174 // so that getFormattedDateFromParts below can decode the parts 175 // back into a DateTime 176 return $date->format($format); 177 } 178 179 public function getEpoch() { 180 if ($this->isDisabled()) { 181 return null; 182 } 183 184 $datetime = $this->newDateTime($this->valueDate, $this->valueTime); 185 if (!$datetime) { 186 return null; 187 } 188 189 return (int)$datetime->format('U'); 190 } 191 192 private function getTimeFormat() { 193 $viewer = $this->getViewer(); 194 $time_key = PhabricatorTimeFormatSetting::SETTINGKEY; 195 return $viewer->getUserSetting($time_key); 196 } 197 198 private function getDateFormat() { 199 $viewer = $this->getViewer(); 200 $date_key = PhabricatorDateFormatSetting::SETTINGKEY; 201 return $viewer->getUserSetting($date_key); 202 } 203 204 private function getFormattedDateFromDate($date, $time) { 205 $datetime = $this->newDateTime($date, $time); 206 if (!$datetime) { 207 return null; 208 } 209 210 return array( 211 $datetime->format($this->getDateFormat()), 212 $datetime->format($this->getTimeFormat()), 213 ); 214 } 215 216 /** 217 * Create a DateTime object including timezone 218 * @param string $date Date, like "2024-08-20" or "2024-07-1" or such 219 * @param string|null $time Time, like "12:00 AM" or such 220 * @return DateTime|null 221 */ 222 private function newDateTime($date, $time) { 223 $date = $this->getStandardDateFormat($date); 224 $time = $this->getStandardTimeFormat($time); 225 226 try { 227 // We need to provide the timezone in the constructor, and also set it 228 // explicitly. If the date is an epoch timestamp, the timezone in the 229 // constructor is ignored. If the date is not an epoch timestamp, it is 230 // used to parse the date. 231 $zone = $this->getTimezone(); 232 $datetime = new DateTime("{$date} {$time}", $zone); 233 $datetime->setTimezone($zone); 234 } catch (Exception $ex) { 235 return null; 236 } 237 238 239 return $datetime; 240 } 241 242 public function newPhutilDateTime() { 243 $datetime = $this->getDateTime(); 244 if (!$datetime) { 245 return null; 246 } 247 248 $all_day = !strlen($this->valueTime); 249 $zone_identifier = $this->viewer->getTimezoneIdentifier(); 250 251 $result = id(new PhutilCalendarAbsoluteDateTime()) 252 ->setYear((int)$datetime->format('Y')) 253 ->setMonth((int)$datetime->format('m')) 254 ->setDay((int)$datetime->format('d')) 255 ->setHour((int)$datetime->format('G')) 256 ->setMinute((int)$datetime->format('i')) 257 ->setSecond((int)$datetime->format('s')) 258 ->setTimezone($zone_identifier); 259 260 if ($all_day) { 261 $result->setIsAllDay(true); 262 } 263 264 return $result; 265 } 266 267 268 private function getFormattedDateFromParts( 269 $year, 270 $month, 271 $day, 272 $time) { 273 274 $zone = $this->getTimezone(); 275 $date_time = new DateTime("{$year}-{$month}-{$day} {$time}", $zone); 276 277 return array( 278 $date_time->format($this->getDateFormat()), 279 $date_time->format($this->getTimeFormat()), 280 ); 281 } 282 283 private function getFormatSeparator() { 284 $format = $this->getDateFormat(); 285 switch ($format) { 286 case 'n/j/Y': 287 return '/'; 288 default: 289 return '-'; 290 } 291 } 292 293 /** 294 * @return DateTime|null 295 */ 296 public function getDateTime() { 297 return $this->newDateTime($this->valueDate, $this->valueTime); 298 } 299 300 /** 301 * @return DateTimeZone 302 */ 303 private function getTimezone() { 304 if ($this->zone) { 305 return $this->zone; 306 } 307 308 $viewer_zone = $this->viewer->getTimezoneIdentifier(); 309 $this->zone = new DateTimeZone($viewer_zone); 310 return $this->zone; 311 } 312 313 private function getStandardDateFormat($date) { 314 // no value entered into the field at all 315 if (!$date) { 316 return null; 317 } 318 $colloquial = array( 319 'newyear' => 'January 1', 320 'valentine' => 'February 14', 321 'pi' => 'March 14', 322 'christma' => 'December 25', 323 ); 324 325 // Lowercase the input, then remove punctuation, a "day" suffix, and an 326 // "s" if one is present. This allows all of these to match. This allows 327 // variations like "New Year's Day" and "New Year" to both match. 328 $normalized = phutil_utf8_strtolower($date); 329 $normalized = preg_replace('/[^a-z]/', '', $normalized); 330 $normalized = preg_replace('/day\z/', '', $normalized); 331 $normalized = preg_replace('/s\z/', '', $normalized); 332 333 if ($normalized !== null && isset($colloquial[$normalized])) { 334 return $colloquial[$normalized]; 335 } 336 337 // If this looks like an epoch timestamp, prefix it with "@" so that 338 // DateTime() reads it as one. Assume small numbers are a "Ymd" digit 339 // string instead of an epoch timestamp for a time in 1970. 340 if (ctype_digit($date) && ($date > 30000000)) { 341 $date = '@'.$date; 342 } 343 344 $separator = $this->getFormatSeparator(); 345 $parts = preg_split('@[,./:-]@', $date); 346 return implode($separator, $parts); 347 } 348 349 private function getStandardTimeFormat($time) { 350 $colloquial = array( 351 'crack of dawn' => '5:00 AM', 352 'dawn' => '6:00 AM', 353 'early' => '7:00 AM', 354 'morning' => '8:00 AM', 355 'elevenses' => '11:00 AM', 356 'morning tea' => '11:00 AM', 357 'noon' => '12:00 PM', 358 'high noon' => '12:00 PM', 359 'lunch' => '12:00 PM', 360 'afternoon' => '2:00 PM', 361 'tea time' => '3:00 PM', 362 'evening' => '7:00 PM', 363 'late' => '11:00 PM', 364 'witching hour' => '12:00 AM', 365 'midnight' => '12:00 AM', 366 ); 367 368 $normalized = phutil_utf8_strtolower($time); 369 if (isset($colloquial[$normalized])) { 370 $time = $colloquial[$normalized]; 371 } 372 // Convert localized times to English 373 $am = pht('AM'); 374 if ($am !== 'AM') { 375 $time = preg_replace('/'.preg_quote($am).'$/', 'AM', $time); 376 } 377 $pm = pht('PM'); 378 if ($pm !== 'PM') { 379 $time = preg_replace('/'.preg_quote($pm).'$/', 'PM', $time); 380 } 381 return $time; 382 } 383 384}