태그를 찾아서 변경하기 위해 preg_replace_callback을 재귀적으로 사용한다.HTML4에서는 다음과 같이 value를 사용하는 것이 허용되기 때문에 태그에 대한 정규표현식은 복잡하다.
인용부호 안의 > 문자는 ">을 제외한 모든 문자"([^>]*)를 의미하며, input 태그 안에서는 동작하면 안 된다. XML과 XHTML의 속성 값에는 <과 >을 사용하는 것이 허용되지 않는다. 이 값들은 반드시 <와 >로 사용되야 한다. HTML4가 사라지게되면 삶은 보다 단순해질 것이다.
getAttributeVal과 replaceAttributeVal 함수는 PHP에서 매우 강력한 정규표현식 함수인 preg_match_all을 사용한다. 이들 함수는 HTML 태그안의 속성들을 찾기 위해 정규식을 사용한다.
/(\w+)((\s*=\s*".*?")|(\s*=\s*".*?")|(\s*=\s*\w+)|())/s
대충 봐서는 이게 무엇을 의미하는지 이해하는 것이 쉽지 않다. 다음과 문자열이 전달된다고 가정해보자.
name="foo" value="123" style=purple checked
정규식의 첫번째 부분인 (\w+)는 name, value, style, checked 등을 검색한다. 이 표현식은 하나 이상의 "단어"들을 일치시킨다. 나머지 정규표현식은 HTML에서 속성 값을 지정할 수 있는 네 가지 방법을 지정하기 위한 것이다. (\s*=\s*".*?")는 ="foo"와 같은 형태를 찾기 위한 것이며, (\s*=\s*".*?")는 ="123"과 같은 형태를, (\s*=\s*\w+)는 =purple과 같은 형태를 찾기 위한 것이다. ()는 어떤 값도 사용하지 않는 checked와 같은 요소를 찾기 위한 것이다.
최대한 많이 일치시키려고 하기 때문에 정규표현식은 탐욕스럽지만 preg_match_all은 여러분이 원하는 작업을 정확하게 수행해줄 것이다. name="foo" value="123" style=purple checked를 전달하면 4개가 일치되었다고 알려줄 것이다.
getAttributeVal의 전체 코드는 다음과 같다.
/**
* Returns value of $attribute, given guts of an HTML tag.
* Returns false if attribute isn"t set.
* Returns empty string for no-value attributes.
*
* @param string $tag Guts of HTML tag, with or without the .
* @param string $attribute E.g. "name" or "value" or "width"
* @return string|false Returns value of attribute (or false)
*/
function getAttributeVal($tag, $attribute) {
$matches = array();
// This regular expression matches attribute="value" or
// attribute="value" or attribute=value or attribute
// It"s also constructed so $matches[1][...] will be the
// attribute names, and $matches[2][...] will be the
// attribute values.
preg_match_all("/(\w+)((\s*=\s*".*?")|(\s*=\s*\".*?\")|(\s*=\s*\w+)|())/s",
$tag, $matches, PREG_PATTERN_ORDER);
for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i]) == strtolower($attribute)) {
// Gotta trim off whitespace, = and any quotes:
$result = ltrim($matches[2][$i], " \n\r\t=");
if ($result[0] == """) { $result = trim($result, """); }
else { $result = trim($result, """); }
return $result;
}
}
return false;
}
preg_match_all에 PREG_PATTERN_ORDER를 전달하면 $matches[1][$i]에서 속성 이름을, $matches[2][$i]에서 속성 값을 반환한다. replaceAttributeVal의 코드도 모든 속성들의 위치를 찾기 위해 PREG_OFFSET_CAPTURE(PHP 4.3.0 이상에서 지원)를 전달하고, 기존 값을 변경하거나 HTML 태그에 값을 추가하기 위해 substr_replace를 사용한다는 점만 제외하면 동일하다.
정리
fillInFormValues()의 첫번째 버전은 인자들을 전역변수에 집어넣고, 이들을 실제로 처리하는 콜백 함수에 인자를 전달하는 것입니다. 모든 콜백 함수는 PHP 함수 네임스페이스(namespace)에 등록됩니다.
"helper" 클래스에서 인자와 콜백 함수를 캡슐화하였고, array( &$this, "function" )과 같이 콜백 인자를 갖는 모든 PHP 함수들이 사용하는 콜백 구문을 사용한다. fillInFormValues()는 헬퍼 객체를 생성하고, 해당 객체에서 모든 작업을 처리할 메서드를 호출한다.
function fillInFormValues($formHTML, $request = null, $formErrors = null)
{
if ($request === null) {
// magic_quotes on: gotta strip slashes:
if (get_magic_quotes_gpc()) {
function stripslashes_deep(&$val) {
$val = is_array($val) ? array_map("stripslashes_deep", $val)
: stripslashes($val);
return $val;
}
$request = stripslashes_deep($_REQUEST);
}
else {
$request = $_REQUEST;
}
}
if ($formErrors === null) { $formErrors = array(); }
$h = new fillInFormHelper($request, $formErrors);
return $h->fill($formHTML);
}
/**
* Helper class, exists to encapsulate info needed between regex callbacks.
*/
class fillInFormHelper
{
var $request; // Normally $_REQUEST, passed into constructor
var $formErrors;
var $idToNameMap; // Map form element ids to names
function fillInFormHelper($r, $e)
{
$this->request = $r;
$this->formErrors = $e;
}
function fill($formHTML)
{
$s = fillInFormHelper::getTagPattern("input");
$formHTML = preg_replace_callback("/$s/is",
array(&$this, "fillInInputTag"), $formHTML);
// Using simpler regex for textarea/select/label, because in practice
// they never have >"s inside them:
$formHTML = preg_replace_callback("!()!is",
array(&$this, "fillInTextArea"), $formHTML);
$formHTML = preg_replace_callback("!(]*>))(.*?)( )!is",
array(&$this, "fillInSelect"), $formHTML);
// Form errors: tag with class="error", and fill in
// with form error messages.
$formHTML = preg_replace_callback("!]*)>!is",
array(&$this, "fillInLabel"), $formHTML);
$formHTML = preg_replace_callback("!!is",
array(&$this, "getErrorList"), $formHTML);
return $formHTML;
}
/**
* Returns pattern to match given a HTML/XHTML/XML tag.
* NOTE: Setup so only the whole expression is captured
* (subpatterns use (?: ...) so they don"t catpure).
* Inspired by http://www.cs.sfu.ca/~cameron/REX.html
*
* @param string $tag E.g. "input"
* @return string $pattern
*/
function getTagPattern($tag)
{
$p = "("; // This is a hairy regex, so build it up bit-by-bit:
$p .= "(?is-U)"; // Set options: case-insensitive, multiline, greedy
$p .= "<$tag"; // Match or />
$p .= ")";
return $p;
}
/**
* Returns value of $attribute, given guts of an HTML tag.
* Returns false if attribute isn"t set.
* Returns empty string for no-value attributes.
*
* @param string $tag Guts of HTML tag, with or without the .
* @param string $attribute E.g. "name" or "value" or "width"
* @return string|false Returns value of attribute (or false)
*/
function getAttributeVal($tag, $attribute) {
$matches = array();
// This regular expression matches attribute="value" or
// attribute="value" or attribute=value or attribute
// It"s also constructed so $matches[1][...] will be the
// attribute names, and $matches[2][...] will be the
// attribute values.
preg_match_all("/(\w+)((\s*=\s*".*?")|(\s*=\s*\".*?\")|(\s*=\s*\w+)|())/s",
$tag, $matches, PREG_PATTERN_ORDER);
for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i]) == strtolower($attribute)) {
// Gotta trim off whitespace, = and any quotes:
$result = ltrim($matches[2][$i], " \n\r\t=");
if ($result[0] == """) { $result = trim($result, """); }
else { $result = trim($result, """); }
return $result;
}
}
return false;
}
/**
* Returns new guts for HTML tag, with an attribute replaced
* with a new value. Pass null for new value to remove the
* attribute completely.
*
* @param string $tag Guts of HTML tag.
* @param string $attribute E.g. "name" or "value" or "width"
* @param string $newValue
* @return string
*/
function replaceAttributeVal($tag, $attribute, $newValue) {
if ($newValue === null) {
$pEQv = "";
}
else {
// htmlspecialchars here to avoid potential cross-site-scripting attacks:
$newValue = htmlspecialchars($newValue);
$pEQv = $attribute."="".$newValue.""";
}
// Same regex as getAttribute, but we wanna capture string offsets
// so we can splice in the new attribute="value":
preg_match_all("/(\w+)((\s*=\s*".*?")|(\s*=\s*\".*?\")|(\s*=\s*\w+)|())/s",
$tag, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i][0]) == strtolower($attribute)) {
$spliceStart = $matches[0][$i][1];
$spliceLength = strlen($matches[0][$i][0]);
$result = substr_replace($tag, $pEQv, $spliceStart, $spliceLength);
return $result;
}
}
if (empty($pEQv)) { return $tag; }
// No match: add attribute="newval" to $tag (before closing tag, if any):
$closed = preg_match("!(.*?)((>|(/>))\s*)$!s", $tag, $matches);
if ($closed) {
return $matches[1] . " $pEQv" . $matches[2];
}
return "$tag $pEQv";
}
/**
* Returns modified tag, based on values in $request.
*
* @param array $matches
* @return string Returns new guts.
*/
function fillInInputTag($matches) {
$tag = $matches[0];
$type = fillInFormHelper::getAttributeVal($tag, "type");
if (empty($type)) { $type = "text"; }
$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $tag; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }
switch ($type) {
/*
* Un-comment this out at your own risk (users shouldn"t be
* able to modify hidden fields):
* case "hidden":
*/
case "text":
case "password":
if (!array_key_exists($name, $this->request)) {
return $tag;
}
return fillInFormHelper::replaceAttributeVal($tag, "value", $this->request[$name]);
break;
case "radio":
case "checkbox":
$value = fillInFormHelper::getAttributeVal($tag, "value");
if (empty($value)) { $value = "on"; }
if (strpos($name, "[]")) {
$name = str_replace("[]", "", $name);
}
if (!array_key_exists($name, $this->request)) {
return fillInFormHelper::replaceAttributeVal($tag, "checked", null);
}
$vals = (is_array($this->request[$name])?$this->request[$name]:array($this->request[$name]));
if (in_array($value, $vals)) {
return fillInFormHelper::replaceAttributeVal($tag, "checked", "checked");
}
return fillInFormHelper::replaceAttributeVal($tag, "checked", null);
}
return $tag;
}
/**
* Returns modified tag, based on values in $request.
*
* @param array $matches
* @return string Returns new value.
*/
function fillInTextArea($matches) {
$tag = $matches[1]; // The tag
$val = $matches[3]; // Stuff between
$endTag = $matches[4]; // The