1<?php
2/**
3 * PHPTAL templating engine
4 *
5 * PHP Version 5
6 *
7 * @category HTML
8 * @package  PHPTAL
9 * @author   Laurent Bedubourg <lbedubourg@motion-twin.com>
10 * @author   Kornel Lesiński <kornel@aardvarkmedia.co.uk>
11 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
12 * @version  SVN: $Id$
13 * @link     http://phptal.org/
14 */
15
16/**
17 * Exception that is related to location within a template.
18 * You can check srcFile and srcLine to find source of the error.
19 *
20 * @package PHPTAL
21 * @subpackage Exception
22 */
23class PHPTAL_TemplateException extends PHPTAL_Exception
24{
25    public $srcFile;
26    public $srcLine;
27    private $is_src_accurate;
28
29    public function __construct($msg, $srcFile='', $srcLine=0)
30    {
31        parent::__construct($msg);
32
33        if ($srcFile && $srcLine) {
34            $this->srcFile = $srcFile;
35            $this->srcLine = $srcLine;
36            $this->is_src_accurate = true;
37        } else {
38            $this->is_src_accurate = $this->setTemplateSource();
39        }
40
41        if ($this->is_src_accurate) {
42            $this->file = $this->srcFile;
43            $this->line = (int)$this->srcLine;
44        }
45    }
46
47    public function __toString()
48    {
49        if (!$this->srcFile || $this->is_src_accurate) return parent::__toString();
50        return "From {$this->srcFile} around line {$this->srcLine}\n".parent::__toString();
51    }
52
53    /**
54     * Set new TAL source file/line if it isn't known already
55     */
56    public function hintSrcPosition($srcFile, $srcLine)
57    {
58        if ($srcFile && $srcLine) {
59            if (!$this->is_src_accurate) {
60                $this->srcFile = $srcFile;
61                $this->srcLine = $srcLine;
62                $this->is_src_accurate = true;
63            } else if ($this->srcLine <= 1 && $this->srcFile === $srcFile) {
64                $this->srcLine = $srcLine;
65            }
66        }
67
68        if ($this->is_src_accurate) {
69            $this->file = $this->srcFile;
70            $this->line = (int)$this->srcLine;
71        }
72    }
73
74    private function isTemplatePath($path)
75    {
76        return preg_match('/[\\\\\/]tpl_[0-9a-f]{8}_[^\\\\]+$/', $path);
77    }
78
79    private function findFileAndLine()
80    {
81        if ($this->isTemplatePath($this->file)) {
82            return array($this->file, $this->line);
83        }
84
85        $eval_line = 0;
86        $eval_path = NULL;
87
88        // searches backtrace to find template file
89        foreach($this->getTrace() as $tr) {
90            if (!isset($tr['file'],$tr['line'])) continue;
91
92            if ($this->isTemplatePath($tr['file'])) {
93                return array($tr['file'], $tr['line']);
94            }
95
96            // PHPTAL.php uses eval() on first run to catch fatal errors. This makes template path invisible.
97            // However, function name matches template path and eval() is visible in backtrace.
98            if (false !== strpos($tr['file'], 'eval()')) {
99                $eval_line = $tr['line'];
100            }
101            else if ($eval_line && isset($tr['function'],$tr['args'],$tr['args'][0]) &&
102                $this->isTemplatePath("/".$tr['function'].".php") && $tr['args'][0] instanceof PHPTAL) {
103                return array($tr['args'][0]->getCodePath(), $eval_line);
104            }
105        }
106
107        return array(NULL,NULL);
108    }
109
110    /**
111     * sets srcLine and srcFile to template path and source line
112     * by checking error backtrace and scanning PHP code file
113     *
114     * @return bool true if found accurate data
115     */
116    private function setTemplateSource()
117    {
118        // not accurate, but better than null
119        $this->srcFile = $this->file;
120        $this->srcLine = $this->line;
121
122        list($file,$line) = $this->findFileAndLine();
123
124        if (NULL === $file) {
125            return false;
126        }
127
128        // this is not accurate yet, hopefully will be overwritten later
129        $this->srcFile = $file;
130        $this->srcLine = $line;
131
132        $lines = @file($file);
133        if (!$lines) {
134            return false;
135        }
136
137        $found_line=false;
138        $found_file=false;
139
140        // scan lines backwards looking for "from line" comments
141        $end = min(count($lines), $line)-1;
142        for($i=$end; $i >= 0; $i--) {
143            if (preg_match('/tag "[^"]*" from line (\d+)/', $lines[$i], $m)) {
144                $this->srcLine = intval($m[1]);
145                $found_line=true;
146                break;
147            }
148        }
149
150        foreach(preg_grep('/Generated by PHPTAL from/',$lines) as $line) {
151            if (preg_match('/Generated by PHPTAL from (.*) \(/', $line, $m)) {
152                $this->srcFile = $m[1];
153                $found_file=true;
154                break;
155            }
156        }
157
158        return $found_line && $found_file;
159    }
160}
161