이 글은 스토리지 및 레퍼런스용 데이터베이스로 MySQL을, 그리고 PHP 백엔드와 XForms를 사용하여 애플리케이션을 만드는 데 관련된 실제적인 몇 가지 문제점에 초점을 둔다. 이 글을 이해하려면 XForms 및 PHP에 익숙해야 한다(익숙하지 않다면 시작하기 전에 참고자료를 통해 확인하기 바란다).
지금까지 다뤄온 많은 부분을 먼저 점검해 보자. 다른 실제 프로젝트와 마찬가지로 X-Trapolate 애플리케이션 역시 시작 단계에서 그렸던 것과는 약간 다른 결과가 나왔다. 다행히 처음 의도했던 것과 같은 애플리케이션을 가지게 되었지만 여전히 문제점은 남아있다. 그 문제점들을 점검해보자.
Part 1에서는 애플리케이션의 전반적인 계획과 각 부에서 XForms의 어느 부분을 다루는지 설명했다. 전체 연재의 소개 부분이니 연재를 계속 읽어오지 않았다면 참조 바란다.
Part 2는 사용자 인증 문제를 다룬다. 여기서 새 계정을 만들 때 필요한 기본 로그인 폼과 등록 폼을 만든다. 시작은 로그인 폼으로 충분했지만 새 기능이 등장하면서 필요 없어졌다. 하지만 새 기능은 사용자의 부서를 고려하지도, 등록 프로세스가 모든 사용자 데이터를 받아들이지도 않았다. 이는 자산 관리에 결정적인 부분이다. 가장 중요한 것은 튜토리얼이 이어지는 내용을 보호하지 못하는, 완전히 고립된 폼을 다룬다는 것이다. 본 Part 6에서는 인증 시스템 수정을 포함하여 이 모든 문제들을 해결하는 방법을 다룰 것이다. 이를 통해 성공적으로 로그인한 사용자만이 폼을 볼 수 있도록 할 것이다.
Part 3에서는 회사의 재정 부분을 다루기 시작한다. 청구 폼에서 시작하여 미 청구 계정을 보여주는 폼을 만든다. 이 폼으로 지불을 승인하고, 계산서를 보내고, 계정을 징수회사에 넘기는 업무 등을 할 수 있다. 또한 업무에 따라 맞춤화된 서신을 쉽게 프린트할 수 있다. 하지만 회사가 지불해야 할 송장(invoice)은 만들 수 없다. 이 문제는 다른 애플리케이션과의 통합 또는 이 기능을 현 애플리케이션에 추가함으로써 해결할 수 있다(송장을 점검하는 것은 Part 4에서 다룬다).
Part 3는 또한 예산 폼을 만드는 것을 상세히 설명한다. 예산 폼을 이용해 부서 사용자들은 자신의 부서와 하위 부서의 예상 이윤과 손실을 볼 수 있고 이것들을 실제 숫자와 쉽게 비교할 수 있다. 이 폼을 이용하면 또한 새 부서 구조를 애플리케이션에 추가할 수 있는데, 이는 XForms의 관점에서 보자면 굉장한 일이지만 결국 다른 계정 관리 분야에서 다뤄질 것이다.
Part 4에서는 이 애플리케이션의 가장 중요한 부분을 다룬다. 여기서는 기존 송장과 그 내용을 확인할 수 있는 폼을 설명하지만 최종적으로는 외부의 청구 애플리케이션과 통합해야 한다. 송장을 만들 수 없기 때문이다.
이 문제는 또한 사용자가 장비에 관련된 문제를 보고하는 방법과 조달 사용자들이 어떤 문제가 해결돼야 하는지 볼 수 있는 방법을 제공하는, Part 4에서 만든 자산 관리 폼에서도 골치거리가 된다. 자산 관리 폼은 새 자산을 만드는 기능을 제공하지 않는다.
Part 5에서는 폼 두 개를 만든다. 하나는 채무 또는 회사가 지불해야 하는 항목뿐 아니라 새 항목을 만들 수 있는 채무 폼이다. 이 폼은 XPath 조작을 사용하여 훌륭한 기능을 수행한다. Part 5는 또한 XForms 데이터를 사용하여 간단한 막대 그래프를 만드는 방법을 보여준다. 꼭 필요한 것은 아니지만 SVG(Scalable Vector Graphics) 같은 기술을 사용하여 이 폼을 확장한다면 좀 더 멋진 모습으로 만들 수 있다.
송장이나 자산처럼 새 데이터를 추가하는 기능은 본 연재에서 이미 다룬 기술로 쉽게 해결할 수 있고 등록 폼에서 생략된 부분을 고치면 곧 제대로 된 모습을 볼 수 있을 것이다. 이 기술은 사실 데이터베이스에 추가해야 할 때 어느 "것"에나 사용할 수 있다.
하지만 여전히 인증 문제가 남아있다. 이 글은 또한 로그인과 인증 시스템을 마무리 짓는 프로세스를 다룰 것이다.
등록 폼은 여러 상황에 적용되기 때문에 일단 이를 고치는 것으로 시작하자. 그렇게 되면 이 폼은 사용자의 성(姓)과 이름, 아이디, 비밀번호뿐 아니라 필요한 모든 데이터를 가진다.
애플리케이션의 완전한 코드는 소스를 다운로드할 수 있으므로 폼의 원래 버전에서 바뀐 것만 표시하겠다.
일단 빠진 데이터를 등록 폼 자체에 추가한다. Listing 1을 보자.
Listing 1. register.xhtml에 추가한 것
...
<xforms:model id="acctToolRegModel" >
<xforms:instance id="acctToolRegInst" >
<registerForm xmlns="">
<password/>
<username/>
<unameopt1/>
<unameopt2/>
<unameopt3/>
<prefix/>
<firstName/>
<lastName/>
<street />
<city />
<state />
<zip />
<phone />
<deptId />
<company />
<submitRegistration/>
</registerForm>
</xforms:instance>
...
<xforms:secret class="required" bind="passwordbind" >
<xforms:label>Password: </xforms:label>
</xforms:secret><br/>
<xforms:input ref="street" >
<xforms:label>Street: </xforms:label>
</xforms:input><br/>
<xforms:input ref="city" >
<xforms:label>City: </xforms:label>
</xforms:input><br/>
<xforms:input ref="state" >
<xforms:label>State: </xforms:label>
</xforms:input><br/>
<xforms:input ref="zip" >
<xforms:label>Zip: </xforms:label>
</xforms:input><br/>
<xforms:input ref="phone" >
<xforms:label>Phone: </xforms:label>
</xforms:input><br/>
<xforms:select1 ref="deptId">
<xforms:label>Department: </xforms:label>
<xforms:item>
<xforms:label>Procurement</xforms:label>
<xforms:value>123</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Human Resources</xforms:label>
<xforms:value>129</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Marketing</xforms:label>
<xforms:value>333</xforms:value>
</xforms:item>
</xforms:select1><br/>
<xforms:input ref="company" >
<xforms:label>Company: </xforms:label>
</xforms:input><br/>
<xforms:submit submission="submit_registration"
bind="submitRegistrationbind">
<xforms:label>Register</xforms:label>
</xforms:submit><br/>
<hr/>
</p>
</body>
</html>
|
Listing 1의 거의 대부분은 인스턴스와 폼에 간단히 값을 추가하는 것일 뿐이다. 부서 정보는 풀다운 메뉴(pulldown menu) 형식으로 추가되었기에 기존 부서만 추가될 수 있다는 것을 주목하자. 여기서는 정적인 엔트리로 나타나지만 실제로는 PHP 스크립트와 함께 인스턴스를 파퓰레이트함으로써 데이터베이스에서 이를 가져오고자 할 것이다(예는 적절한 메뉴 항목 제공하기 부분을 참조하자).
결과는 그림 1과 같다.
그림 1. 새 등록 폼
결과는 역시 추가 데이터가 더해져야 하는 PHP 스크립트로 작동한다(Listing 2).
Listing 2. register.php에 추가하는 것
<?php
if (!isset($HTTP_RAW_POST_DATA)) $HTTP_RAW_POST_DATA =
file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');
$doc->loadXML($xml);
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost because: '
. mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct1 database on MySQL server @ ' .
'localhost because: ' . mysql_error());
// first check to see if that username is already taken
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
$sqlQuery = sprintf('SELECT * FROM accounts WHERE username=\'%s\'',
mysql_real_escape_string($uname));
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not query the accounts table in the acct db because:'
. mysql_error());
if(mysql_num_rows($queryData) > 0)
{
mysql_close($sqldb);
$doc->getElementsByTagName("message")->item(0)->nodeValue =
'The username ' . $uname .
' is already taken. Choose another.';
echo $doc->saveXML();
exit;
}
else
{
$pass = mysql_real_escape_string(
$doc->getElementsByTagName("password")->item(0)->nodeValue);
$fname = mysql_real_escape_string(
$doc->getElementsByTagName("firstName")->item(0)->nodeValue);
$prefix = mysql_real_escape_string(
$doc->getElementsByTagName("prefix")->item(0)->nodeValue);
$lname = mysql_real_escape_string(
$doc->getElementsByTagName("lastName")->item(0)->nodeValue);
$street = mysql_real_escape_string(
$doc->getElementsByTagName("street")->item(0)->nodeValue);
$city = mysql_real_escape_string(
$doc->getElementsByTagName("city")->item(0)->nodeValue);
$state = mysql_real_escape_string(
$doc->getElementsByTagName("state")->item(0)->nodeValue);
$zip = mysql_real_escape_string(
$doc->getElementsByTagName("zip")->item(0)->nodeValue);
$phone = mysql_real_escape_string(
$doc->getElementsByTagName("phone")->item(0)->nodeValue);
$deptId = mysql_real_escape_string(
$doc->getElementsByTagName("deptId")->item(0)->nodeValue);
$company = mysql_real_escape_string(
$doc->getElementsByTagName("company")->item(0)->nodeValue);
$sqlQuery = "INSERT INTO accounts ( username, password, " .
"firstName, lastName, prefix, street, city, ".
"state, zip, phone, deptId, company) VALUES ( ".
sprintf("'%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ".
"'%s', '%s', '%s', '%s');",
$uname, $pass, $fname, $lname, $prefix, $street, $city,
$state, $zip, $phone, $deptId, $company);
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not insert token into accounts table on MySQL ".
"server because: ' . mysql_error());
mysql_close($sqldb);
$host= $_SERVER['HTTP_HOST'];
$reldir= rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
header("Location: http://$host$reldir/login.xhtml");
exit;
}
?>
|
변화된 부분이 두루 있으므로 여기에 전체 스크립트를 넣었다. 전체 스크립트는 다른 추가한 것의 예 역할을 하므로 애플리케이션에 이를 반영해야 한다. 이제 데이터베이스에 연결하여 제출된 인스턴스를 로드하자. 여기서부터 먼저 신청한 아이디가 이전에 채택됐는지 여부를 확인해야 한다. 단일 인용부호, 세미콜론 등이 빠져 SQL이 망가지는 것을 보호할 수 있도록 mysql_real_escape_string() 함수를 사용하자. 사용자가 제출한 데이터를 사용하여 SQL 구문을 만들기 전에 항상 데이터가 깨끗한지 확인해야 한다.
신청한 아이디가 이미 채택됐다면 XForms 폼에 메시지를 보내야 할 것이다. 이는 메시지를 XML 인스턴스에 추가함으로써 반환되도록 한다. 이 때 일반적인 텍스트 오류 메시지는 폼에 나타나지 않는다.
아이디가 사용 가능하다면 기존 데이터를 추출하여 깨끗이 해야 함을 기억하자. 그리고 나면 insert 구문에서 이를 사용하여 직접 데이터를 데이터베이스에 추가할 수 있다. 완성되면 데이터베이스를 닫고 HTTP 헤더를 사용하여 브라우저에 로그인 페이지를 띄운다.
이 기법은 새 자산, 송장 등을 추가하는 데 사용할 수 있다. 이제 로그인 프로세스 자체로 이동하자.
일반적인 애플리케이션에서는 사용자를 인증하는 페이지만 보이고 이후 인증 여부에 따라 추가 페이지들이 보인다. PHP로 만들어진 페이지의 경우 세션 변수나 다른 지속적 지표를 확인하면 되므로 이 프로세스는 쉽다. 하지만 XHTML과 같은 정적 폼인 경우 프로세스가 약간 더 복잡해진다.
이 때 초기 데이터 인스턴스를 동적으로 로드하고 그 인스턴스에 포함된 데이터 구조 상의 페이지 내용을 기반으로 하는 것이 하나의 옵션이 될 수 있다. 예를 들어, 인스턴스에 loggedIn 요소가 들어 있을 때 데이터는 모두 관련이 있어야 한다(이 예는 곧 설명하겠다). 하지만 이 옵션을 택한다면 여태까지 한 모든 작업을 완전히 바꿔야 한다.
두 번째 옵션은 PHP "래퍼"를 만드는 것으로, 이는 사용자가 로그인되었을 때만 정적 XHTML을 보여준다. 이 옵션의 장점은 기존 페이지에 약간의 수정을 가하거나 아예 수정을 하지 않아도 해결된다는 것이다. 그러므로 이를 택하겠다.
먼저, 이 프로세스를 쉽게 하기 위해서는 로그인 페이지 자체로 필요한 모든 것을 할 수 있는지를 확인해야 한다. 원래는 간단히 아이디와 비밀번호만 확인한 후 loginToken을 만들었다. 이제 다음으로 넘어가자.
다행히 필요한 모든 기능(사용자의 부서 ID를 이 세션에 추가하는 것 등)은 login_logout.php 스크립트에 이미 만들었다. 그러므로 첫 단계로 로그인 폼을 이 스크립트에 넣는다. Listing 3을 보자.
Listing 3. login.xhtml 고치기
...
<xforms:submission id="submit_login"
action="login_logout.php" method="post"
instance="acctToolLoginInst"
replace="instance"
ref="instance('acctToolLoginInst')" >
</xforms:submission>
</xforms:model>
</head>
<body>
<p>
...
<xforms:output class="errormessage"
ref="instance('acctToolLoginInst')/message" />
<xforms:trigger
model="acctToolLoginModel"
ref="instance('acctToolLoginInst')/loggedIn">
<xforms:label>Menu</xforms:label>
<xforms:load ev:event="DOMActivate"
resource="appManager.php?module=appMenu"/>
</xforms:trigger><br/>
</p>
</body>
</html>
|
첫 변경 작업은 매우 명확하다. 이 폼을 강화된 PHP 로그인 스크립트에 넣는 것이다. 하지만 사용자가 성공적으로 로그인을 했다면 어떻게 되는가? 이 기능은 Part 2에서 남겨두었던 문제다. 이제 해결해보자.
사용자가 성공적으로 로그인했다면 응답은 새 loggedIn 요소를 포함할 것이고 -- 곧 다루겠다 -- 그렇게 되면 Menu 트리거가 나타난다. 이 트리거는 사용자에게 적합한 선택만을 포함하는 새 페이지를 로드한다. 이 기능은 페이지 보호하기와 적절한 메뉴 제공하기에서 만들겠지만, 그 전에 login_logout.php 스크립트에 몇 가지 변경작업을 했다.
login_logout.php 스크립트는 "범용" 스크립트로 만들어졌고 사용자가 각 페이지 별로 로그인과 로그아웃을 할 수 있게끔 설계됐다. 이제 사용자가 한 곳에서 로그인하는 데 이 스크립트가 쓰이므로 단순화할 수 있다. Listing 4을 보자.
Listing 4. login_logout.php
<?php
session_start();
unset($_SESSION['loginToken']);
unset($_SESSION['username']);
unset($_SESSION['at_deptId']);
unset($_SESSION['acctId']);
if (!isset($HTTP_RAW_POST_DATA))
$HTTP_RAW_POST_DATA = file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');
if ($xml != "") {
$doc->loadXML($xml);
} else {
header("Location: login.xhtml");
}
if ($doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue
== 'Logout'){
$doc->GetElementsByTagName('deptId')->item(0)->nodeValue = "";
$doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue =
"Login";
$doc->GetElementsByTagName("username")->item(0)->nodeValue = "";
$doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = "";
$doc->GetElementsByTagName('login')->item(0)->nodeValue=1;
echo $doc->saveXML();
exit;
}
$pass = $doc->getElementsByTagName("password")->item(0)->nodeValue;
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
if ($uname == '' && $pass == ''){
echo $doc->saveXML();
exit;
}
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost because: ' .
mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct database on MySQL server @ ".
"localhost because: ' . mysql_error());
$sqlQuery = sprintf(
"SELECT * FROM accounts WHERE username='%s' and password='%s'",
mysql_real_escape_string($uname), mysql_real_escape_string($pass));
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData) die ('Could not insert token into tokentable on ".
"MySQL server because: ' . mysql_error());
$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$numRows = mysql_num_rows($queryData);
if($numRows <= 0)
{
mysql_close($sqldb);
$doc->getElementsByTagName("message")->item(0)->nodeValue =
'Invalid login credentials!';
echo $doc->saveXML();
exit;
}
else
{
$row = mysql_fetch_assoc($queryData);
$_SESSION['at_deptId'] = $row['deptId'];
$_SESSION['acctId'] = $row['acctId'];
$_SESSION['username'] = $uname;
$loginToken = rand();
$_SESSION['loginToken'] = $loginToken;
$now = localtime();
$datetime = sprintf("%04d-%02d-%02d",$now[5]+1900,$now[4],$now[3]);
$sqlQuery = sprintf(
"INSERT INTO logintokens (logintoken, username, creation)".
"VALUES ( '%d', '%s', '%s');",
$_SESSION['loginToken'],
mysql_real_escape_string($username),
$datetime);
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not insert token into tokentable on MySQL server".
" because: ' . mysql_error());
mysql_close($sqldb);
$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$doc->GetElementsByTagName("errorMessage")->item(0)->nodeValue =
sprintf('%s Logged In',$_SESSION['username']);
$doc->getElementsByTagName('loginLogout')->item(0)->nodeValue =
"Logout";
$doc->GetElementsByTagName("loginToken")->item(0)->nodeValue =
$_SESSION['loginToken'];
$doc->getElementsByTagName('deptId')->item(0)->nodeValue =
$_SESSION['at_deptId'];
$doc->GetElementsByTagName('login')->item(0)->nodeValue=0;
$notLoggedElement =
$doc->getElementsByTagname('notLoggedIn')->item(0);
$notLoggedElement->parentNode
->appendChild($doc->createElement('loggedIn'));
$notLoggedElement->parentNode->removeChild($notLoggedElement);
echo $doc->saveXML();
exit;
}
?>
|
관련된 세션 변수를 설정하지 않음으로써 사용자가 로그아웃되었는지 먼저 확인하자. 사용자가 성공적으로 로그인한다면 또 다른 loginToken을 갖게 될 것이고, 그렇지 않다면 로그아웃 상태가 될 것이다.
다음으로 로그인 정보가 실제 제출되었는지 확인한다. 제출되지 않았다면 간단히 로그인 페이지로 되돌아가면 된다.
스크립트는 여전히 명시적인 로그아웃 요청을 다룰 수 있고 적절한 인스턴스 정보를 만들고 반환할 수 있다. 그러므로 각각의 페이지에 있는 링크는 여전히 작동한다.
사용자가 아이디와 비밀번호를 제출하지 않았다면 스크립트는 원래 인스턴스를 로그인 페이지로 반환한다. 그러나 정보가 그대로 있다면 데이터베이스와 비교해 확인한다. 로그인 정보 승인에 실패하면 스크립트는 그림 2 처럼 message 요소(errormessage 요소의 반대인)에 영향을 주는 메시지를 삽입한다.
그림 2. 로그인 실패
이와 반대로 로그인 정보가 승인되면 logintokens 테이블인 세션을 파퓰레이트하고 기존처럼 반환한다. 하지만 여기서 변경해야 할 사항은 loggedIn 요소를 또한 반환된 인스턴스에 추가하고 notLoggedIn 요소를 제거하는 것이다.
인스턴스에 이런 변화를 주는 것은 로그인 폼보다는 Menu 버튼에 적절하다. 그림 3과 같다.
그림 3. 성공적인 로그인
Menu 버튼을 클릭하면 사용자는 페이지 목록을 볼 수 있지만 성공적으로 로그인하지 않았다면 직접 방문은 할 수 없게 된다.
Menu 버튼을 클릭하면 사용자는 http://<myhost>/appManager.php?module=appMenu로 이동한다.
이 페이지는 민감한 정보를 제공하기 전에 사용자가 로그인되었는지 확인하는 PHP 스크립트다. Listing 5를 보자.
Listing 5. appManager.php
<?php
session_start();
$loggedIn = false;
if (isset($_SESSION['loginToken'])){
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost: ' .
mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct database on MySQL server '.
"@ localhost because: ' . mysql_error());
$sqlQuery =
sprintf("SELECT * FROM logintokens WHERE logintoken='%s'",
mysql_real_escape_string($_SESSION['loginToken']));
$queryData = mysql_query($sqlQuery, $sqldb);
if ( mysql_fetch_assoc($queryData)){
$loggedIn = true;
}
}
$module = $_GET['module'];
if ($loggedIn){
$filename = $module.".xhtml";
header("Content-type: text/xml");
echo file_get_contents($filename);
} else {
header("Location: login.xhtml");
}
?>
|
먼저 세션을 초기화하고 loggedIn 변수를 false로 설정한다. 데이터베이스에서 검증한 것처럼 세션에서 적절한 loginToken을 찾지 않는 한 그 상태로 남아 있을 것이다(이는 또한 데이터베이스에서 토큰을 만료시키는 등의 작업을 할 수 있도록 한다).
그리고 나서 querystring에서 페이지 또는 "모듈"을 찾아온다. 사용자가 로그인 상태라면 파일이 XML 형태인지 먼저 확인한 후 브라우저에 적절한 파일의 내용을 보낼 것이다(브라우저는 이를 PHP 스크립트라고 생각하기 때문에 자동으로 작동하진 않을 것이다). 사용자가 로그인 상태가 아니라면 그냥 로그인 페이지로 돌려보낸다.
이제 이 예에서 모든 페이지는 같은 디렉터리에 있게 되는데, 이 디렉터리는 안전하게 보인다고도, 안전하지 않다고도 할 수 없다. 그러나 웹 서버를 사용하여 접근할 수 없는 디렉터리에 페이지를 보낼 수밖에 없다. 페이지를 보내면 appManager.php를 통해서만 페이지에 접근할 수 있고, 사용자가 로그인 상태가 아니라면 접근 자체가 불가능하다.
또한 사용자가 접근할 수 있는 파일의 권한을 확인하기 위해 이 파일을 확장할 수 있다. 예를 들어 관리를 위해서만 예산 페이지를 한정할 수 있는 식으로 말이다.
이제 애플리케이션은 메뉴를 제외하고는 거의 완벽해졌다.
이제 메뉴는 로그인한 사용자만 접근할 수 있게 됐다. 하지만 사용자가 권한이 있는 옵션만 가질 수 있는지 여부를 어떻게 확인할 수 있을까? 어쨌든 메뉴 페이지 자체는 정적인 XHTML 페이지다. Listing 6을 보자.
Listing 6. 메뉴 페이지
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/D/tdxhtml1-strict.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
>
<head>
<title>XForms Accounting Menu Page</title>
<link rel="stylesheet" href="style.css" type="text/css"/>
<xforms:model id="menuPage" >
<xforms:instance xmlns="" src="appMenu.php" />
</xforms:model>
</head>
<body>
<p>
<xforms:repeat nodeset="//link">
<xforms:trigger>
<xforms:label ref="." />
<xforms:action ev:event="DOMActivate">
<xforms:load ref="./@href" show="replace" />
</xforms:action>
</xforms:trigger>
</xforms:repeat>
</p>
</body>
</html>
|
중요한 것은 인스턴스에 있다. 옵션은 정적 목록을 가지는 대신에 PHP 스크립트인 appMenu.php로부터 데이터를 받는다. 그리고 나면 이들 각 요소는 트리거를 만들고 요소를 로드하는 데 쓰일 수 있다. 이 요소는 브라우저를 어디로 보낼지 결정하는 각 링크 요소의 href 속성에 있는 데이터를 사용한다.
appMenu.php 스크립트는 간단하다. Listing 7을 보자.
Listing 7. appMenu.php
<?php
header("Content-type: text/xml");
$deptId = $_SESSION['deptId'];
if ($deptId = 123){
?>
<links>
<link href="appManager.php?module=billing">Billing</link>
<link href="appManager.php?module=budget">Budget</link>
<link href="appManager.php?module=invoices">Invoices</link>
<link href=
"appManager.php?module=assetManagement">Asset Management</link>
<link href="appManager.php?module=payables">Payables</link>
<link href="appManager.php?module=analyze">Analyze</link>
</links>
<?php
} else {
?>
<links>
<link href="appManager.php?module=billing">Billing</link>
<link href="appManager.php?module=budget">Budget</link>
<link href="appManager.php?module=invoices">Invoices</link>
<link href="appManager.php?module=payables">Payables</link>
<link href="appManager.php?module=analyze">Analyze</link>
</links>
<?php
}
?>
|
appManager.php를 통해서만 XForms 폼에 접속할 수 있으므로 여기서 사용자는 이미 로그인 상태여야 한다. 즉, 부서 ID를 사용하여 어떤 페이지를 보여줄지 동적으로 결정할 수 있다. 이들 각 링크는 appManager.php 스크립트를 통해 페이지를 보여준다는 것에 주목하자. 결과는 그림 4와 같다.
그림 4. 메뉴 페이지
원하는 대로 페이지를 꾸밀 수 있음은 물론이다.
지금까지 6회 연재를 통해 XForms, PHP, MySQL을 사용하여 회계 도구를 만드는 방법을 살펴봤다. 이 도구는 사용자가 PHP에 로그인했는지 여부를 판단하는 동작 없이 송장을 확인하고 XForms의 강점을 이용한 예산 계획 같은 동작을 수행한다. 이 방법으로 애플리케이션을 만들면 약점을 최소화하며 강점을 최대한 활용할 수 있다.
그리고 이것이 실제 세계에서의 개발의 모든 것이다.
| 설명 | 이름 | 크기 | 다운로드 방식 |
|---|---|---|---|
| Part 6 샘플 코드 | xtrapolate.zip | 42KB | HTTP |
교육
-
XForms 소개, Part 1: 폼을 위한 새로운 웹 표준(Chris Herborth, developerWorks, 2006년 9월), Introduction to XForms, Part 2: Forms, models, controls, and submission actions(Chris Herborth, developerWorks, 2006년 9월), Introduction to XForms, Part 3: Using actions and events(Chris Herborth, developerWorks, 2005년 9월)에서 XForms에 대한 기본 소개를 읽자.
-
Learning PHP, Part 1: Register for an account, upload files for approval, and view and download approved files(Nicholas Chase와 Tyler Anderson, developerWorks, 2005년 6월), Learning PHP, Part 2: Upload files and use XML to store and display file information(Nicholas Chase와 Tyler Anderson, developerWorks, 2005년 6월), Learning PHP, Part 3: Authentication, objects, exceptions, and streaming(Nicholas Chase와 Tyler Anderson, developerWorks, 2005년 7월)에서 PHP의 기본을 배우자.
-
PHP Manual.
-
Creating dynamic Web sites with PHP and MySQL(Md. Ashraful Anam, developerWorks, 2001년 5월)에서 MySQL의 기초를 배우자.
-
XForms tip: Using form submission events(Nicholas Chase, developerWorks, 2006년 11월)에서 XForms 제출 이벤트에 대해 더 많은 내용을 배우자.
-
자바(Nicholas Chase, developerWorks, 2006년 10월), 펄(Tyler Anderson, developerWorks, 2006년 10월), PHP(Nicholas Chase, developerWorks, 2006년 9월)에서 XForms 데이터를 수용하는 방법을 참조하라.
-
PHP and SQL Injection과 SQL Injection: Modes of Attack, Defence, and Why It Matters를 통해 SQL 주입(injection) 공격에 대해 더 배우자.
-
IBM XML certification: XML 및 관련 기술 분야에서 IBM 공인 개발자가 될 수 있는 방법을 소개한다.
-
XML 기술 자료: 한국 developerWorks XML 존에서 다양한 기술문서, 팁, 튜토리얼, 표준 및 IBM Redbook을 볼 수 있다.
-
developerWorks 기술 행사와 웹 캐스트: 최신 기술을 습득하자.
-
한국 developerWorks XML 존에서 XML과 XForms에 대해 배워보자.
-
XForms에 대해 알고자 하는 모든 것에 세세하게 초점을 맞춘 커뮤니티인 새로운 XForms community topic을 꼭 방문해보자.
제품 및 기술 얻기
-
XForms 권고안: W3C에서 유지 관리한다.
-
미리 설정되어 바로 사용할 수 있는 PHP와 MySQL을 포함하는 WAMP 다운로드
-
인터넷 익스플로러에서 XForms를 렌더링하기 위해 사용할 수 있는 오픈 소스 컨트롤인 MozzIE를 사용해 보라.
토론