source: branches/version-2_12-dev/data/class/helper/SC_Helper_Transform.php @ 21775

Revision 21775, 22.8 KB checked in by sunat134, 12 years ago (diff)

#1736 Smartyのコメントはあらかじめ削除するように
#1751 Transform対象のテンプレートに<HTML>タグが含まれる場合に対応

Line 
1<?php
2/*
3 * This file is part of EC-CUBE
4 *
5 * Copyright(c) 2000-2012 LOCKON CO.,LTD. All Rights Reserved.
6 *
7 * http://www.lockon.co.jp/
8 *
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 2
12 * of the License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22 */
23
24/**
25 * テンプレートをDOM変形するためのヘルパークラス
26 *
27 * @package Helper
28 * @version $Id$
29 */
30class SC_Helper_Transform {
31    protected $objDOM;
32    protected $arrSmartyTagsOrg;
33    protected $arrSmartyTagsSub;
34    protected $smarty_tags_idx;
35    protected $arrErr;
36    protected $arrElementTree;
37    protected $arrSelectElements;
38    protected $html_source;
39    protected $header_source;
40    protected $footer_source;
41    protected $search_depth;
42
43    const ERR_TARGET_ELEMENT_NOT_FOUND = 1;
44
45    /**
46     * SmartyのHTMLソースをDOMに変換しておく
47     *
48     * @param string $source 変形対象のテンプレート
49     * @return void
50     */
51    public function __construct($source) {
52        $this->objDOM = new DOMDocument();
53        $this->objDOM->strictErrorChecking = false;
54        $this->snip_count      = 0;
55        $this->smarty_tags_idx = 0;
56        $this->arrErr          = array();
57        $this->arrElementTree  = array();
58        $this->arrSelectElements = array();
59        $this->html_source = $source;
60        $this->header_source = NULL;
61        $this->footer_source = NULL;
62        $this->search_depth = 0;
63
64        if (!in_array(mb_detect_encoding($source), array('ASCII', 'UTF-8'))) {
65            SC_Utils_Ex::sfDispSiteError(FREE_ERROR_MSG, '', true, 'テンプレートの文字コードがUTF-8ではありません');
66        }
67       
68        // Smartyのコメントを削除
69        $source = preg_replace(
70            '/<\!--{\*.+?\*\}-->/s',
71            '',
72            $source
73        );
74        // JavaScript内にSmartyのタグが存在するものを、コメント形式に置換
75        $source = preg_replace_callback(
76            '/<script.+?\/script>/s',
77            array($this, 'lfCaptureSmartyTags2Comment'),
78            $source
79        );
80        // HTMLタグ内にSmartyのタグが存在するものを、まず置換する
81        $source = preg_replace_callback(
82            '/<(?:[^<>]*?(?:(<\!--\{.+?\}-->)|(?R))[^<>]*?)*?>/s',
83            array($this, 'lfCaptureSmartyTagsInTag'),
84            $source
85        );
86        // 通常のノードに属する部分を、コメント形式に置換
87        $source = preg_replace_callback(
88            '/<\!--{.+?\}-->/s',
89            array($this, 'lfCaptureSmartyTags2Comment'),
90            $source
91        );
92
93        // BODYタグの外側は退避させる
94        if (preg_match('/^(.*?<body[^>]*>)(.+)(<\/body>.*)$/is', $source, $arrMatches)) {
95            $this->header_source = $arrMatches[1];
96            $source = $arrMatches[2];
97            $this->footer_source = $arrMatches[3];
98        }
99       
100        $source = '<meta http-equiv="content-type" content="text/html; charset=UTF-8" /><html><body><!--TemplateTransformer start-->'.$source.'<!--TemplateTransformer end--></body></html>';
101        @$this->objDOM->loadHTML($source);
102        $this->lfScanChild($this->objDOM);
103    }
104
105
106    /**
107     * jQueryライクなセレクタを用いてエレメントを選択する
108     *
109     * @param string  $selector      セレクタ
110     * @param integer $index         インデックス(指定がある場合)
111     * @param boolean $require       エレメントが見つからなかった場合、エラーとするか
112     * @param string  $err_msg       エラーメッセージ
113     * @return SC_Helper_Transformオブジェクト
114     */
115    public function select($selector, $index = NULL, $require = true, $err_msg = NULL) {
116        $this->arrSelectElements = array();
117        $this->search_depth = 0;
118
119        $regex = $this->lfSelector2Regex($selector);    // セレクタをツリー検索用正規表現に変換
120
121        $cur_idx = 0;
122        // ツリーを初めから全検索する
123        for ($iLoop=0; $iLoop < count($this->arrElementTree); $iLoop++) {
124            if (preg_match($regex, $this->arrElementTree[$iLoop][0])) {
125                // インデックスが指定されていない(見つけたエレメント全て)、もしくは指定されたインデックスなら選択する
126                if (is_null($index) || $cur_idx == $index) {
127                    $this->lfAddElement($iLoop, $this->arrElementTree[$iLoop]);
128                }
129                $cur_idx++;
130            }
131        }
132
133        // 見つからなかった場合エラーとするならエラーを記録する
134        if ($require && $cur_idx == 0) {
135            $this->lfSetError(
136                $selector,
137                self::ERR_TARGET_ELEMENT_NOT_FOUND,
138                $err_msg
139            );
140        }
141
142        return $this;
143    }
144
145
146    /**
147     * jQueryライクなセレクタを用いて、選択したエレメント内をさらに絞り込む
148     *
149     * @param string  $selector      セレクタ
150     * @param integer $index         インデックス(指定がある場合)
151     * @param boolean $require       エレメントが見つからなかった場合、エラーとするか
152     * @param string  $err_msg       エラーメッセージ
153     * @return SC_Helper_Transformオブジェクト
154     */
155    public function find($selector, $index = NULL, $require = true, $err_msg = NULL) {
156        $arrParentElements = $this->arrSelectElements[$this->search_depth];
157        $this->search_depth++;
158        $this->arrSelectElements[$this->search_depth] = array();
159
160        foreach ($arrParentElements as $key => &$objElement) {
161            $regex = $this->lfSelector2Regex($selector, $objElement[0]);    // セレクタをツリー検索用正規表現に変換(親要素のセレクタを頭に付ける)
162
163            $cur_idx = 0;
164            // 親エレメント位置からツリーを検索する
165            for ($iLoop=$objElement[0]; $iLoop < count($this->arrElementTree); $iLoop++) {
166                if (preg_match($regex, $this->arrElementTree[$iLoop][0])) {
167                    // インデックスが指定されていない(見つけたエレメント全て)、もしくは指定されたインデックスなら選択する
168                    if (is_null($index) || $cur_idx == $index) {
169                        $this->lfAddElement($iLoop, $this->arrElementTree[$iLoop]);
170                    }
171                    $cur_idx++;
172                }
173            }
174        }
175
176        // 見つからなかった場合エラーとするならエラーを記録する
177        if ($require && count($this->arrSelectElements[$this->search_depth]) == 0) {
178            $this->lfSetError(
179                $selector,
180                self::ERR_TARGET_ELEMENT_NOT_FOUND,
181                $err_msg
182            );
183        }
184
185        return $this;
186    }
187
188
189    /**
190     * 選択状態を指定数戻す
191     *
192     * @param int $back_num 選択状態を戻す数
193     * @return SC_Helper_Transformオブジェクト
194     */
195    public function end($back_num = 1) {
196        if ($this->search_depth >= $back_num) {
197            $this->search_depth -= $back_num;
198        } else {
199            $this->search_depth = 0;
200        }
201
202        return $this;
203    }
204
205
206    /**
207     * 要素の前にHTMLを挿入
208     *
209     * @param string $html_snip 挿入するHTMLの断片
210     * @return SC_Helper_Transformオブジェクト
211     */
212    public function insertBefore($html_snip) {
213        foreach ($this->arrSelectElements[$this->search_depth] as $key => $objElement) {
214            $this->lfSetTransform('insertBefore', $objElement[0], $html_snip);
215        }
216        return $this;
217    }
218
219
220    /**
221     * 要素の後にHTMLを挿入
222     *
223     * @param string $html_snip 挿入するHTMLの断片
224     * @return SC_Helper_Transformオブジェクト
225     */
226    public function insertAfter($html_snip) {
227        foreach ($this->arrSelectElements[$this->search_depth] as $key => $objElement) {
228            $this->lfSetTransform('insertAfter', $objElement[0], $html_snip);
229        }
230        return $this;
231    }
232
233
234    /**
235     * 要素の先頭にHTMLを挿入
236     *
237     * @param string $html_snip 挿入するHTMLの断片
238     * @return SC_Helper_Transformオブジェクト
239     */
240    public function appendFirst($html_snip) {
241        foreach ($this->arrSelectElements[$this->search_depth] as $key => $objElement) {
242            $this->lfSetTransform('appendFirst', $objElement[0], $html_snip);
243        }
244        return $this;
245    }
246
247
248    /**
249     * 要素の末尾にHTMLを挿入
250     *
251     * @param string $html_snip 挿入するHTMLの断片
252     * @return SC_Helper_Transformオブジェクト
253     */
254    public function appendChild($html_snip) {
255        foreach ($this->arrSelectElements[$this->search_depth] as $key => $objElement) {
256            $this->lfSetTransform('appendChild', $objElement[0], $html_snip);
257        }
258        return $this;
259    }
260
261
262    /**
263     * 要素を指定したHTMLに置換
264     *
265     * @param string $html_snip 置換後のHTMLの断片
266     * @return SC_Helper_Transformオブジェクト
267     */
268    public function replaceElement($html_snip) {
269        foreach ($this->arrSelectElements[$this->search_depth] as $key => &$objElement) {
270            $this->lfSetTransform('replaceElement', $objElement[0], $html_snip);
271        }
272        return $this;
273    }
274
275
276    /**
277     * 要素を削除する
278     *
279     * @return SC_Helper_Transformオブジェクト
280     */
281    public function removeElement() {
282        foreach ($this->arrSelectElements[$this->search_depth] as $key => &$objElement) {
283            $this->lfSetTransform('replaceElement', $objElement[0], '');
284        }
285        return $this;
286    }
287
288
289    /**
290     * HTMLに戻して、Transform用に付けたマーカーを削除し、Smartyのタグを復元する
291     *
292     * @return string トランスフォーム済みHTML。まったくトランスフォームが行われなかった場合は元のHTMLを返す。。
293     */
294    public function getHTML() {
295        if (count($this->arrErr)) {
296            // エラーメッセージ組み立て
297            $err_msg = '';
298            foreach ($this->arrErr as $arrErr) {
299                if ($arrErr['err_msg']) {
300                    $err_msg .= '<br />'.$arrErr['err_msg'];
301                } else {
302                    if ($arrErr['type'] == self::ERR_TARGET_ELEMENT_NOT_FOUND) {
303                        $err_msg .= "<br />${arrErr['selector']} が存在しません";
304                    } else {
305                        $err_msg .= '<br />'.print_r($arrErr, true);
306                    }
307                }
308            }
309            // エラー画面表示
310            SC_Utils_Ex::sfDispSiteError(FREE_ERROR_MSG, '', true, 'テンプレートの操作に失敗しました。' . $err_msg);
311        } elseif ($this->snip_count) {
312            $html = $this->objDOM->saveHTML();
313            $html = preg_replace('/^.*<\!--TemplateTransformer start-->/s', '', $html);
314            $html = preg_replace('/<\!--TemplateTransformer end-->.*$/s', '', $html);
315            $html = $this->header_source.$html.$this->footer_source;
316            $html = str_replace($this->arrSmartyTagsSub, $this->arrSmartyTagsOrg, $html);
317            return $html;
318        } else {
319            return $this->html_source;
320        }
321    }
322
323
324
325
326    /**
327     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
328     *
329     * コメント形式への置換
330     *
331     * @param array $arrMatches マッチしたタグの情報
332     * @return string 代わりの文字列
333     */
334    protected function lfCaptureSmartyTags2Comment(array $arrMatches) {
335        $substitute_tag = sprintf('<!--###%08d###-->', $this->smarty_tags_idx);
336        $this->arrSmartyTagsOrg[$this->smarty_tags_idx] = $arrMatches[0];
337        $this->arrSmartyTagsSub[$this->smarty_tags_idx] = $substitute_tag;
338        $this->smarty_tags_idx++;
339        return $substitute_tag;
340    }
341
342
343    /**
344     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
345     *
346     * HTMLエレメント内部の処理
347     *
348     * @param array $arrMatches マッチしたタグの情報
349     * @return string 代わりの文字列
350     */
351    protected function lfCaptureSmartyTagsInTag(array $arrMatches) {
352        // Smartyタグ内のクォートを処理しやすいよう、いったんダミーのタグに
353        $html = preg_replace_callback('/<\!--{.+?\}-->/s', array($this, 'lfCaptureSmartyTags2Temptag'), $arrMatches[0]);
354        $html = preg_replace_callback('/\"[^"]*?\"/s', array($this, 'lfCaptureSmartyTagsInQuote'), $html);
355        $html = preg_replace_callback('/###TEMP(\d{8})###/s', array($this, 'lfCaptureSmartyTags2Attr'), $html);
356        return $html;
357    }
358
359
360    /**
361     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
362     *
363     * ダミーへの置換実行
364     *
365     * @param array $arrMatches マッチしたタグの情報
366     * @return string 代わりの文字列
367     */
368    protected function lfCaptureSmartyTags2Temptag(array $arrMatches) {
369        $substitute_tag = sprintf('###TEMP%08d###', $this->smarty_tags_idx);
370        $this->arrSmartyTagsOrg[$this->smarty_tags_idx] = $arrMatches[0];
371        $this->arrSmartyTagsSub[$this->smarty_tags_idx] = $substitute_tag;
372        $this->smarty_tags_idx++;
373        return $substitute_tag;
374    }
375
376
377    /**
378     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
379     *
380     * クォート内(=属性値)内にあるSmartyタグ(ダミーに置換済み)を、テキストに置換
381     *
382     * @param array $arrMatches マッチしたタグの情報
383     * @return string 代わりの文字列
384     */
385    protected function lfCaptureSmartyTagsInQuote(array $arrMatches) {
386        $html = preg_replace_callback('/###TEMP(\d{8})###/s', array($this, 'lfCaptureSmartyTags2Value'), $arrMatches[0]);
387        return $html;
388    }
389
390
391    /**
392     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
393     *
394     * テキストへの置換実行
395     *
396     * @param array $arrMatches マッチしたタグの情報
397     * @return string 代わりの文字列
398     */
399    protected function lfCaptureSmartyTags2Value(array $arrMatches) {
400        $tag_idx = (int)$arrMatches[1];
401        $substitute_tag = sprintf('###%08d###', $tag_idx);
402        $this->arrSmartyTagsSub[$tag_idx] = $substitute_tag;
403        return $substitute_tag;
404    }
405
406
407    /**
408     * DOMの処理の邪魔になるSmartyのタグを代理文字に置換する preg_replace_callback のコールバック関数
409     *
410     * エレメント内部にあって、属性値ではないものを、ダミーの属性として置換
411     *
412     * @param array $arrMatches マッチしたタグの情報
413     * @return string 代わりの文字列
414     */
415    protected function lfCaptureSmartyTags2Attr(array $arrMatches) {
416        $tag_idx = (int)$arrMatches[1];
417        $substitute_tag = sprintf('rel%08d="######"', $tag_idx);
418        $this->arrSmartyTagsSub[$tag_idx] = $substitute_tag;
419        return ' '.$substitute_tag.' '; // 属性はパース時にスペースが詰まるので、こちらにはスペースを入れておく
420    }
421
422
423    /**
424     * DOM Element / Document を走査し、name、class別に分類する
425     *
426     * @param  DOMNode $objDOMElement DOMNodeオブジェクト
427     * @return void
428     */
429    protected function lfScanChild(DOMNode $objDOMElement, $parent_selector = '') {
430        $objNodeList = $objDOMElement->childNodes;
431        if (is_null($objNodeList)) return;
432
433        foreach ($objNodeList as $element) {
434            // DOMElementのみ取り出す
435            if ($element instanceof DOMElement) {
436                $arrAttr = array();
437                $arrAttr[] = $element->tagName;
438                if (method_exists($element, 'getAttribute')) {
439                    // idを持っていればidを付加する
440                    if ($element->hasAttribute('id'))
441                        $arrAttr[] = '#'.$element->getAttribute('id');
442                    // classを持っていればclassを付加する(複数の場合は複数付加する)
443                    if ($element->hasAttribute('class')) {
444                        $arrClasses = preg_split('/\s+/', $element->getAttribute('class'));
445                        foreach ($arrClasses as $classname) $arrAttr[] = '.'.$classname;
446                    }
447                }
448                // 親要素のセレクタを付けてツリーへ登録する
449                $this_selector = $parent_selector.' '.implode('', $arrAttr);
450                $this->arrElementTree[] = array($this_selector, $element);
451                // エレメントが子孫要素を持っていればさらに調べる
452                if ($element->hasChildNodes()) $this->lfScanChild($element, $this_selector);
453            }
454        }
455    }
456
457
458    /**
459     * セレクタ文字列をツリー検索用の正規表現に変換する
460     *
461     * @param string $selector      セレクタ
462     * @param string $parent_index  セレクタ検索時の親要素の位置(子孫要素検索のため)
463     * @return string 正規表現文字列
464     */
465    protected function lfSelector2Regex($selector, $parent_index = NULL){
466        // jQueryライクなセレクタを正規表現に
467        $selector = preg_replace('/ *> */', ' >', $selector);   // 子セレクタをツリー検索用に 「A >B」の記法にする
468        $regex = '/';
469        if (!is_null($parent_index)) $regex .= preg_quote($this->arrElementTree[$parent_index][0], '/');    // (親要素の指定(絞り込み時)があれば頭に付加する(特殊文字はエスケープ)
470        $arrSelectors = explode(' ', $selector);
471        foreach ($arrSelectors as $sub_selector) {
472            if (preg_match('/^(>?)([\w\-]+)?(#[\w\-]+)?(\.[\w\-]+)*$/', $sub_selector, $arrMatch)) {
473                // 子セレクタ
474                if (isset($arrMatch[1]) && $arrMatch[1]) $regex .= ' ';
475                else $regex .= '.* ';
476                // タグ名
477                if (isset($arrMatch[2]) && $arrMatch[2]) $regex .= preg_quote($arrMatch[2], '/');
478                else $regex .= '([\w\-]+)?';
479                // id
480                if (isset($arrMatch[3]) && $arrMatch[3]) $regex .= preg_quote($arrMatch[3], '/');
481                else $regex .= '(#(\w|\-|#{3}[0-9]{8}#{3})+)?';
482                // class
483                if (isset($arrMatch[4]) && $arrMatch[4]) $regex .= '(\.(\w|\-|#{3}[0-9]{8}#{3})+)*'.preg_quote($arrMatch[4], '/').'(\.(\w|\-|#{3}[0-9]{8}#{3})+)*'; // class指定の時は前後にもclassが付いているかもしれない
484                else $regex .= '(\.(\w|\-|#{3}[0-9]{8}#{3})+)*';
485            }
486        }
487        $regex .= '$/i';
488
489        return $regex;
490    }
491
492
493    /**
494     * 見つかった要素をプロパティに登録
495     *
496     * @param integer $elementNo  エレメントのインデックス
497     * @param array   $arrElement インデックスとDOMオブジェクトをペアとした配列
498     * @return void
499     */
500    protected function lfAddElement($elementNo, array &$arrElement) {
501        if (!array_key_exists($arrElement[0], $this->arrSelectElements[$this->search_depth])) {
502            $this->arrSelectElements[$this->search_depth][$arrElement[0]] = array($elementNo, &$arrElement[1]);
503        }
504    }
505
506
507    /**
508     * DOMを用いた変形を実行する
509     *
510     * @param string $mode       実行するメソッドの種類
511     * @param string $target_key 対象のエレメントの完全なセレクタ
512     * @param string $html_snip  HTMLコード
513     * @return boolean
514     */
515    protected function lfSetTransform($mode, $target_key, $html_snip) {
516
517        $substitute_tag = sprintf('<!--###%08d###-->', $this->smarty_tags_idx);
518        $this->arrSmartyTagsOrg[$this->smarty_tags_idx] = $html_snip;
519        $this->arrSmartyTagsSub[$this->smarty_tags_idx] = $substitute_tag;
520        $this->smarty_tags_idx++;
521
522        $this->objDOM->createDocumentFragment();
523        $objSnip = $this->objDOM->createDocumentFragment();
524        $objSnip->appendXML($substitute_tag);
525
526        $objElement = false;
527        if (isset($this->arrElementTree[$target_key]) && $this->arrElementTree[$target_key][0]) {
528            $objElement = &$this->arrElementTree[$target_key][1];
529        }
530
531        if (!$objElement) return false;
532
533        try {
534            switch ($mode) {
535                case 'appendFirst':
536                    if ($objElement->hasChildNodes()) {
537                        $objElement->insertBefore($objSnip, $objElement->firstChild);
538                    } else {
539                        $objElement->appendChild($objSnip);
540                    }
541                    break;
542                case 'appendChild':
543                    $objElement->appendChild($objSnip);
544                    break;
545                case 'insertBefore':
546                    if (!is_object($objElement->parentNode)) return false;
547                    $objElement->parentNode->insertBefore($objSnip, $objElement);
548                    break;
549                case 'insertAfter':
550                    if ($objElement->nextSibling) {
551                         $objElement->parentNode->insertBefore($objSnip, $objElement->nextSibling);
552                    } else {
553                         $objElement->parentNode->appendChild($objSnip);
554                    }
555                    break;
556                case 'replaceElement':
557                    if (!is_object($objElement->parentNode)) return false;
558                    $objElement->parentNode->replaceChild($objSnip, $objElement);
559                    break;
560                default:
561                    break;
562            }
563            $this->snip_count++;
564        }
565        catch (Exception $e) {
566            SC_Utils_Ex::sfDispSiteError(FREE_ERROR_MSG, '', true, 'テンプレートの操作に失敗しました。');
567        }
568
569        return true;
570    }
571
572
573    /**
574     * セレクタエラーを記録する
575     *
576     * @param string  $selector    セレクタ
577     * @param integer $type        エラーの種類
578     * @param string  $err_msg     エラーメッセージ
579     * @return void
580     */
581    protected function lfSetError($selector, $type, $err_msg = NULL) {
582        $this->arrErr[] = array(
583            'selector'    => $selector,
584            'type'        => $type,
585            'err_msg'     => $err_msg
586        );
587    }
588}
Note: See TracBrowser for help on using the repository browser.