Add well-known timestamps to JSON for PHP (#3564)

diff --git a/conformance/conformance_php.php b/conformance/conformance_php.php
index b6e12c0..2e3f783 100755
--- a/conformance/conformance_php.php
+++ b/conformance/conformance_php.php
@@ -38,6 +38,10 @@
 
 use  \Conformance\WireFormat;
 
+if (!ini_get("date.timezone")) {
+  ini_set("date.timezone", "UTC");
+}
+
 $test_count = 0;
 
 function doTest($request)
diff --git a/conformance/failure_list_php.txt b/conformance/failure_list_php.txt
index 2f91a3f..c1713cb 100644
--- a/conformance/failure_list_php.txt
+++ b/conformance/failure_list_php.txt
@@ -7,11 +7,6 @@
 Recommended.Proto3.JsonInput.DurationHas6FractionalDigits.Validator
 Recommended.Proto3.JsonInput.DurationHas9FractionalDigits.Validator
 Recommended.Proto3.JsonInput.DurationHasZeroFractionalDigit.Validator
-Recommended.Proto3.JsonInput.TimestampHas3FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHas6FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHas9FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHasZeroFractionalDigit.Validator
-Recommended.Proto3.JsonInput.TimestampZeroNormalized.Validator
 Required.DurationProtoInputTooLarge.JsonOutput
 Required.DurationProtoInputTooSmall.JsonOutput
 Required.Proto3.JsonInput.Any.JsonOutput
@@ -82,16 +77,6 @@
 Required.Proto3.JsonInput.RepeatedUint64Wrapper.ProtobufOutput
 Required.Proto3.JsonInput.Struct.JsonOutput
 Required.Proto3.JsonInput.Struct.ProtobufOutput
-Required.Proto3.JsonInput.TimestampMaxValue.JsonOutput
-Required.Proto3.JsonInput.TimestampMaxValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampMinValue.JsonOutput
-Required.Proto3.JsonInput.TimestampMinValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampRepeatedValue.JsonOutput
-Required.Proto3.JsonInput.TimestampRepeatedValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampWithNegativeOffset.JsonOutput
-Required.Proto3.JsonInput.TimestampWithNegativeOffset.ProtobufOutput
-Required.Proto3.JsonInput.TimestampWithPositiveOffset.JsonOutput
-Required.Proto3.JsonInput.TimestampWithPositiveOffset.ProtobufOutput
 Required.Proto3.JsonInput.ValueAcceptBool.JsonOutput
 Required.Proto3.JsonInput.ValueAcceptBool.ProtobufOutput
 Required.Proto3.JsonInput.ValueAcceptFloat.JsonOutput
diff --git a/conformance/failure_list_php_c.txt b/conformance/failure_list_php_c.txt
index 2e37884..088708e 100644
--- a/conformance/failure_list_php_c.txt
+++ b/conformance/failure_list_php_c.txt
@@ -19,11 +19,6 @@
 Recommended.Proto3.JsonInput.StringFieldSurrogateInWrongOrder
 Recommended.Proto3.JsonInput.StringFieldUnpairedHighSurrogate
 Recommended.Proto3.JsonInput.StringFieldUnpairedLowSurrogate
-Recommended.Proto3.JsonInput.TimestampHas3FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHas6FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHas9FractionalDigits.Validator
-Recommended.Proto3.JsonInput.TimestampHasZeroFractionalDigit.Validator
-Recommended.Proto3.JsonInput.TimestampZeroNormalized.Validator
 Recommended.Proto3.JsonInput.Uint64FieldBeString.Validator
 Recommended.Proto3.ProtobufInput.OneofZeroBytes.JsonOutput
 Recommended.Proto3.ProtobufInput.OneofZeroBytes.ProtobufOutput
@@ -160,16 +155,6 @@
 Required.Proto3.JsonInput.StringFieldUnicodeEscapeWithLowercaseHexLetters.ProtobufOutput
 Required.Proto3.JsonInput.Struct.JsonOutput
 Required.Proto3.JsonInput.Struct.ProtobufOutput
-Required.Proto3.JsonInput.TimestampMaxValue.JsonOutput
-Required.Proto3.JsonInput.TimestampMaxValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampMinValue.JsonOutput
-Required.Proto3.JsonInput.TimestampMinValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampRepeatedValue.JsonOutput
-Required.Proto3.JsonInput.TimestampRepeatedValue.ProtobufOutput
-Required.Proto3.JsonInput.TimestampWithNegativeOffset.JsonOutput
-Required.Proto3.JsonInput.TimestampWithNegativeOffset.ProtobufOutput
-Required.Proto3.JsonInput.TimestampWithPositiveOffset.JsonOutput
-Required.Proto3.JsonInput.TimestampWithPositiveOffset.ProtobufOutput
 Required.Proto3.JsonInput.Uint32FieldMaxFloatValue.JsonOutput
 Required.Proto3.JsonInput.Uint32FieldMaxFloatValue.ProtobufOutput
 Required.Proto3.JsonInput.Uint64FieldMaxValue.JsonOutput
diff --git a/php/src/Google/Protobuf/Internal/FieldDescriptor.php b/php/src/Google/Protobuf/Internal/FieldDescriptor.php
index 1443c6f..6644a2e 100644
--- a/php/src/Google/Protobuf/Internal/FieldDescriptor.php
+++ b/php/src/Google/Protobuf/Internal/FieldDescriptor.php
@@ -181,6 +181,12 @@
                $this->getMessageType()->getOptions()->getMapEntry();
     }
 
