PHPTemplate::offsetExists()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 4
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
1
<?php
2
3
namespace Ayesh\PHPTemplate;
4
5
use Ayesh\PHPTemplate\Exception\BadMethodCallException;
6
use Ayesh\PHPTemplate\Exception\TemplateError;
7
use Ayesh\PHPTemplate\Exception\TemplateNotFound;
8
9
final class PHPTemplate implements \ArrayAccess {
10
11
  /**
12
   * @var array Raw user-provided variables.
13
   */
14
  private $vars;
15
16
  /**
17
   * @var string Template file path.
18
   */
19
  private $template_file;
20
21
  /**
22
   * @var array Allowed url protocols.
23
   */
24
  private const ALLOWED_URL_PROTOCOLS = [
25
    'http',
26
    'https',
27
    'ftp'
28
  ];
29
30
  public function __construct(string $template_file = null, array $vars = []) {
31
    $this->template_file = $template_file;
32
    $this->set($vars);
33
  }
34
35
  /**
36
   * Set view data. This overwrites existing variables.
37
   *
38
   * @param array $vars The view data
39
   * @return void
40
   */
41
  public function set(array $vars = []): void {
42
    $this->vars = $vars;
43
  }
44
45
  /**
46
   * Render template.
47
   *
48
   * @param array|null $vars The view data. If not provided, variables set in a
49
   *  provious set() call or the constructor will be used.
50
   * @return string Rendered template.
51
   * @throws \Throwable
52
   */
53
  public function render(array $vars = null): string {
54
    if (empty($this->template_file)) {
55
      throw new BadMethodCallException('Calls to render() method is illegal when a template path is not set.');
56
    }
57
    if (!file_exists($this->template_file)) {
58
      throw new TemplateNotFound(sprintf('Template %s could not be loaded.', $this->template_file));
59
    }
60
61
    if (null !== $vars) {
62
      $this->set($vars);
63
    }
64
65
    return trim(self::renderTemplate($this->template_file, $this));
66
  }
67
68
  /**
69
   * Render template.
70
   *
71
   * @param string $path The path to the template file.
72
   * @param PHPTemplate $v The PHPTemplate instance
73
   * @return string The rendered content
74
   * @throws \Throwable
75
   */
76
  private static function renderTemplate(string $path, PHPTemplate $v): string {
77
    ob_start();
78
    try {
79
      /** @noinspection PhpIncludeInspection */
80
      include $path;
81
    } catch (\Throwable $exception) {
82
      ob_end_clean();
83
      throw $exception;
84
    }
85
86
    return ob_get_clean();
87
  }
88
89
  /**
90
   * Get view data by key name WITHOUT applying any sanitization filters. Use
91
   *  array access pattern (e.g $v['var']) to benefit from automatic string
92
   *  sanitization functions.
93
   *
94
   * @param string $key The key of the user-provided to variable to fetch.
95
   * @return mixed|null The raw value, or null if not set.
96
   */
97
  public function get(string $key) {
98
    return $this->vars[$key] ?? NULL;
99
  }
100
101
  /**
102
   * Check whether an offset exists.
103
   *
104
   * @param string $offset An offset to check for
105
   * @return bool Returns true if the offset exists and is not null. False otherwise.
106
   */
107
  public function offsetExists($offset): bool {
108
    return isset($this->vars[$offset]);
109
  }
110
111
  /**
112
   * Retrieve an offset from the variables, with sanitization filters applied.
113
   *
114
   * When retrieving variables, the first character is matched against a list
115
   * of sanitization techniques provided by this library.
116
   *
117
   * If the variable starts with a colon (":"), it will be sanitized to be used
118
   *  as a URL, with all dangerous protocol neatralized.
119
   *
120
   * The exclaimation character marks the variable must be returned as-is. This
121
   *  will still make sure it's a string, but it wil not strip any dangerous
122
   *  HTML and other characters. This is useful if you want to run different
123
   *  sanitizations functionson the raw variable and want the lubrary to stay
124
   *  away.
125
   *
126
   * If the variable has no special character prefix, it will be sanitized with
127
   *  HTML, as in no HTML will be intepreted by the browser.
128
   *
129
   * @param string $offset The offset to retrieve.
130
   * @return mixed Returns the value at specified offset, with the sanitization
131
   *   filters applied based on the way their were asked.
132
   */
133
  public function offsetGet($offset) {
134
    $first_char = $offset[0];
135
136
    switch ($first_char) {
137
      case ':':
138
        return $this->url($this->getVar(substr($offset, 1)));
139
140
      case '!':
141
        return $this->getVar(substr($offset, 1));
142
      default:
143
        return $this->escape($this->getVar($offset));
144
    }
145
  }
146
147
  /**
148
   * Assign a value to the specified offset.
149
   *
150
   * @param string $offset The offset to assign the value to
151
   * @param mixed $value The value to set
152
   * @return void
153
   */
154
  public function offsetSet($offset, $value): void {
155
    $this->vars[$offset] = $value;
156
  }
157
158
  /**
159
   * Unset an offset.
160
   *
161
   * @param string $offset The offset to unset.
162
   * @return void
163
   */
164
  public function offsetUnset($offset): void {
165
    unset($this->vars[$offset]);
166
  }
167
168
  private function getVar(string $offset): string {
169
    if (isset($this->vars[$offset]) && \is_scalar($this->vars[$offset]) && !\is_bool($this->vars[$offset])) {
170
      return $this->vars[$offset];
171
    }
172
    return '';
173
  }
174
175
  /**
176
   * Html encoding.
177
   *
178
   * Escapes the provided literal string to make sure it does not contain
179
   * anything that would be interpreted as HTML. You can also use this escape
180
   * method inside HTML attributes as it would also convert single and double
181
   * quotes to HTML entities.
182
   *
183
   * @param string $input The value to encode
184
   * @return string The html encoded value
185
   */
186
  public function escape(string $input): string {
187
    return \htmlspecialchars($input, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
188
  }
189
190
  /**
191
   * URL encoding.
192
   *
193
   * Sanitizes the provided URL making sure it only contains relative paths,
194
   * or URIs whose protocol http://, https://, or ftp://. Any other protocols
195
   * such as javascript: will be removed.
196
   *
197
   * @param string $uri The url
198
   * @return string The url encoded string
199
   */
200
  public function url(string $uri): string {
201
    $allowed_protocols = array_flip(static::ALLOWED_URL_PROTOCOLS);
202
203
    // Iteratively remove any invalid protocol found.
204
    // Borrowed from Drupal.
205
    do {
206
      $before = $uri;
207
      $colon_position = strpos($uri, ':');
208
      if ($colon_position > 0) {
209
210
        // We found a colon, possibly a protocol. Verify.
211
        $protocol = substr($uri, 0, $colon_position);
212
213
        // If a colon is preceded by a slash, question mark or hash, it cannot
214
        // possibly be part of the URL scheme. This must be a relative URL, which
215
        // inherits the (safe) protocol of the base document.
216
        if (preg_match('![/?#]!', $protocol)) {
217
          break;
218
        }
219
220
        // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
221
        // (URI Comparison) scheme comparison must be case-insensitive.
222
        if (!isset($allowed_protocols[strtolower($protocol)])) {
223
          $uri = substr($uri, $colon_position + 1);
224
        }
225
      }
226
    } while ($before !== $uri);
227
    return $this->escape($uri);
228
  }
229
230
  /**
231
   * Expands the provided array into HTML attributes.
232
   *
233
   * @param array $attributes The attributes
234
   *
235
   * @return string
236
   */
237
  public function attributes(array $attributes): string {
238
    foreach ($attributes as $attribute => &$data) {
239
      $data = implode(' ', (array) $data);
240
      $data = $attribute . '="' . $this->escape($data) . '"';
241
    }
242
243
    return $attributes ? ' ' . implode(' ', $attributes) : '';
244
  }
245
246
  /**
247
   * This is a shortcut to throw an exception of type TemplateError that the
248
   * parent caller can catch. You will probably never need to use this method,
249
   * but this is an easy way to throw an error if your template cannot proceed
250
   * and you want to terminate and report it to the parent callers.
251
   *
252
   * @param string $error_message The error nessage
253
   * @param int $error_code The error code
254
   *
255
   * @throws TemplateError
256
   */
257
  public function error(string $error_message, int $error_code = 0): void {
258
    throw new TemplateError($error_message, $error_code);
259
  }
260
261
  /**
262
   * Cast to string. Always returns a blank string.
263
   *
264
   * @return string Blank string
265
   */
266
  public function __toString(): string {
267
    return '';
268
  }
269
270
  /**
271
   * Silently discard all attempts to set object properties.
272
   *
273
   * @param mixed $name The name
274
   * @param mixed $value The value
275
   * @return void
276
   */
277
  public function __set($name, $value): void {
278
  }
279
280
  /**
281
   * Silently fail all attempts to inspect any object properties.
282
   *
283
   * @param mixed $name The name
284
   * @return bool Status
285
   */
286
  public function __isset($name) {
287
    return false;
288
  }
289
290
  /**
291
   * Return an empty string when attempted to fetch object properties. This is
292
   * to discourage bypassing the ArrayAccess pattern.
293
   *
294
   * @param mixed $name The name
295
   * @return string Blank string
296
   */
297
  public function __get($name) {
298
    return '';
299
  }
300
}
301