1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Error;
13
14use Twig\Source;
15use Twig\Template;
16
17/**
18 * Twig base exception.
19 *
20 * This exception class and its children must only be used when
21 * an error occurs during the loading of a template, when a syntax error
22 * is detected in a template, or when rendering a template. Other
23 * errors must use regular PHP exception classes (like when the template
24 * cache directory is not writable for instance).
25 *
26 * To help debugging template issues, this class tracks the original template
27 * name and line where the error occurred.
28 *
29 * Whenever possible, you must set these information (original template name
30 * and line number) yourself by passing them to the constructor. If some or all
31 * these information are not available from where you throw the exception, then
32 * this class will guess them automatically.
33 *
34 * @author Fabien Potencier <fabien@symfony.com>
35 */
36class Error extends \Exception
37{
38    private $lineno;
39    private $rawMessage;
40    private ?Source $source;
41    private string $phpFile;
42    private int $phpLine;
43
44    /**
45     * Constructor.
46     *
47     * By default, automatic guessing is enabled.
48     *
49     * @param string      $message The error message
50     * @param int         $lineno  The template line where the error occurred
51     * @param Source|null $source  The source context where the error occurred
52     */
53    public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null)
54    {
55        parent::__construct('', 0, $previous);
56
57        $this->phpFile = $this->getFile();
58        $this->phpLine = $this->getLine();
59        $this->lineno = $lineno;
60        $this->source = $source;
61        $this->rawMessage = $message;
62        $this->updateRepr();
63    }
64
65    public function getRawMessage(): string
66    {
67        return $this->rawMessage;
68    }
69
70    public function getTemplateLine(): int
71    {
72        return $this->lineno;
73    }
74
75    public function setTemplateLine(int $lineno): void
76    {
77        $this->lineno = $lineno;
78        $this->updateRepr();
79    }
80
81    public function getSourceContext(): ?Source
82    {
83        return $this->source;
84    }
85
86    public function setSourceContext(?Source $source = null): void
87    {
88        $this->source = $source;
89        $this->updateRepr();
90    }
91
92    public function guess(): void
93    {
94        if ($this->lineno > -1) {
95            return;
96        }
97
98        $this->guessTemplateInfo();
99        $this->updateRepr();
100    }
101
102    public function appendMessage($rawMessage): void
103    {
104        $this->rawMessage .= $rawMessage;
105        $this->updateRepr();
106    }
107
108    private function updateRepr(): void
109    {
110        if ($this->source && $this->source->getPath()) {
111            // we only update the file and the line together
112            $this->file = $this->source->getPath();
113            if ($this->lineno > 0) {
114                $this->line = $this->lineno;
115            } else {
116                $this->line = -1;
117            }
118        }
119
120        $this->message = $this->rawMessage;
121        $last = substr($this->message, -1);
122        if ($punctuation = '.' === $last || '?' === $last ? $last : '') {
123            $this->message = substr($this->message, 0, -1);
124        }
125        if ($this->source && $this->source->getName()) {
126            $this->message .= \sprintf(' in "%s"', $this->source->getName());
127        }
128        if ($this->lineno > 0) {
129            $this->message .= \sprintf(' at line %d', $this->lineno);
130        }
131        if ($punctuation) {
132            $this->message .= $punctuation;
133        }
134    }
135
136    private function guessTemplateInfo(): void
137    {
138        // $this->source is never null here (see guess() usage in Template)
139
140        $this->lineno = 0;
141        $template = null;
142        $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
143        foreach ($backtrace as $trace) {
144            if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) {
145                $template = $trace['object'];
146
147                break;
148            }
149        }
150
151        if (null === $template) {
152            return; // Impossible to guess the info as the template was not found in the backtrace
153        }
154
155        $r = new \ReflectionObject($template);
156        $file = $r->getFileName();
157
158        $exceptions = [$e = $this];
159        while ($e = $e->getPrevious()) {
160            $exceptions[] = $e;
161        }
162
163        while ($e = array_pop($exceptions)) {
164            $traces = $e->getTrace();
165            array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]);
166            while ($trace = array_shift($traces)) {
167                if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
168                    continue;
169                }
170
171                foreach ($template->getDebugInfo() as $codeLine => $templateLine) {
172                    if ($codeLine <= $trace['line']) {
173                        // update template line
174                        $this->lineno = $templateLine;
175
176                        return;
177                    }
178                }
179            }
180        }
181    }
182}
183