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

Revision 21767, 22.2 KB checked in by shutta, 10 years ago (diff)

#1296 SC_* のコンストラクタの拡張が無視される
コンストラクタ名を、PHP4の記法(クラス名と同名の関数名)から、PHP5以降で標準的なconstructに変更。

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