1<?php
2
3/**
4 * Utilities for wrangling JSON.
5 *
6 * @task pretty Formatting JSON Objects
7 * @task internal Internals
8 */
9final class PhutilJSON extends Phobject {
10
11
12/* -(  Formatting JSON Objects  )-------------------------------------------- */
13
14
15  /**
16   * Encode an object in JSON and pretty-print it. This generates a valid JSON
17   * object with human-readable whitespace and indentation.
18   *
19   * @param   dict    An object to encode in JSON.
20   * @return  string  Pretty-printed object representation.
21   */
22  public function encodeFormatted(array $object) {
23    return $this->encodeFormattedObject($object, 0)."\n";
24  }
25
26
27  /**
28   * Encode a list in JSON and pretty-print it, discarding keys.
29   *
30   * @param list<wild> List to encode in JSON.
31   * @return string Pretty-printed list representation.
32   */
33  public function encodeAsList(array $list) {
34    return $this->encodeFormattedArray($list, 0)."\n";
35  }
36
37
38/* -(  Internals  )---------------------------------------------------------- */
39
40
41  /**
42   * Pretty-print a JSON object.
43   *
44   * @param   dict    Object to format.
45   * @param   int     Current depth, for indentation.
46   * @return  string  Pretty-printed value.
47   * @task internal
48   */
49  private function encodeFormattedObject($object, $depth) {
50    if (empty($object)) {
51      return '{}';
52    }
53
54    $pre = $this->getIndent($depth);
55    $key_pre = $this->getIndent($depth + 1);
56    $keys = array();
57    $vals = array();
58    $max = 0;
59    foreach ($object as $key => $val) {
60      $ekey = $this->encodeFormattedValue((string)$key, 0);
61      $max = max($max, strlen($ekey));
62      $keys[] = $ekey;
63      $vals[] = $this->encodeFormattedValue($val, $depth + 1);
64    }
65    $key_lines = array();
66    foreach ($keys as $k => $key) {
67      $key_lines[] = $key_pre.$key.': '.$vals[$k];
68    }
69    $key_lines = implode(",\n", $key_lines);
70
71    $out  = "{\n";
72    $out .= $key_lines;
73    $out .= "\n";
74    $out .= $pre.'}';
75
76    return $out;
77  }
78
79
80  /**
81   * Pretty-print a JSON list.
82   *
83   * @param   list    List to format.
84   * @param   int     Current depth, for indentation.
85   * @return  string  Pretty-printed value.
86   * @task internal
87   */
88  private function encodeFormattedArray($array, $depth) {
89    if (empty($array)) {
90      return '[]';
91    }
92
93    $pre = $this->getIndent($depth);
94    $val_pre = $this->getIndent($depth + 1);
95
96    $vals = array();
97    foreach ($array as $val) {
98      $vals[] = $val_pre.$this->encodeFormattedValue($val, $depth + 1);
99    }
100    $val_lines = implode(",\n", $vals);
101
102    $out  = "[\n";
103    $out .= $val_lines;
104    $out .= "\n";
105    $out .= $pre.']';
106
107    return $out;
108  }
109
110
111  /**
112   * Pretty-print a JSON value.
113   *
114   * @param   dict    Value to format.
115   * @param   int     Current depth, for indentation.
116   * @return  string  Pretty-printed value.
117   * @task internal
118   */
119  private function encodeFormattedValue($value, $depth) {
120    if (is_array($value)) {
121      if (phutil_is_natural_list($value)) {
122        return $this->encodeFormattedArray($value, $depth);
123      } else {
124        return $this->encodeFormattedObject($value, $depth);
125      }
126    } else {
127      if (defined('JSON_UNESCAPED_SLASHES')) {
128        // If we have a new enough version of PHP, disable escaping of slashes
129        // when pretty-printing values. Escaping slashes can defuse an attack
130        // where the attacker embeds "</script>" inside a JSON string, but that
131        // isn't relevant when rendering JSON for human viewers.
132        return json_encode($value, JSON_UNESCAPED_SLASHES);
133      } else {
134        return json_encode($value);
135      }
136    }
137  }
138
139
140  /**
141   * Render a string corresponding to the current indent depth.
142   *
143   * @param   int     Current depth.
144   * @return  string  Indentation.
145   * @task internal
146   */
147  private function getIndent($depth) {
148    if (!$depth) {
149      return '';
150    } else {
151      return str_repeat('  ', $depth);
152    }
153  }
154
155}
156