+    public function isTimestamp()
+    {
+        return $this->getType() == GPBType::MESSAGE &&
+            $this->getMessageType()->getClass() === "Google\Protobuf\Timestamp";
+    }
+
     private static function isTypePackable($field_type)
     {
         return ($field_type !== GPBType::STRING  &&
diff --git a/php/src/Google/Protobuf/Internal/GPBUtil.php b/php/src/Google/Protobuf/Internal/GPBUtil.php
index 84e8ecf..a27220a 100644
--- a/php/src/Google/Protobuf/Internal/GPBUtil.php
+++ b/php/src/Google/Protobuf/Internal/GPBUtil.php
@@ -38,6 +38,9 @@
 
 class GPBUtil
 {
+    const NANOS_PER_MILLISECOND = 1000000;
+    const NANOS_PER_MICROSECOND = 1000;
+
     public static function divideInt64ToInt32($value, &$high, &$low, $trim = false)
     {
         $isNeg = (bccomp($value, 0) < 0);
@@ -340,4 +343,81 @@
         }
         return $result;
     }
+    
+    public static function parseTimestamp($timestamp)
+    {
+        // prevent parsing timestamps containing with the non-existant year "0000"
+        // DateTime::createFromFormat parses without failing but as a nonsensical date
+        if (substr($timestamp, 0, 4) === "0000") {
+            throw new \Exception("Year cannot be zero.");
+        }
+        // prevent parsing timestamps ending with a lowercase z
+        if (substr($timestamp, -1, 1) === "z") {
+            throw new \Exception("Timezone cannot be a lowercase z.");
+        }
+        
+        $nanoseconds = 0;
+        $periodIndex = strpos($timestamp, ".");
+        if ($periodIndex !== false) {
+            $nanosecondsLength = 0;
+            // find the next non-numeric character in the timestamp to calculate
+            // the length of the nanoseconds text
+            for ($i = $periodIndex + 1, $length = strlen($timestamp); $i < $length; $i++) {
+                if (!is_numeric($timestamp[$i])) {
+                    $nanosecondsLength = $i - ($periodIndex + 1);
+                    break;
+                }
+            }
+            if ($nanosecondsLength % 3 !== 0) {
+                throw new \Exception("Nanoseconds must be disible by 3.");
+            }
+            if ($nanosecondsLength > 9) {
+                throw new \Exception("Nanoseconds must be in the range of 0 to 999,999,999 nanoseconds.");
+            }
+            if ($nanosecondsLength > 0) {
+                $nanoseconds = substr($timestamp, $periodIndex + 1, $nanosecondsLength);
+                $nanoseconds = intval($nanoseconds);
+
+                // remove the nanoseconds and preceding period from the timestamp
+                $date = substr($timestamp, 0, $periodIndex - 1);
+                $timezone = substr($timestamp, $periodIndex + $nanosecondsLength);
+                $timestamp = $date.$timezone;
+            }
+        }
+
+        $date = \DateTime::createFromFormat(\DateTime::RFC3339, $timestamp, new \DateTimeZone("UTC"));
+        if ($date === false) {
+            throw new \Exception("Invalid RFC 3339 timestamp.");
+        }
+
+        $value = new \Google\Protobuf\Timestamp();
+        $seconds = $date->format("U");
+        $value->setSeconds($seconds);
+        $value->setNanos($nanoseconds);
+        return $value;
+    }
+    
+    public static function formatTimestamp($value)
+    {
+        $nanoseconds = static::getNanosecondsForTimestamp($value->getNanos());
+        if (!empty($nanoseconds)) {
+            $nanoseconds = ".".$nanoseconds;
+        }
+        $date = new \DateTime('@'.$value->getSeconds(), new \DateTimeZone("UTC"));
+        return $date->format("Y-m-d\TH:i:s".$nanoseconds."\Z");
+    }
+
+    public static function getNanosecondsForTimestamp($nanoseconds)
+    {
+        if ($nanoseconds == 0) {
+            return '';
+        }
+        if ($nanoseconds % static::NANOS_PER_MILLISECOND == 0) {
+            return sprintf('%03d', $nanoseconds / static::NANOS_PER_MILLISECOND);
+        }
+        if ($nanoseconds % static::NANOS_PER_MICROSECOND == 0) {
+            return sprintf('%06d', $nanoseconds / static::NANOS_PER_MICROSECOND);
+        }
+        return sprintf('%09d', $nanoseconds);
+    }
 }
diff --git a/php/src/Google/Protobuf/Internal/Message.php b/php/src/Google/Protobuf/Internal/Message.php
index 8886e61..c0a3218 100644
--- a/php/src/Google/Protobuf/Internal/Message.php
+++ b/php/src/Google/Protobuf/Internal/Message.php
@@ -699,12 +699,25 @@
         switch ($field->getType()) {
             case GPBType::MESSAGE:
                 $klass = $field->getMessageType()->getClass();
-                if (!is_object($value) && !is_array($value)) {
-                    throw new \Exception("Expect message.");
-                }
                 $submsg = new $klass;
-                if (!is_null($value) &&
-                    $klass !== "Google\Protobuf\Any") {
+
+                if ($field->isTimestamp()) {
+                    if (!is_string($value)) {
+                        throw new GPBDecodeException("Expect string.");
+                    }
+                    try {
+                        $timestamp = GPBUtil::parseTimestamp($value);
+                    } catch (\Exception $e) {
+                        throw new GPBDecodeException("Invalid RFC 3339 timestamp: ".$e->getMessage());
+                    }
+
+                    $submsg->setSeconds($timestamp->getSeconds());
+                    $submsg->setNanos($timestamp->getNanos());
+                } else if ($klass !== "Google\Protobuf\Any") {
+                    if (!is_object($value) && !is_array($value)) {
+                        throw new GPBDecodeException("Expect message.");
+                    }
+
                     $submsg->mergeFromJsonArray($value);
                 }
                 return $submsg;
@@ -1038,22 +1051,28 @@
      */
     public function serializeToJsonStream(&$output)
     {
-        $output->writeRaw("{", 1);
-        $fields = $this->desc->getField();
-        $first = true;
-        foreach ($fields as $field) {
-            if ($this->existField($field)) {
-                if ($first) {
-                    $first = false;
-                } else {
-                    $output->writeRaw(",", 1);
-                }
-                if (!$this->serializeFieldToJsonStream($output, $field)) {
-                    return false;
+        if (get_class($this) === 'Google\Protobuf\Timestamp') {
+            $timestamp = GPBUtil::formatTimestamp($this);
+            $timestamp = json_encode($timestamp);
+            $output->writeRaw($timestamp, strlen($timestamp));
+        } else {
+            $output->writeRaw("{", 1);
+            $fields = $this->desc->getField();
+            $first = true;
+            foreach ($fields as $field) {
+                if ($this->existField($field)) {
+                    if ($first) {
+                        $first = false;
+                    } else {
+                        $output->writeRaw(",", 1);
+                    }
+                    if (!$this->serializeFieldToJsonStream($output, $field)) {
+                        return false;
+                    }
                 }
             }
+            $output->writeRaw("}", 1);
         }
-        $output->writeRaw("}", 1);
         return true;
     }
 
@@ -1341,6 +1360,7 @@
     private function fieldJsonByteSize($field)
     {
         $size = 0;
+
         if ($field->isMap()) {
             $getter = $field->getGetter();
             $values = $this->$getter();
@@ -1443,21 +1463,26 @@
     public function jsonByteSize()
     {
         $size = 0;
-
-        // Size for "{}".
-        $size += 2;
-
-        $fields = $this->desc->getField();
-        $count = 0;
-        foreach ($fields as $field) {
-            $field_size = $this->fieldJsonByteSize($field);
-            $size += $field_size;
-            if ($field_size != 0) {
-              $count++;
+        if (get_class($this) === 'Google\Protobuf\Timestamp') {
+            $timestamp = GPBUtil::formatTimestamp($this);
+            $timestamp = json_encode($timestamp);
+            $size += strlen($timestamp);
+        } else {
+            // Size for "{}".
+            $size += 2;
+            
+            $fields = $this->desc->getField();
+            $count = 0;
+            foreach ($fields as $field) {
+                $field_size = $this->fieldJsonByteSize($field);
+                $size += $field_size;
+                if ($field_size != 0) {
+                  $count++;
+                }
             }
+            // size for comma
+            $size += $count > 0 ? ($count - 1) : 0;
         }
-        // size for comma
-        $size += $count > 0 ? ($count - 1) : 0;
         return $size;
     }
 }