diff --git a/data/begin.html b/data/begin.html new file mode 100644 index 0000000000000000000000000000000000000000..4d9db486140ac42178d330e1d3e24fe0a093b6cb --- /dev/null +++ b/data/begin.html @@ -0,0 +1,36 @@ +<html> +<head> + <title>Begin Your UCARE Application</title> +<link rel="stylesheet" href="http://www.unl.edu/unlpub/styles/sitestyl.css"> +<style type="text/css"> +<!-- +--> +</style> + +</head> + +<body bgcolor="#FFFFFF"> + +<table width="600" border="0" align="center" cellpadding="0" cellspacing="0"> + <tr> + <td align="center"><br /><br /> + + <span class="sserifstoryhead">Begin Your UCARE Application</span><br /><br /> + <hr align="center" width="50%" size="1" noshade><br /> + <span class="sseriftext"> + You may enter your UCARE Application online from any computer connected to the UNL campus network.<br /><br /> + You must complete the form in its entirety in a single session. You will need the email address and phone number of your sponsor, your current grade point average, and specific information about your project, including its title, duration (start and end dates) and a funding amount requested.<br /><br /> + You may submit your Year One UCARE application online: <a href="#" onClick="window.open('FMPro?-db=ucareapp&-lay=web&-format=newyr1.shtml&-view','studentappwindow','height=700,width=762,innerheight=700,innerwidth=762,scrollbars=1,resizable=1,status=0,toolbars=0')" + title="Click to Begin Your UCARE Year 1 Application"><br /><br /><b>Year 1 Online Application</b></a><br /><br /> + + If you prefer, a <a href="http://www.unl.edu/ucare/forms/ucareappYear1_0809.pdf">fillable PDF form</a> is available.<br /><br /><br /> + You may submit your Year Two UCARE application online: <a href="#" onClick="window.open('FMPro?-db=ucareapp&-lay=web&-format=newyr2.shtml&-view','studentappwindow','height=700,width=762,innerheight=700,innerwidth=762,scrollbars=1,resizable=1,status=0,toolbars=0')" + title="Click to Begin Your UCARE Year 2 Application"><br /><br /><b>Year 2 Online Application</b></a><br /><br /> + If you prefer, a <a href="http://www.unl.edu/ucare/forms/ucareappYear2_0809.pdf">fillable PDF form</a> is available.<br /><br /> + </span> + + </td> + </tr> +</table> +</body> +</html> \ No newline at end of file diff --git a/data/year1_app.html b/data/year1_app.html new file mode 100644 index 0000000000000000000000000000000000000000..8b7d6a6f6c78cae8ec834a4a8eb43e485fa6bffa --- /dev/null +++ b/data/year1_app.html @@ -0,0 +1,426 @@ +<HTML> +<HEAD> + <TITLE>UCARE Application - Year 1 Proposal</TITLE> +<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> +<meta http-equiv="Pragma" content="no-cache"> +<link rel="stylesheet" href="http://www.unl.edu/unlpub/styles/sitestyl.css"> +<style type="text/css"> +</style> + +<SCRIPT LANGUAGE="JavaScript"> +// Preload images +var empty = new Image(); empty.src = "redx.gif"; + +var haveerrors = 0; + +function showImage(imagename, imageurl, errors) { +document[imagename].src = imageurl; +if (!haveerrors && errors) haveerrors = errors; +} + +function val(studentsubmitform) { + +rExpEmail = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/ + +rExpZip = /^\d{5}$|^\d{5}[\-\s]?\d{4}$/ + +rExpPhone = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$/ + +rExpPhoneACOptional = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$|^\D?(\d{3})\D?(\d{4})$/ + +rExpPhoneACOptionalUNL = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$|^\D?(\d{3})\D?(\d{4})$|^2\D?(\d{4})$/ + +rExpNUID = /^\d{8}$/ + +rExpURL = /((mailto\:|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/ + +rExpIPAddr = /\b((25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)\.){3}(25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)\b/ + +rExpName = /^([a-zA-Z]+\s?)+$/ + +rExpTime = /^([1-9]|1[0-2]):[0-5]\d$/ + +rExpDate = /^\d{1,2}(\-|\/|\.)\d{1,2}\1\d{4}$/ + +rExpDateBTW0619 = /^([1-9]|0[1-9]|1[0-2])[\-|\/|\.]([1-9]|0[1-9]|[12][0-9]|3[01])[\-|\/|\.](200[6-9]|201[0-9])$/ + +rExpDollarAmount = /\$\d{1,3}(,\d{3})*\.\d{2}/ + +rExpBobsAllDigits = /^\d+$/ + +rExpBobs3or4Digits = /^\d{3,4}$/ + +rExpBobsGradDate = /^\D+\x20\d{4}$|^\d{1,2}(\-|\/|\.)\d{4}$/ + +rExpBobsGPA = /^\d.\d{1,2}$/ + +rExpTitle = /^(w+\s?)+$/ + +haveerrors = 0; + +// validate student last name +(!rExpName.test(studentsubmitform.studentnamelast.value)) +? showImage("studentnamelasterror", "images/redx.gif", true) +: showImage("studentnamelasterror", "images/greencheck.gif", false); + +// validate student first name +(!rExpName.test(studentsubmitform.studentnamefirst.value)) +? showImage("studentnamefirsterror", "images/redx.gif", true) +: showImage("studentnamefirsterror", "images/greencheck.gif", false); + +// validate student middle name if data present +(!rExpName.test(studentsubmitform.studentnamemiddle.value)) && (studentsubmitform.studentnamemiddle.value.length > 0) +? showImage("studentnamemiddleerror", "images/redx.gif", true) +: showImage("studentnamemiddleerror", "images/greencheck.gif", false); + +// validate student id length and format; 9 digits or 3-2-4 +(!rExpNUID.test(studentsubmitform.studentid.value)) +? showImage("studentiderror", "images/redx.gif", true) +: showImage("studentiderror", "images/greencheck.gif", false); + +// validate student phone length and format +(!rExpPhoneACOptionalUNL.test(studentsubmitform.studentphone.value)) +? showImage("studentphoneerror", "images/redx.gif", true) +: showImage("studentphoneerror", "images/greencheck.gif", false); + +// validate student gpa length, format and value +(!rExpBobsGPA.test(studentsubmitform.studentgpa.value)) || (studentsubmitform.studentgpa.value > 4) +? showImage("studentgpaerror", "images/redx.gif", true) +: showImage("studentgpaerror", "images/greencheck.gif", false); + +// validate graduation date +(!rExpBobsGradDate.test(studentsubmitform.studentgraddate.value)) +? showImage("studentgraddateerror", "images/redx.gif", true) +: showImage("studentgraddateerror", "images/greencheck.gif", false); + +// validate major +(studentsubmitform.studentmajor.value.length < 1) +? showImage("studentmajorerror", "images/redx.gif", true) +: showImage("studentmajorerror", "images/greencheck.gif", false); + +// validate street address length +(studentsubmitform.studentlocaddr.value.length < 1) +? showImage("studentlocaddrerror", "images/redx.gif", true) +: showImage("studentlocaddrerror", "images/greencheck.gif", false); + +// validate projecttitle +(studentsubmitform.projecttitle.value.length < 1) +? showImage("projecttitleerror", "images/redx.gif", true) +: showImage("projecttitleerror", "images/greencheck.gif", false); + +// validate projectsummary +(studentsubmitform.projectsummary.value.length < 1) +? showImage("projectsummaryerror", "images/redx.gif", true) +: showImage("projectsummaryerror", "images/greencheck.gif", false); + +// validate class standing +(!studentsubmitform.studentclassstanding[0].checked && +!studentsubmitform.studentclassstanding[1].checked && +!studentsubmitform.studentclassstanding[2].checked && +!studentsubmitform.studentclassstanding[3].checked) +? showImage("studentclassstandingerror", "images/redx.gif", true) +: showImage("studentclassstandingerror", "images/greencheck.gif", false); + +// validate student email +(!rExpEmail.test(studentsubmitform.studentemail.value)) +? showImage("studentemailerror", "images/redx.gif", true) +: showImage("studentemailerror", "images/greencheck.gif", false); + +// validate project dates +(!rExpDateBTW0619.test(studentsubmitform.projectdurationfrom.value) || !rExpDateBTW0619.test(studentsubmitform.projectdurationto.value)) +|| +(Date.parse(studentsubmitform.projectdurationfrom.value) > Date.parse(studentsubmitform.projectdurationto.value)) +? showImage("projectdateserror", "images/redx.gif", true) +: showImage("projectdateserror", "images/greencheck.gif", false); + +// validate fund amount +(!studentsubmitform.projectfundamount[0].checked && !studentsubmitform.projectfundamount[1].checked && !studentsubmitform.projectfundamount[2].checked && !studentsubmitform.projectfundamount[3].checked && !studentsubmitform.projectfundamount[4].checked) // if nothing's checked +|| +(!studentsubmitform.projectfundamount[0].checked && !studentsubmitform.projectfundamount[1].checked && !studentsubmitform.projectfundamount[2].checked && !studentsubmitform.projectfundamount[3].checked && !studentsubmitform.projectfundamount[4].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if nothing's checked and there's something in the other amount field +|| +(studentsubmitform.projectfundamount[0].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the first project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[1].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the second project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[2].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the third project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[3].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the fourth project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[4].checked && !rExpBobs3or4Digits.test(studentsubmitform.projectfundotheramount.value))// if the fifth project fund radio button is checked and there's anything other than 3 or 4 digits in the other amount field +|| +(studentsubmitform.projectfundamount[4].checked && studentsubmitform.projectfundotheramount.value > 5000)// if the fifth project fund radio button is checked and there's an amount over 5000 in the other amount field +? showImage("projectfundamounterror", "images/redx.gif", true) +: showImage("projectfundamounterror", "images/greencheck.gif", false); + +// validate sponsor last name +(!rExpName.test(studentsubmitform.sponsornamelast.value)) +? showImage("sponsornamelasterror", "images/redx.gif", true) +: showImage("sponsornamelasterror", "images/greencheck.gif", false); + +// validate sponsor first name +(!rExpName.test(studentsubmitform.sponsornamefirst.value)) +? showImage("sponsornamefirsterror", "images/redx.gif", true) +: showImage("sponsornamefirsterror", "images/greencheck.gif", false); + +// validate sponsor middle name if data present +(!rExpName.test(studentsubmitform.sponsornamemiddle.value)) && (studentsubmitform.sponsornamemiddle.value.length > 0) +? showImage("sponsornamemiddleerror", "images/redx.gif", true) +: showImage("sponsornamemiddleerror", "images/greencheck.gif", false); + +// validate sponsor email +(!rExpEmail.test(studentsubmitform.sponsoremail.value)) +? showImage("sponsoremailerror", "images/redx.gif", true) +: showImage("sponsoremailerror", "images/greencheck.gif", false); + +// validate sponsor phone length and format +(!rExpPhoneACOptionalUNL.test(studentsubmitform.sponsorphone.value)) +? showImage("sponsorphoneerror", "images/redx.gif", true) +: showImage("sponsorphoneerror", "images/greencheck.gif", false); + +// validate fund amount +(!studentsubmitform.irbcomplystdt.checked) // if nothing's checked +? showImage("irbcomplystdt", "images/redx.gif", true) +: showImage("irbcomplystdt", "images/greencheck.gif", false); + + + return (!haveerrors); +} + + +// End --> +</script> + + +<script language="javascript" type="text/javascript"> + +var submitcount=0; + +function checksubmits(studentsubmitform) { + d1=new Date() + studentsubmitform.studentsubmitdate.value=(d1.getMonth()+1)+"/"+(d1.getDate())+"/"+(d1.getFullYear()) +if (submitcount == 0) + { + submitcount++; + return true; + } + else + { + alert("This form has already been submitted. Thanks!"); + return false; + } + } + +</script> + +</HEAD> + + +<BODY BGCOLOR="#f2f2f2" onLoad="self.focus()"> + <FORM name="studentsubmitform" onSubmit="return (val(this)&&checksubmits(this))" ACTION="FMPro" METHOD="post"> + <INPUT TYPE="hidden" NAME="-DB" VALUE="ucareapp"> + <INPUT TYPE="hidden" NAME="-Lay" VALUE="web"> + <INPUT TYPE="hidden" NAME="-format" VALUE="studentsubmitint1.htm"> + <INPUT TYPE="hidden" NAME="-error" VALUE="new_error.htm"> + <INPUT TYPE="hidden" NAME="studentnetaddress" VALUE="129.93.245.115"> + <INPUT TYPE="hidden" NAME="studentsubmitdate" VALUE=""> + + <INPUT TYPE="hidden" NAME="hproposalyear" VALUE="YEAR 1"> + + <div align="center"> + <table width="720" border="0" align="center" cellpadding="6" cellspacing="0" bgcolor="#FFFFFF" class="sseriftext"> + <tr> + <td colspan="3" align="center"> + <span class="sserifstoryhead"> + <br> + UCARE APPLICATION<br> + + YEAR 1 Proposal</span> </td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"><span class="sseriftext"><i><br> + <strong>Hover over symbols</strong> ('?', 'X') <strong>for field requirements</strong>. Your internet address is 129.93.245.115.<br> + + </i></span></td> + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>STUDENT INFORMATION</strong></span></td> + </tr> + <tr valign="bottom"> + <td colspan="2"><table width="100%" border="0" cellspacing="0" cellpadding="0" class="sseriftext"> + <tr valign="bottom"> + + <td>last name <img name=studentnamelasterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='studentnamelast' TYPE=text id="studentnamelast" SIZE=16></td> + <td>first name <img name=studentnamefirsterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='studentnamefirst' TYPE=text id="studentnamefirst" SIZE=16></td> + <td>middle <img name=studentnamemiddleerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='studentnamemiddle' TYPE=text id="studentnamemiddle" SIZE=8></td> + </tr> + + </table></td> + <td>student ID <img name=studentiderror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be 8 digits in standard NUID form (NNNNNNNN)"><br> + <INPUT NAME='studentid' TYPE=text id="studentid" SIZE=16></td> + </tr> + <tr valign="bottom"> + <td colspan="2">local address <img name=studentlocaddrerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter your street address, with city, state and zip if outside Lincoln"><br> + <INPUT NAME='studentlocaddr' TYPE=text id="studentlocaddr" SIZE=48></td> + <td>current class <img name=studentclassstandingerror src="images/yellowqmark24x.gif" width="24" height="24" title="Select one"><br> + + <br> + + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='freshman' >freshman + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='sophomore' >sophomore + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='junior' >junior + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='senior' >senior </td> + </tr> + <tr valign="bottom"> + <td>email <img name=studentemailerror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be a properly-formatted email address"><br> + <INPUT NAME='studentemail' TYPE=text id="studentemail" SIZE=24> + + </td> + <td>phone <img name=studentphoneerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter phone number with or without area code; '2-' exchange acceptable for on-campus numbers"><br> + <INPUT NAME='studentphone' TYPE=text id="studentphone" SIZE=24></td> + <td> </td> + </tr> + <tr valign="bottom"> + <td>major <img name=studentmajorerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter your major"><br> + <INPUT NAME='studentmajor' TYPE=text id="studentmajor" SIZE=24></td> + + <td>graduation date (est.) <img name=studentgraddateerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter as Month YYYY or MM/YYYY"><br> + <INPUT NAME='studentgraddate' TYPE=text id="studentgraddate" SIZE=24></td> + <td>gpa <img name=studentgpaerror src="images/yellowqmark24x.gif" width="24" height="24" title="One digit, followed by a decimal, followed by one or two digits; not more than 4.00"><br> + <INPUT NAME='studentgpa' TYPE=text id="studentgpa" SIZE=24></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"> </td> + </tr> + + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>PROJECT INFORMATION</strong></span></td> + </tr> + <tr valign="bottom"> + <td colspan="3">project title <img name=projecttitleerror src="images/yellowqmark24x.gif" width="24" height="24" title="May contain any characters"><br> + <INPUT NAME='projecttitle' TYPE=text id="projecttitle" SIZE=90></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + + <td colspan="3">SUMMARY STATEMENT <img name=projectsummaryerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text"><br>Provide a 100 to 200-word summary of the project on which you will be working, including your proposed role in this project. Please include the scientific or creative methods to be employed and the significance of the work to the field of knowledge in general.<br> + <textarea name='projectsummary' rows=6 cols=90 wrap=virtual class="sseriftext"></textarea> + </td> + </tr> + <tr valign="bottom"> + <td>duration <img name=projectdateserror src="images/yellowqmark24x.gif" width="24" height="24" title="MM/DD/YYYY format"><br> + <table width="100%" border="0" cellspacing="0" cellpadding="0" class="sseriftext"> + + <tr> + <td>from<br> + <INPUT NAME='projectdurationfrom' TYPE=text id="projectdurationfrom" SIZE=12></td> + <td>to<br> + <INPUT NAME='projectdurationto' TYPE=text id="projectdurationto" SIZE=12></td> + </tr> + </table></td> + <td colspan="2">amount requested <img name=projectfundamounterror src="images/yellowqmark24x.gif" width="24" height="24" title="Check an amount. If 'other amount,' must be from 100 to 5000, no punctuation"><br> + + + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$500' >$500 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$1000' >$1000 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$1500' >$1500 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$2000' >$2000 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='other amount' >other amount + <br> + other amount: + <INPUT NAME='projectfundotheramount' TYPE=text id="projectfundotheramount" SIZE=12></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"> </td> + </tr> + + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>SPONSOR INFORMATION</strong> (PROFESSOR FOR WHOM YOU WILL BE A RESEARCH ASSISTANT)</span></td> + </tr> + <tr valign="bottom"> + <td>sponsor's last name <img name=sponsornamelasterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='sponsornamelast' TYPE=text id="sponsornamelast" SIZE=16></td> + <td><p>sponsor's first name <img name=sponsornamefirsterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + + <INPUT NAME='sponsornamefirst' TYPE=text id="sponsornamefirst" SIZE=16> + <br> + </p> + </td> + <td>sponsor's middle <img name=sponsornamemiddleerror src="images/yellowqmark24x.gif" width="24" height="24" title="Optional; may include any text characters"><br> + <INPUT NAME='sponsornamemiddle' TYPE=text id="sponsornamemiddle" SIZE=16></td> + </tr> + <tr valign="bottom"> + + <td colspan="2">sponsor's email address <img name=sponsoremailerror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be a properly-formatted email address"><br> + <INPUT NAME='sponsoremail' TYPE=text id="sponsoremail" SIZE=24></td> + <td>sponsor's phone <img name=sponsorphoneerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter phone number with or without area code; '2-' exchange acceptable for on-campus numbers"><br> + <INPUT NAME='sponsorphone' TYPE=text id="sponsorphone" SIZE=24></td> + </tr> + <tr> + <td> </td> + <td> </td> + + <td> </td> + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>RESEARCH COMPLIANCE</strong></span></td> + </tr> + <tr valign="bottom"> + <td colspan="3" valign="top">All research involving human subjects + at or associated with the University of Nebraska–Lincoln must + be reviewed and approved by the Institutional Review Board (IRB) + before research can proceed. Human research includes everything + from surveys to physical testing. Any human research that is conducted + without IRB approval can place the University, its faculty, staff, + students and human research participants in jeopardy. Further information + can be accessed from the following website: <a href="http://www.unl.edu/research/ReComp1/IRBguide2.pdf">http://www.unl.edu/research/ReComp1/IRBguide2.pdf</a>. + Any questions or concerns can be directed to Dr. Dan Vasgird, Research + Compliance Services, 472-1837 or email <a href="mailto:dvasgird2@unl.edu">dvasgird2@unl.edu</a>.<br> + + <table border="0" cellpadding="0" width="100%" class=sseriftext> + <tr> + <td width="24"> + <input type="checkbox" name="irbcomplystdt" value="irbcomplychecked"> + </td> + <td> + I have read this statement and will comply with IRB guidelines. + </td> + <td width="24"><img src="images/yellowqmark24x.gif" alt="helpanderror" name="irbcomplystdt" width="24" height="24" align="middle" title="You must acknowledge that you have read and understand the Research Compliance statement by checking the box provided."> + + </td> + </tr> + </table> + </td> + </tr> + + <tr> + <td> </td> + <td> </td> + <td> </td> + + </tr> + <tr> + <td colspan="3"><div align="center"> + </div> + <CENTER> + Please do not click the 'Submit My Application' button without first + carefully reviewing your entries.<br> + An application <i>cannot be modified</i> after + it is submitted.<br> + + <br> + <IMG SRC="images/transpixel.gif"><br> + <BR> + <INPUT TYPE="submit" NAME="-New" VALUE="Submit My Application"> + + <br> + </CENTER> + </td> + </tr> + </table> + +</div> +</form> +</BODY> +</HTML> diff --git a/data/year2_app.html b/data/year2_app.html new file mode 100644 index 0000000000000000000000000000000000000000..08985f06b4d14bb60cf83383edca80a19191f185 --- /dev/null +++ b/data/year2_app.html @@ -0,0 +1,433 @@ + +<HTML> +<HEAD> + <TITLE>UCARE Application - Year 2 Proposal</TITLE> +<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> +<meta http-equiv="Pragma" content="no-cache"> +<link rel="stylesheet" href="http://www.unl.edu/unlpub/styles/sitestyl.css"> +<style type="text/css"> +</style> + +<SCRIPT LANGUAGE="JavaScript"> +// Preload images +var empty = new Image(); empty.src = "redx.gif"; + +var haveerrors = 0; + +function showImage(imagename, imageurl, errors) { +document[imagename].src = imageurl; +if (!haveerrors && errors) haveerrors = errors; +} + +function val(studentsubmitform) { + +rExpEmail = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/ + +rExpZip = /^\d{5}$|^\d{5}[\-\s]?\d{4}$/ + +rExpPhone = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$/ + +rExpPhoneACOptional = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$|^\D?(\d{3})\D?(\d{4})$/ + +rExpPhoneACOptionalUNL = /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$|^\D?(\d{3})\D?(\d{4})$|^2\D?(\d{4})$/ + +rExpNUID = /^\d{8}$/ + +rExpURL = /((mailto\:|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/ + +rExpIPAddr = /\b((25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)\.){3}(25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)\b/ + +rExpName = /^([a-zA-Z]+\s?)+$/ + +rExpTime = /^([1-9]|1[0-2]):[0-5]\d$/ + +rExpDate = /^\d{1,2}(\-|\/|\.)\d{1,2}\1\d{4}$/ + +rExpDateBTW0619 = /^([1-9]|0[1-9]|1[0-2])[\-|\/|\.]([1-9]|0[1-9]|[12][0-9]|3[01])[\-|\/|\.](200[6-9]|201[0-9])$/ + +rExpDollarAmount = /\$\d{1,3}(,\d{3})*\.\d{2}/ + +rExpBobsAllDigits = /^\d+$/ + +rExpBobs3or4Digits = /^\d{3,4}$/ + +rExpBobsGradDate = /^\D+\x20\d{4}$|^\d{1,2}(\-|\/|\.)\d{4}$/ + +rExpBobsGPA = /^\d.\d{1,2}$/ + +rExpTitle = /^(w+\s?)+$/ + +haveerrors = 0; + +// validate student last name +(!rExpName.test(studentsubmitform.studentnamelast.value)) +? showImage("studentnamelasterror", "images/redx.gif", true) +: showImage("studentnamelasterror", "images/greencheck.gif", false); + +// validate student first name +(!rExpName.test(studentsubmitform.studentnamefirst.value)) +? showImage("studentnamefirsterror", "images/redx.gif", true) +: showImage("studentnamefirsterror", "images/greencheck.gif", false); + +// validate student middle name if data present +(!rExpName.test(studentsubmitform.studentnamemiddle.value)) && (studentsubmitform.studentnamemiddle.value.length > 0) +? showImage("studentnamemiddleerror", "images/redx.gif", true) +: showImage("studentnamemiddleerror", "images/greencheck.gif", false); + +// validate student id length and format; 9 digits or 3-2-4 +(!rExpNUID.test(studentsubmitform.studentid.value)) +? showImage("studentiderror", "images/redx.gif", true) +: showImage("studentiderror", "images/greencheck.gif", false); + +// validate student phone length and format +(!rExpPhoneACOptionalUNL.test(studentsubmitform.studentphone.value)) +? showImage("studentphoneerror", "images/redx.gif", true) +: showImage("studentphoneerror", "images/greencheck.gif", false); + +// validate student gpa length, format and value +(!rExpBobsGPA.test(studentsubmitform.studentgpa.value)) || (studentsubmitform.studentgpa.value > 4) +? showImage("studentgpaerror", "images/redx.gif", true) +: showImage("studentgpaerror", "images/greencheck.gif", false); + +// validate graduation date +(!rExpBobsGradDate.test(studentsubmitform.studentgraddate.value)) +? showImage("studentgraddateerror", "images/redx.gif", true) +: showImage("studentgraddateerror", "images/greencheck.gif", false); + +// validate major +(studentsubmitform.studentmajor.value.length < 1) +? showImage("studentmajorerror", "images/redx.gif", true) +: showImage("studentmajorerror", "images/greencheck.gif", false); + +// validate street address length +(studentsubmitform.studentlocaddr.value.length < 1) +? showImage("studentlocaddrerror", "images/redx.gif", true) +: showImage("studentlocaddrerror", "images/greencheck.gif", false); + +// validate projecttitle +(studentsubmitform.projecttitle.value.length < 1) +? showImage("projecttitleerror", "images/redx.gif", true) +: showImage("projecttitleerror", "images/greencheck.gif", false); + +// validate projectsummary +(studentsubmitform.projectsummary.value.length < 1) +? showImage("projectsummaryerror", "images/redx.gif", true) +: showImage("projectsummaryerror", "images/greencheck.gif", false); + +// validate class standing +(!studentsubmitform.studentclassstanding[0].checked && +!studentsubmitform.studentclassstanding[1].checked && +!studentsubmitform.studentclassstanding[2].checked && +!studentsubmitform.studentclassstanding[3].checked) +? showImage("studentclassstandingerror", "images/redx.gif", true) +: showImage("studentclassstandingerror", "images/greencheck.gif", false); + +// validate student email +(!rExpEmail.test(studentsubmitform.studentemail.value)) +? showImage("studentemailerror", "images/redx.gif", true) +: showImage("studentemailerror", "images/greencheck.gif", false); + +// validate project dates +(!rExpDateBTW0619.test(studentsubmitform.projectdurationfrom.value) || !rExpDateBTW0619.test(studentsubmitform.projectdurationto.value)) +|| +(Date.parse(studentsubmitform.projectdurationfrom.value) > Date.parse(studentsubmitform.projectdurationto.value)) +? showImage("projectdateserror", "images/redx.gif", true) +: showImage("projectdateserror", "images/greencheck.gif", false); + +// validate fund amount +(!studentsubmitform.projectfundamount[0].checked && !studentsubmitform.projectfundamount[1].checked && !studentsubmitform.projectfundamount[2].checked && !studentsubmitform.projectfundamount[3].checked && !studentsubmitform.projectfundamount[4].checked && !studentsubmitform.projectfundamount[5].checked) // if nothing's checked +|| +(!studentsubmitform.projectfundamount[0].checked && !studentsubmitform.projectfundamount[1].checked && !studentsubmitform.projectfundamount[2].checked && !studentsubmitform.projectfundamount[3].checked && !studentsubmitform.projectfundamount[4].checked && !studentsubmitform.projectfundamount[5].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if nothing's checked and there's something in the other amount field +|| +(studentsubmitform.projectfundamount[0].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the first project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[1].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the second project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[2].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the third project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[3].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the fourth project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[4].checked && studentsubmitform.projectfundotheramount.value.length > 0) // if the fifth project fund radio button is checked and there's anything in the other amount field +|| +(studentsubmitform.projectfundamount[5].checked && !rExpBobs3or4Digits.test(studentsubmitform.projectfundotheramount.value))// if the sixth project fund radio button is checked and there's anything other than 3 or 4 digits in the other amount field +|| +(studentsubmitform.projectfundamount[5].checked && studentsubmitform.projectfundotheramount.value > 5000)// if the sixth project fund radio button is checked and there's an amount over 5000 in the other amount field +? showImage("projectfundamounterror", "images/redx.gif", true) +: showImage("projectfundamounterror", "images/greencheck.gif", false); + +// validate sponsor last name +(!rExpName.test(studentsubmitform.sponsornamelast.value)) +? showImage("sponsornamelasterror", "images/redx.gif", true) +: showImage("sponsornamelasterror", "images/greencheck.gif", false); + +// validate sponsor first name +(!rExpName.test(studentsubmitform.sponsornamefirst.value)) +? showImage("sponsornamefirsterror", "images/redx.gif", true) +: showImage("sponsornamefirsterror", "images/greencheck.gif", false); + +// validate sponsor middle name if data present +(!rExpName.test(studentsubmitform.sponsornamemiddle.value)) && (studentsubmitform.sponsornamemiddle.value.length > 0) +? showImage("sponsornamemiddleerror", "images/redx.gif", true) +: showImage("sponsornamemiddleerror", "images/greencheck.gif", false); + +// validate sponsor email +(!rExpEmail.test(studentsubmitform.sponsoremail.value)) +? showImage("sponsoremailerror", "images/redx.gif", true) +: showImage("sponsoremailerror", "images/greencheck.gif", false); + +// validate sponsor phone length and format +(!rExpPhoneACOptionalUNL.test(studentsubmitform.sponsorphone.value)) +? showImage("sponsorphoneerror", "images/redx.gif", true) +: showImage("sponsorphoneerror", "images/greencheck.gif", false); + +// validate if irb compliance checkbox is checked +(!studentsubmitform.irbcomplystdt.checked) // if nothing's checked +? showImage("irbcomplystdt", "images/redx.gif", true) +: showImage("irbcomplystdt", "images/greencheck.gif", false); + + + return (!haveerrors); +} + + +// End --> +</script> + + +<SCRIPT LANGUAGE="JavaScript"> + +var submitcount=0; + +function checksubmits(studentsubmitform) { + d1=new Date() + studentsubmitform.studentsubmitdate.value=(d1.getMonth()+1)+"/"+(d1.getDate())+"/"+(d1.getFullYear()) + +if (submitcount == 0) + { + submitcount++; + return true; + } + else + { + alert("This form has already been submitted. Thanks!"); + return false; + } + } +// End --> + +</script> +</HEAD> + + +<BODY BGCOLOR="#f2f2f2" onLoad="self.focus()"> + <FORM name="studentsubmitform" onSubmit="return (val(this)&&checksubmits(this))" ACTION="FMPro" METHOD="post"> + <INPUT TYPE="hidden" NAME="-DB" VALUE="ucareapp"> + <INPUT TYPE="hidden" NAME="-Lay" VALUE="web"> + <INPUT TYPE="hidden" NAME="-format" VALUE="studentsubmitint1.htm"> + <INPUT TYPE="hidden" NAME="-error" VALUE="new_error.htm"> + <INPUT TYPE="hidden" NAME="studentnetaddress" VALUE="129.93.245.115"> + <INPUT TYPE="hidden" NAME="studentsubmitdate" VALUE=""> + <INPUT TYPE="hidden" NAME="hproposalyear" VALUE="YEAR 2"> + + <div align="center"> + <table width="720" border="0" align="center" cellpadding="6" cellspacing="0" bgcolor="#FFFFFF" class="sseriftext"> + <tr> + <td colspan="3" align="center"> + <span class="sserifstoryhead"> + <br> + UCARE APPLICATION<br> + YEAR 2 Proposal + </span> + + </td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"><span class="sseriftext"><i><br> + <strong>Hover over symbols</strong> ('?', 'X') <strong>for field requirements</strong>. Your internet address is 129.93.245.115.<br> + </i></span></td> + + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>STUDENT INFORMATION</strong></span></td> + </tr> + <tr valign="bottom"> + <td colspan="2"><table width="100%" border="0" cellspacing="0" cellpadding="0" class="sseriftext"> + <tr valign="bottom"> + <td>last name <img name=studentnamelasterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + + <INPUT NAME='studentnamelast' TYPE=text id="studentnamelast" SIZE=16></td> + <td>first name <img name=studentnamefirsterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='studentnamefirst' TYPE=text id="studentnamefirst" SIZE=16></td> + <td>middle <img name=studentnamemiddleerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='studentnamemiddle' TYPE=text id="studentnamemiddle" SIZE=8></td> + </tr> + </table></td> + <td>student ID <img name=studentiderror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be 8 digits in standard NUID form (NNNNNNNN)"><br> + + <INPUT NAME='studentid' TYPE=text id="studentid" SIZE=16></td> + </tr> + <tr valign="bottom"> + <td colspan="2">local address <img name=studentlocaddrerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter your street address, with city, state and zip if outside Lincoln"><br> + <INPUT NAME='studentlocaddr' TYPE=text id="studentlocaddr" SIZE=48></td> + <td>current class <img name=studentclassstandingerror src="images/yellowqmark24x.gif" width="24" height="24" title="Select one"><br> + <br> + + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='freshman' >freshman + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='sophomore' >sophomore + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='junior' >junior + <INPUT TYPE=radio NAME='studentclassstanding' VALUE='senior' >senior + </td> + + </tr> + <tr valign="bottom"> + <td>email <img name=studentemailerror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be a properly-formatted email address"><br> + <INPUT NAME='studentemail' TYPE=text id="studentemail" SIZE=24> + </td> + <td>phone <img name=studentphoneerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter phone number with or without area code; '2-' exchange acceptable for on-campus numbers"><br> + <INPUT NAME='studentphone' TYPE=text id="studentphone" SIZE=24></td> + <td> </td> + + </tr> + <tr valign="bottom"> + <td>major <img name=studentmajorerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter your major"><br> + <INPUT NAME='studentmajor' TYPE=text id="studentmajor" SIZE=24></td> + <td>graduation date (est.) <img name=studentgraddateerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter as Month YYYY or MM/YYYY"><br> + <INPUT NAME='studentgraddate' TYPE=text id="studentgraddate" SIZE=24></td> + <td>gpa <img name=studentgpaerror src="images/yellowqmark24x.gif" width="24" height="24" title="One digit, followed by a decimal, followed by one or two digits; not more than 4.00"><br> + + <INPUT NAME='studentgpa' TYPE=text id="studentgpa" SIZE=24></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"> </td> + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>PROJECT INFORMATION</strong></span></td> + </tr> + + <tr valign="bottom"> + <td colspan="3">project title <img name=projecttitleerror src="images/yellowqmark24x.gif" width="24" height="24" title="May contain any characters"><br> + <INPUT NAME='projecttitle' TYPE=text id="projecttitle" SIZE=90></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3">SUMMARY STATEMENT <img name=projectsummaryerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text"><br>Provide a 100 to 200-word summary of the project on which you will be working, including your proposed role in this project. Please include the scientific or creative methods to be employed and the significance of the work to the field of knowledge in general.<br> + <textarea name='projectsummary' rows=6 cols=90 wrap=virtual class="sseriftext"></textarea> + + </td> + </tr> + <tr valign="bottom"> + <td>duration <img name=projectdateserror src="images/yellowqmark24x.gif" width="24" height="24" title="MM/DD/YYYY format"><br> + <table width="100%" border="0" cellspacing="0" cellpadding="0" class="sseriftext"> + <tr> + <td>from<br> + <INPUT NAME='projectdurationfrom' TYPE=text id="projectdurationfrom" SIZE=12></td> + + <td>to<br> + <INPUT NAME='projectdurationto' TYPE=text id="projectdurationto" SIZE=12></td> + </tr> + </table></td> + <td colspan="2">amount requested <img name=projectfundamounterror src="images/yellowqmark24x.gif" width="24" height="24" title="Check an amount. If 'other amount,' must be from 100 to 5000, no punctuation"><br> + + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$500' >$500 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$1000' >$1000 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$1500' >$1500 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$2000' >$2000 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='$2400' >$2400 + <INPUT TYPE=radio NAME='projectfundamount' VALUE='other amount' >other amount + <br> + + other amount: + <INPUT NAME='projectfundotheramount' TYPE=text id="projectfundotheramount" SIZE=12></td> + </tr> + <tr valign="bottom" bgcolor="#FFFFFF"> + <td colspan="3"> </td> + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>SPONSOR INFORMATION</strong> (PROFESSOR FOR WHOM YOU WILL BE A RESEARCH ASSISTANT)</span></td> + + </tr> + <tr valign="bottom"> + <td>sponsor's last name <img name=sponsornamelasterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='sponsornamelast' TYPE=text id="sponsornamelast" SIZE=16></td> + <td><p>sponsor's first name <img name=sponsornamefirsterror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter any text characters"><br> + <INPUT NAME='sponsornamefirst' TYPE=text id="sponsornamefirst" SIZE=16> + <br> + </p> + + </td> + <td>sponsor's middle <img name=sponsornamemiddleerror src="images/yellowqmark24x.gif" width="24" height="24" title="Optional; may include any text characters"><br> + <INPUT NAME='sponsornamemiddle' TYPE=text id="sponsornamemiddle" SIZE=16></td> + </tr> + <tr valign="bottom"> + <td colspan="2">sponsor's email address <img name=sponsoremailerror src="images/yellowqmark24x.gif" width="24" height="24" title="Must be a properly-formatted email address"><br> + <INPUT NAME='sponsoremail' TYPE=text id="sponsoremail" SIZE=24></td> + <td>sponsor's phone <img name=sponsorphoneerror src="images/yellowqmark24x.gif" width="24" height="24" title="Enter phone number with or without area code; '2-' exchange acceptable for on-campus numbers"><br> + + <INPUT NAME='sponsorphone' TYPE=text id="sponsorphone" SIZE=24></td> + </tr> + <tr> + <td> </td> + <td> </td> + <td> </td> + </tr> + <tr valign="bottom" bgcolor="#f2f2f2"> + <td colspan="3"><span class="sseriftext"><strong>RESEARCH COMPLIANCE</strong></span></td> + + </tr> + <tr valign="bottom"> + <td colspan="3" valign="top">All research involving human subjects + at or associated with the University of Nebraska–Lincoln must + be reviewed and approved by the Institutional Review Board (IRB) + before research can proceed. Human research includes everything + from surveys to physical testing. Any human research that is conducted + without IRB approval can place the University, its faculty, staff, + students and human research participants in jeopardy. Further information + can be accessed from the following website: <a href="http://www.unl.edu/research/ReComp1/IRBguide2.pdf">http://www.unl.edu/research/ReComp1/IRBguide2.pdf</a>. + Any questions or concerns can be directed to Dr. Dan Vasgird, Research + Compliance Services, 472-1837 or email <a href="mailto:dvasgird2@unl.edu">dvasgird2@unl.edu</a>.<br> + <table border="0" cellpadding="0" width="100%" class=sseriftext> + <tr> + <td width="24"> + + <input type="checkbox" name="irbcomplystdt" value="irbcomplychecked"> + </td> + <td> + I have read this statement and will comply with IRB guidelines. + </td> + <td width="24"><img src="images/yellowqmark24x.gif" alt="helpanderror" name="irbcomplystdt" width="24" height="24" align="middle" title="You must acknowledge that you have read and understand the Research Compliance statement by checking the box provided."> + </td> + </tr> + </table> + + </td> + </tr> + + <tr> + <td> </td> + <td> </td> + <td> </td> + </tr> + <tr> + <td colspan="3"><div align="center"> + + </div> + <CENTER> + Please do not click the 'Submit My Application' button without first + carefully reviewing your entries.<br> + An application <i>cannot be modified</i> after + it is submitted.<br> + <br> + <IMG SRC="images/transpixel.gif"><br> + + <BR> + <INPUT TYPE="submit" NAME="-New" VALUE="Submit My Application"> + + <br> + </CENTER> + </td> + </tr> + </table> +</div> +</form> +</BODY> + +</HTML> diff --git a/lib/.configsnapshots/configsnapshot-2009-11-30 11-10-40.xml b/lib/.configsnapshots/configsnapshot-2009-11-30 11-10-40.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ae2f2fb8eef36453c6ad59e51f79ccf4a5cbf74 --- /dev/null +++ b/lib/.configsnapshots/configsnapshot-2009-11-30 11-10-40.xml @@ -0,0 +1,2 @@ +<?xml version="1.0"?> +<pearconfig version="1.0"><php_dir>/Users/bbieber/Documents/workspace/ucare_app/lib/php</php_dir><ext_dir>/usr/local/lib/php/extensions/no-debug-non-zts-20090115/</ext_dir><cfg_dir>/Users/bbieber/Sites/ucare_app/lib/cfg</cfg_dir><doc_dir>/Users/bbieber/Sites/ucare_app/lib/docs</doc_dir><bin_dir>/Users/bbieber/workspace/ucare_app/lib/bin</bin_dir><data_dir>/Users/bbieber/Documents/workspace/ucare_app/lib/data</data_dir><www_dir>/Users/bbieber/Sites/ucare_app/lib/www</www_dir><test_dir>/Users/bbieber/Sites/ucare_app/lib/tests</test_dir><src_dir>/Users/bbieber/Sites/ucare_app/lib/src</src_dir><php_bin>/usr/local/bin/php</php_bin><php_ini>/usr/local/lib/php.ini</php_ini><php_prefix></php_prefix><php_suffix></php_suffix></pearconfig> diff --git a/lib/.pear2registry b/lib/.pear2registry index 4b735cd904fc772daa7efb34c00e940528978569..0bebbd803ab67ae57535bf33ab2009bac3e0c9ce 100644 Binary files a/lib/.pear2registry and b/lib/.pear2registry differ diff --git a/lib/.xmlregistry/packages/pear.php.net/HTTP_Request2/0.5.1-info.xml b/lib/.xmlregistry/packages/pear.php.net/HTTP_Request2/0.5.1-info.xml new file mode 100644 index 0000000000000000000000000000000000000000..798696355fe2a8b290bef83567763992d7847629 --- /dev/null +++ b/lib/.xmlregistry/packages/pear.php.net/HTTP_Request2/0.5.1-info.xml @@ -0,0 +1,279 @@ +<?xml version="1.0" encoding="UTF-8"?> +<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> + <name>HTTP_Request2</name> + <channel>pear.php.net</channel> + <extends>HTTP_Request</extends> + <summary>Provides an easy way to perform HTTP requests.</summary> + <description>PHP5 rewrite of HTTP_Request package. Provides cleaner API and pluggable +Adapters. Currently available are: + * Socket adapter, based on old HTTP_Request code, + * Curl adapter, wraps around PHP's cURL extension, + * Mock adapter, to use for testing packages dependent on HTTP_Request2. +Supports POST requests with data and file uploads, basic and digest +authentication, cookies, proxies, gzip and deflate encodings, redirects, +monitoring the request progress with Observers...</description> + <lead> + <name>Alexey Borzov</name> + <user>avb</user> + <email>avb@php.net</email> + <active>yes</active> + </lead> + <date>2009-11-30</date> + <time>11:10:58</time> + <version> + <release>0.5.1</release> + <api>0.5.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +* Content-Type request header is no longer removed for POST and PUT requests + with empty request body (request #16799). +* CURLOPT_NOBODY option is now set when doing HEAD requests with Curl adapter. + </notes> + <contents> + <dir name="/"> + <file baseinstalldir="HTTP" md5sum="1fb55dfe18831f8fe6280280e72ad216" name="tests/_files/response_headers" role="test"/> + <file baseinstalldir="HTTP" md5sum="722328bfe89a9c9f7de5a020ed2c4589" name="tests/_files/response_gzip_broken" role="test"/> + <file baseinstalldir="HTTP" md5sum="c36530c79c044fde1745b244c38d381f" name="tests/_files/response_gzip" role="test"/> + <file baseinstalldir="HTTP" md5sum="12d80db889f528922a31b5c03f693647" name="tests/_files/response_deflate" role="test"/> + <file baseinstalldir="HTTP" md5sum="d1d2beb78782f56e8611100a009fb1f6" name="tests/_files/response_cookies" role="test"/> + <file baseinstalldir="HTTP" md5sum="120ea8a25e5d487bf68b5f7096440019" name="tests/_files/plaintext.txt" role="test"/> + <file baseinstalldir="HTTP" md5sum="fc94fb0c3ed8a8f909dbc7630a0987ff" name="tests/_files/empty.gif" role="test"/> + <file baseinstalldir="HTTP" md5sum="22d7f11b85dd00bd8919a4226a5a0388" name="tests/_files/bug_15305" role="test"/> + <file baseinstalldir="HTTP" md5sum="96e929eb014758b7b5712be5c45110b9" name="tests/Request2Test.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="7ae3f0ef61d832d166671b044191efbf" name="tests/Request2/ResponseTest.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="ec1c8a2a8ab448183ed831ebd1c20518" name="tests/Request2/MultipartBodyTest.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="3f9867c9347815f15eac65c87b5c55e3" name="tests/Request2/AllTests.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="fb8e45c13ca3fb1c8594e22df9ab90d2" name="tests/Request2/Adapter/MockTest.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="d389619e0dad1f891a8b18117177496f" name="tests/Request2/Adapter/AllTests.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="f8691b81e639395523bc828b569fa716" name="tests/ObserverTest.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="f24af905aa262adefa4f692e2fe2a167" name="tests/AllTests.php" role="test"/> + <file baseinstalldir="HTTP" md5sum="07bfa5cff153994f244ceee9bd77de2a" name="Request2/Response.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="10b4d5d88f298a4e2fe7df08e5a218bb" name="Request2/Observer/Log.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="20b26f829852793a0e545c5df648a9b6" name="Request2/MultipartBody.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="a0b53a6710a47bc718dabc0edef36555" name="Request2/Exception.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="496dda12e9ca16561d9237651b1065e3" name="Request2/Adapter/Socket.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="ee8a541eb43be5cd04fd392770f90548" name="Request2/Adapter/Mock.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="90c77d4b4425b4bb63cf6ee921e5e3d1" name="Request2/Adapter/Curl.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="043e3d6edeeec91f7d21bf825090c2dd" name="Request2/Adapter.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="588ec605871ca474a8bcc3bf8f8035fc" name="Request2.php" role="php"> + <tasks:replace from="@package_version@" to="version" type="package-info"/> + </file> + <file baseinstalldir="HTTP" md5sum="7e6017dfdf042dbd443ce6c8c024f40d" name="docs/examples/upload-rapidshare.php" role="doc"/> + </dir> + </contents> + <dependencies> + <required> + <php> + <min>5.1.4</min> + </php> + <pearinstaller> + <min>1.5.4</min> + </pearinstaller> + <package> + <name>Net_URL2</name> + <channel>pear.php.net</channel> + <min>0.2.0</min> + </package> + </required> + <optional> + <extension> + <name>curl</name> + </extension> + <extension> + <name>fileinfo</name> + </extension> + <extension> + <name>zlib</name> + </extension> + <extension> + <name>openssl</name> + </extension> + </optional> + </dependencies> + <phprelease> + <changelog> + <release> + <version> + <release>0.5.0</release> + <api>0.5.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-11-18</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +* Redirect support added, new configuration parameters 'follow_redirects', + 'max_redirects' and 'strict_redirects' available + +* Implemented workaround for PHP bug #47204, Curl Adapter can now handle + Digest authentication and redirects when doing POST requests, unfortunately + this requires loading the entire request body into memory. +* Config parameter 'use_brackets' is propagated to created instances of Net_URL2 +* Prevent memory leaks due to circular references (request #16646) +* Fixed a misleading error message when timing out due to default_socket_timeout +* HTTP_Request2::setBody() can now accept an instance of HTTP_Request2_MultipartBody + without trying to convert it to string +* Calling HTTP_Request2::setBody() now clears post parameters and uploads + </notes> + </release> + <release> + <version> + <release>0.4.1</release> + <api>0.4.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-09-14</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +* Decoding of gzipped responses failed if mbstring.func_overload was enabled + (bug #16555) +* Changed boundary generation in multipart bodies to work correctly with + rapidshare.com, added first usage example: file uploading to rapidshare.com +* Added forgotten optional dependency on OpenSSL PHP extension + </notes> + </release> + <release> + <version> + <release>0.4.0</release> + <api>0.4.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-05-03</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +* Added 'store_body' config parameter, if set to false it will prevent storing + the response body in Response object (request #15881) +* HTTP_Request2::setHeader() method now works as documented, setHeader('name') + will remove the 'name' header, while setHeader('name', '') will set 'name' + header to empty value (bug #15937) +* Custom 'Host' header will not be overwritten by generated one (bug #16146) +* When trying to reuse the connected socket in Socket adapter, make sure that + it is still connected (bug #16149) + </notes> + </release> + <release> + <version> + <release>0.3.0</release> + <api>0.3.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-01-28</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +API changes: + * Removed HTTP_Request2::getConfigValue() method + +Feature additions: + * Added digest authentication (RFC 2617) support to Socket adapter. Thanks + to Tom Snyder (tomsn at inetoffice dot com) who sent me a prototype + implementation for HTTP_Request a couple of years ago. + * Added HTTPS proxy support to Socket adapter, this works through CONNECT + request described in RFC 2817. + * Mock adapter can now throw an Exception instead of returning a response + if Exception object is added via its addResponse() method (request #15629) + +Other changes and fixes: + * Support RFC 3986 by not encoding '~' in POST body (request #15368) + * Prevent an error with particular versions of PHP and Curl (bug #15617) + * Regular expressions used in HTTP_Request2 are now class constants + (request #15630) + * Curl adapter now throws an exception in case of malformed (non-HTTP) + response rather than dies with a fatal error (bug #15716) + * Curl handle wasn't closed in Curl adapter in case of error (bug #15721) + * Curl adapter sent an extra 'sentHeaders' event and returned bogus + response status when server returned 100-Continue response (bug #15785) + </notes> + </release> + <release> + <version> + <release>0.2.0</release> + <api>0.2.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-01-07</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +API changes: + * HTTP_Request2::getConfigValue() is deprecated and will be removed in next + release. Use HTTP_Request2::getConfig(). + * Changed HTTP_Request2::setConfig() to accept a pair of parameter name and + parameter value in addition to array('parameter name' => 'value') + * Added HTTP_Request2::getConfig() method that can return a single + configuration parameter or the whole configuration array + +Other additions and changes: + * Added a debug Observer that can log request progress to a file or an + instance of PEAR::Log (thanks to David Jean Louis, request #15424) + * Added a new 'timeout' parameter that limits total number of seconds + a request can take (see requests #5735 and #8964) + * Added various SSL protocol options: 'ssl_verify_peer', 'ssl_verify_host', + 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase'. Note that + 'ssl_verify_host' option behaves differently in Socket and Curl Adapters: + http://bugs.php.net/bug.php?id=47030 + +Fixes: + * Fixed 'data error' when processing response encoded by 'deflate' + encoding (bug #15305) + * Curl Adapter now passes full request headers in 'sentHeaders' event + </notes> + </release> + <release> + <version> + <release>0.1.0</release> + <api>0.1.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2008-11-17</date> + <license uri="http://opensource.org/licenses/bsd-license.php">BSD License</license> + <notes> +Initial release. The features supported are mostly the same as those of +HTTP_Request, with the following additional feature requests implemented: + * cURL extension support (request #5463) + * It is now possible to monitor the file upload progress with Observers + (request #7630) + * Added 'sentHeaders' notification providing the request headers to the + Observers (request #7633) + * Added support for 'deflate' encoding (request #11246) + </notes> + </release> + </changelog> + </phprelease> +</package> diff --git a/lib/.xmlregistry/packages/pear.php.net/Net_URL2/0.3.0-info.xml b/lib/.xmlregistry/packages/pear.php.net/Net_URL2/0.3.0-info.xml new file mode 100644 index 0000000000000000000000000000000000000000..728043732018d4a7c9cfbd22550df0d91acbfa3d --- /dev/null +++ b/lib/.xmlregistry/packages/pear.php.net/Net_URL2/0.3.0-info.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.5.1" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> + <name>Net_URL2</name> + <channel>pear.php.net</channel> + <summary>Class for parsing and handling URL.</summary> + <description>Provides parsing of URLs into their constituent parts (scheme, host, path etc.), URL generation, and resolving of relative URLs.</description> + <lead> + <name>David Coallier</name> + <user>davidc</user> + <email>davidc@php.net</email> + <active>yes</active> + </lead> + <lead> + <name>Christian Schmidt</name> + <user>schmidt</user> + <email>schmidt@php.net</email> + <active>yes</active> + </lead> + <date>2009-11-30</date> + <time>11:10:40</time> + <version> + <release>0.3.0</release> + <api>0.3.0</api> + </version> + <stability> + <release>beta</release> + <api>beta</api> + </stability> + <license uri="http://www.opensource.org/licenses/bsd-license.php">BSD</license> + <notes>* Fixed #14399 (Errors in URL parsing (items #1 and #3)) +* Fixed #14735 (Encode query string values) +* Fixed #15546 (Add adding __toString()) +* Fixed #15367 (Use RFC 3986-compliant version of rawurlencode() in PHP < 5.2) +* Fixed #14289 (Add __get() and __set())</notes> + <contents> + <dir name="/"> + <file baseinstalldir="Net" name="URL2.php" role="php" md5sum="8a713828923fe747e969966d1d297087"/> + <file baseinstalldir="Net" name="docs/example.php" role="doc" md5sum="587a224d39fbffa47cacb9fb67b51da1"/> + <file baseinstalldir="Net" name="docs/6470.php" role="doc" md5sum="ea8b73061588566519fd0e55a230f2a6"/> + </dir> + </contents> + <dependencies> + <required> + <php> + <min>5.0</min> + </php> + <pearinstaller> + <min>1.4.0b1</min> + </pearinstaller> + </required> + </dependencies> + <phprelease> + <changelog> + <release> + <version> + <release>0.2.0</release> + <api>0.2.0</api> + </version> + <stability> + <release>beta</release> + <api>beta</api> + </stability> + <date>2008-06-18</date> + <license>BSD</license> + <notes>Major rewrite to comply with RFC3986 (bug 11574). + Much better support for resolving relative URLs. + WARNING: Method and property names has changed to reflect the terminology used in the RFC - THIS RELEASE IS NOT BACKWARDS COMPATIBLE WITH VERSION 0.1.0.</notes> + </release> + <release> + <version> + <release>0.1.0</release> + <api>0.1.0</api> + </version> + <stability> + <release>beta</release> + <api>beta</api> + </stability> + <date>2007-05-08</date> + <license>BSD</license> + <notes>Convert to PHP5 only. PHP4 users should continue with version 1.0.15</notes> + </release> + </changelog> + </phprelease> +</package> diff --git a/lib/.xmlregistry/packages/simplecas.googlecode.com!svn/SimpleCAS/0.3.1-info.xml b/lib/.xmlregistry/packages/simplecas.googlecode.com!svn/SimpleCAS/0.3.1-info.xml new file mode 100644 index 0000000000000000000000000000000000000000..c27e9f7fba9c79361075e47a55722167aa2ab60a --- /dev/null +++ b/lib/.xmlregistry/packages/simplecas.googlecode.com!svn/SimpleCAS/0.3.1-info.xml @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.8.0alpha2" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> + <name>SimpleCAS</name> + <channel>simplecas.googlecode.com/svn</channel> + <summary>A PHP5 library for CAS Authentication.</summary> + <description>This package is a PHP5 only library for identifying users +in a JA-SIG CAS secured environment.</description> + <lead> + <name>Brett Bieber</name> + <user>saltybeagle</user> + <email>brett.bieber@gmail.com</email> + <active>yes</active> + </lead> + <helper> + <name>John Thiltges</name> + <user>jthiltges</user> + <email>jthiltges@gmail.com</email> + <active>yes</active> + </helper> + <date>2009-11-30</date> + <time>11:10:58</time> + <version> + <release>0.3.1</release> + <api>0.3.1</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Prefix session variables with __SIMPLECAS [saltybeagle] + </notes> + <contents> + <dir name="/"> + <file baseinstalldir="/" md5sum="ad9c06f186ce8fa739d6e716f6c9b48d" name="SimpleCAS/SingleSignOut.php" role="php"/> + <file baseinstalldir="/" md5sum="7a49aa1c67563ed628202c3e9244dcdc" name="SimpleCAS/ProxyGranting/Storage/File.php" role="php"/> + <file baseinstalldir="/" md5sum="26b6d753c3f9098be6493c818aabd959" name="SimpleCAS/ProxyGranting/Storage.php" role="php"/> + <file baseinstalldir="/" md5sum="e96b4c13218c39664fdeb8dc8d4e7541" name="SimpleCAS/ProxyGranting.php" role="php"/> + <file baseinstalldir="/" md5sum="46feaaf938c6eb0f7f260125a646b3fb" name="SimpleCAS/Protocol/Version2/ValidationResponse.php" role="php"/> + <file baseinstalldir="/" md5sum="7848ea2f951f206b1ee829bb61ff3b9f" name="SimpleCAS/Protocol/Version2.php" role="php"/> + <file baseinstalldir="/" md5sum="0401bb6d1c85808706ff83265876b82b" name="SimpleCAS/Protocol/Version1.php" role="php"/> + <file baseinstalldir="/" md5sum="1a6ad6fae9ab04cd85c163eca232b80f" name="SimpleCAS/Protocol.php" role="php"/> + <file baseinstalldir="/" md5sum="d93480efa786598948935145b06352a6" name="SimpleCAS/Autoload.php" role="php"/> + <file baseinstalldir="/" md5sum="102cea2567d470e2bdd644ac8bc932e5" name="SimpleCAS.php" role="php"/> + <file baseinstalldir="/" md5sum="9d686c3553b6c8be0af988ddfb363f0c" name="docs/examples/Zend_Auth_Adapter_SimpleCAS.php" role="doc"/> + <file baseinstalldir="/" md5sum="2254593f10efc23cd0eadab309506e15" name="docs/examples/simple.php" role="doc"/> + </dir> + </contents> + <dependencies> + <required> + <php> + <min>5.2.5</min> + </php> + <pearinstaller> + <min>1.5.4</min> + </pearinstaller> + <package> + <name>HTTP_Request2</name> + <channel>pear.php.net</channel> + <min>0.1.0</min> + </package> + </required> + </dependencies> + <phprelease> + <changelog> + <release> + <version> + <release>0.1.0</release> + <api>0.1.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2008-12-08</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +First Release. + </notes> + </release> + <release> + <version> + <release>0.1.1</release> + <api>0.1.1</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2008-12-08</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Fix Notice: Trying to get property of non-object in SimpleCAS/Server/Version2/ValidationResponse.php on line 23 +* Change PHP dependency to 5.2.5 + </notes> + </release> + <release> + <version> + <release>0.1.2</release> + <api>0.1.1</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-02-05</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Update PEAR dependency to 1.5.4 +* Match case for variables (jthiltges) +* In CAS v2 validateTicket, typecast successful return value to string (jthiltges) + </notes> + </release> + <release> + <version> + <release>0.2.0</release> + <api>0.2.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-02-11</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Allow setting HTTP_Request2 object so configuration can be set. + $server->getRequest()->setConfig('ssl_verify_peer', false); +* Add $server->getRequest(), $server->setRequest(HTTP_Request2 $http_request) +Change interface for server to abstract class. + </notes> + </release> + <release> + <version> + <release>0.2.1</release> + <api>0.2.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-02-12</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Explicitly call __toString() for PHP 5.1 compatibility. [jthiltges] +* Add Zend_Auth_Adapter_SimpleCAS to the examples. [jthiltges] + </notes> + </release> + <release> + <version> + <release>0.3.0</release> + <api>0.3.0</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-03-03</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Issue 1: Rename SimpleCAS_Server to SimpleCAS_Protocol [saltybeagle] +* Issue 2: Switch to arrays for the protocol constructors. [saltybeagle] +* Exit immediately after re-direct. + </notes> + </release> + <release> + <version> + <release>0.3.1</release> + <api>0.3.1</api> + </version> + <stability> + <release>alpha</release> + <api>alpha</api> + </stability> + <date>2009-04-06</date> + <license uri="http://www1.unl.edu/wdn/wiki/Software_License">BSD License</license> + <notes> +* Prefix session variables with __SIMPLECAS [saltybeagle] + </notes> + </release> + </changelog> + </phprelease> +</package> diff --git a/lib/php/HTTP/Request2.php b/lib/php/HTTP/Request2.php new file mode 100644 index 0000000000000000000000000000000000000000..19dfe66a9e58e2b21c9cb8e1b1e2e70fc1075b16 --- /dev/null +++ b/lib/php/HTTP/Request2.php @@ -0,0 +1,861 @@ +<?php +/** + * Class representing a HTTP request message + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Request2.php 290921 2009-11-18 17:31:58Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * A class representing an URL as per RFC 3986. + */ +require_once 'Net/URL2.php'; + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Class representing a HTTP request message + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + * @link http://tools.ietf.org/html/rfc2616#section-5 + */ +class HTTP_Request2 implements SplSubject +{ + /**#@+ + * Constants for HTTP request methods + * + * @link http://tools.ietf.org/html/rfc2616#section-5.1.1 + */ + const METHOD_OPTIONS = 'OPTIONS'; + const METHOD_GET = 'GET'; + const METHOD_HEAD = 'HEAD'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_DELETE = 'DELETE'; + const METHOD_TRACE = 'TRACE'; + const METHOD_CONNECT = 'CONNECT'; + /**#@-*/ + + /**#@+ + * Constants for HTTP authentication schemes + * + * @link http://tools.ietf.org/html/rfc2617 + */ + const AUTH_BASIC = 'basic'; + const AUTH_DIGEST = 'digest'; + /**#@-*/ + + /** + * Regular expression used to check for invalid symbols in RFC 2616 tokens + * @link http://pear.php.net/bugs/bug.php?id=15630 + */ + const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!'; + + /** + * Regular expression used to check for invalid symbols in cookie strings + * @link http://pear.php.net/bugs/bug.php?id=15630 + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + */ + const REGEXP_INVALID_COOKIE = '/[\s,;]/'; + + /** + * Fileinfo magic database resource + * @var resource + * @see detectMimeType() + */ + private static $_fileinfoDb; + + /** + * Observers attached to the request (instances of SplObserver) + * @var array + */ + protected $observers = array(); + + /** + * Request URL + * @var Net_URL2 + */ + protected $url; + + /** + * Request method + * @var string + */ + protected $method = self::METHOD_GET; + + /** + * Authentication data + * @var array + * @see getAuth() + */ + protected $auth; + + /** + * Request headers + * @var array + */ + protected $headers = array(); + + /** + * Configuration parameters + * @var array + * @see setConfig() + */ + protected $config = array( + 'adapter' => 'HTTP_Request2_Adapter_Socket', + 'connect_timeout' => 10, + 'timeout' => 0, + 'use_brackets' => true, + 'protocol_version' => '1.1', + 'buffer_size' => 16384, + 'store_body' => true, + + 'proxy_host' => '', + 'proxy_port' => '', + 'proxy_user' => '', + 'proxy_password' => '', + 'proxy_auth_scheme' => self::AUTH_BASIC, + + 'ssl_verify_peer' => true, + 'ssl_verify_host' => true, + 'ssl_cafile' => null, + 'ssl_capath' => null, + 'ssl_local_cert' => null, + 'ssl_passphrase' => null, + + 'digest_compat_ie' => false, + + 'follow_redirects' => false, + 'max_redirects' => 5, + 'strict_redirects' => false + ); + + /** + * Last event in request / response handling, intended for observers + * @var array + * @see getLastEvent() + */ + protected $lastEvent = array( + 'name' => 'start', + 'data' => null + ); + + /** + * Request body + * @var string|resource + * @see setBody() + */ + protected $body = ''; + + /** + * Array of POST parameters + * @var array + */ + protected $postParams = array(); + + /** + * Array of file uploads (for multipart/form-data POST requests) + * @var array + */ + protected $uploads = array(); + + /** + * Adapter used to perform actual HTTP request + * @var HTTP_Request2_Adapter + */ + protected $adapter; + + + /** + * Constructor. Can set request URL, method and configuration array. + * + * Also sets a default value for User-Agent header. + * + * @param string|Net_Url2 Request URL + * @param string Request method + * @param array Configuration for this Request instance + */ + public function __construct($url = null, $method = self::METHOD_GET, array $config = array()) + { + $this->setConfig($config); + if (!empty($url)) { + $this->setUrl($url); + } + if (!empty($method)) { + $this->setMethod($method); + } + $this->setHeader('user-agent', 'HTTP_Request2/0.5.1 ' . + '(http://pear.php.net/package/http_request2) ' . + 'PHP/' . phpversion()); + } + + /** + * Sets the URL for this request + * + * If the URL has userinfo part (username & password) these will be removed + * and converted to auth data. If the URL does not have a path component, + * that will be set to '/'. + * + * @param string|Net_URL2 Request URL + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function setUrl($url) + { + if (is_string($url)) { + $url = new Net_URL2( + $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets']) + ); + } + if (!$url instanceof Net_URL2) { + throw new HTTP_Request2_Exception('Parameter is not a valid HTTP URL'); + } + // URL contains username / password? + if ($url->getUserinfo()) { + $username = $url->getUser(); + $password = $url->getPassword(); + $this->setAuth(rawurldecode($username), $password? rawurldecode($password): ''); + $url->setUserinfo(''); + } + if ('' == $url->getPath()) { + $url->setPath('/'); + } + $this->url = $url; + + return $this; + } + + /** + * Returns the request URL + * + * @return Net_URL2 + */ + public function getUrl() + { + return $this->url; + } + + /** + * Sets the request method + * + * @param string + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception if the method name is invalid + */ + public function setMethod($method) + { + // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1 + if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) { + throw new HTTP_Request2_Exception("Invalid request method '{$method}'"); + } + $this->method = $method; + + return $this; + } + + /** + * Returns the request method + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Sets the configuration parameter(s) + * + * The following parameters are available: + * <ul> + * <li> 'adapter' - adapter to use (string)</li> + * <li> 'connect_timeout' - Connection timeout in seconds (integer)</li> + * <li> 'timeout' - Total number of seconds a request can take. + * Use 0 for no limit, should be greater than + * 'connect_timeout' if set (integer)</li> + * <li> 'use_brackets' - Whether to append [] to array variable names (bool)</li> + * <li> 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)</li> + * <li> 'buffer_size' - Buffer size to use for reading and writing (int)</li> + * <li> 'store_body' - Whether to store response body in response object. + * Set to false if receiving a huge response and + * using an Observer to save it (boolean)</li> + * <li> 'proxy_host' - Proxy server host (string)</li> + * <li> 'proxy_port' - Proxy server port (integer)</li> + * <li> 'proxy_user' - Proxy auth username (string)</li> + * <li> 'proxy_password' - Proxy auth password (string)</li> + * <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li> + * <li> 'ssl_verify_peer' - Whether to verify peer's SSL certificate (bool)</li> + * <li> 'ssl_verify_host' - Whether to check that Common Name in SSL + * certificate matches host name (bool)</li> + * <li> 'ssl_cafile' - Cerificate Authority file to verify the peer + * with (use with 'ssl_verify_peer') (string)</li> + * <li> 'ssl_capath' - Directory holding multiple Certificate + * Authority files (string)</li> + * <li> 'ssl_local_cert' - Name of a file containing local cerificate (string)</li> + * <li> 'ssl_passphrase' - Passphrase with which local certificate + * was encoded (string)</li> + * <li> 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6 + * in using URL without query string in digest + * authentication (boolean)</li> + * <li> 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)</li> + * <li> 'max_redirects' - Maximum number of redirects to follow (integer)</li> + * <li> 'strict_redirects' - Whether to keep request method on redirects via status 301 and + * 302 (true, needed for compatibility with RFC 2616) + * or switch to GET (false, needed for compatibility with most + * browsers) (boolean)</li> + * </ul> + * + * @param string|array configuration parameter name or array + * ('parameter name' => 'parameter value') + * @param mixed parameter value if $nameOrConfig is not an array + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception If the parameter is unknown + */ + public function setConfig($nameOrConfig, $value = null) + { + if (is_array($nameOrConfig)) { + foreach ($nameOrConfig as $name => $value) { + $this->setConfig($name, $value); + } + + } else { + if (!array_key_exists($nameOrConfig, $this->config)) { + throw new HTTP_Request2_Exception( + "Unknown configuration parameter '{$nameOrConfig}'" + ); + } + $this->config[$nameOrConfig] = $value; + } + + return $this; + } + + /** + * Returns the value(s) of the configuration parameter(s) + * + * @param string parameter name + * @return mixed value of $name parameter, array of all configuration + * parameters if $name is not given + * @throws HTTP_Request2_Exception If the parameter is unknown + */ + public function getConfig($name = null) + { + if (null === $name) { + return $this->config; + } elseif (!array_key_exists($name, $this->config)) { + throw new HTTP_Request2_Exception( + "Unknown configuration parameter '{$name}'" + ); + } + return $this->config[$name]; + } + + /** + * Sets the autentification data + * + * @param string user name + * @param string password + * @param string authentication scheme + * @return HTTP_Request2 + */ + public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC) + { + if (empty($user)) { + $this->auth = null; + } else { + $this->auth = array( + 'user' => (string)$user, + 'password' => (string)$password, + 'scheme' => $scheme + ); + } + + return $this; + } + + /** + * Returns the authentication data + * + * The array has the keys 'user', 'password' and 'scheme', where 'scheme' + * is one of the HTTP_Request2::AUTH_* constants. + * + * @return array + */ + public function getAuth() + { + return $this->auth; + } + + /** + * Sets request header(s) + * + * The first parameter may be either a full header string 'header: value' or + * header name. In the former case $value parameter is ignored, in the latter + * the header's value will either be set to $value or the header will be + * removed if $value is null. The first parameter can also be an array of + * headers, in that case method will be called recursively. + * + * Note that headers are treated case insensitively as per RFC 2616. + * + * <code> + * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar' + * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz' + * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux' + * $req->setHeader('FOO'); // removes 'Foo' header from request + * </code> + * + * @param string|array header name, header string ('Header: value') + * or an array of headers + * @param string|null header value, header will be removed if null + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function setHeader($name, $value = null) + { + if (is_array($name)) { + foreach ($name as $k => $v) { + if (is_string($k)) { + $this->setHeader($k, $v); + } else { + $this->setHeader($v); + } + } + } else { + if (null === $value && strpos($name, ':')) { + list($name, $value) = array_map('trim', explode(':', $name, 2)); + } + // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2 + if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) { + throw new HTTP_Request2_Exception("Invalid header name '{$name}'"); + } + // Header names are case insensitive anyway + $name = strtolower($name); + if (null === $value) { + unset($this->headers[$name]); + } else { + $this->headers[$name] = $value; + } + } + + return $this; + } + + /** + * Returns the request headers + * + * The array is of the form ('header name' => 'header value'), header names + * are lowercased + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Appends a cookie to "Cookie:" header + * + * @param string cookie name + * @param string cookie value + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function addCookie($name, $value) + { + $cookie = $name . '=' . $value; + if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) { + throw new HTTP_Request2_Exception("Invalid cookie: '{$cookie}'"); + } + $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; '; + $this->setHeader('cookie', $cookies . $cookie); + + return $this; + } + + /** + * Sets the request body + * + * @param string Either a string with the body or filename containing body + * @param bool Whether first parameter is a filename + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function setBody($body, $isFilename = false) + { + if (!$isFilename) { + if (!$body instanceof HTTP_Request2_MultipartBody) { + $this->body = (string)$body; + } else { + $this->body = $body; + } + } else { + if (!($fp = @fopen($body, 'rb'))) { + throw new HTTP_Request2_Exception("Cannot open file {$body}"); + } + $this->body = $fp; + if (empty($this->headers['content-type'])) { + $this->setHeader('content-type', self::detectMimeType($body)); + } + } + $this->postParams = $this->uploads = array(); + + return $this; + } + + /** + * Returns the request body + * + * @return string|resource|HTTP_Request2_MultipartBody + */ + public function getBody() + { + if (self::METHOD_POST == $this->method && + (!empty($this->postParams) || !empty($this->uploads)) + ) { + if ('application/x-www-form-urlencoded' == $this->headers['content-type']) { + $body = http_build_query($this->postParams, '', '&'); + if (!$this->getConfig('use_brackets')) { + $body = preg_replace('/%5B\d+%5D=/', '=', $body); + } + // support RFC 3986 by not encoding '~' symbol (request #15368) + return str_replace('%7E', '~', $body); + + } elseif ('multipart/form-data' == $this->headers['content-type']) { + require_once 'HTTP/Request2/MultipartBody.php'; + return new HTTP_Request2_MultipartBody( + $this->postParams, $this->uploads, $this->getConfig('use_brackets') + ); + } + } + return $this->body; + } + + /** + * Adds a file to form-based file upload + * + * Used to emulate file upload via a HTML form. The method also sets + * Content-Type of HTTP request to 'multipart/form-data'. + * + * If you just want to send the contents of a file as the body of HTTP + * request you should use setBody() method. + * + * @param string name of file-upload field + * @param mixed full name of local file + * @param string filename to send in the request + * @param string content-type of file being uploaded + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function addUpload($fieldName, $filename, $sendFilename = null, + $contentType = null) + { + if (!is_array($filename)) { + if (!($fp = @fopen($filename, 'rb'))) { + throw new HTTP_Request2_Exception("Cannot open file {$filename}"); + } + $this->uploads[$fieldName] = array( + 'fp' => $fp, + 'filename' => empty($sendFilename)? basename($filename): $sendFilename, + 'size' => filesize($filename), + 'type' => empty($contentType)? self::detectMimeType($filename): $contentType + ); + } else { + $fps = $names = $sizes = $types = array(); + foreach ($filename as $f) { + if (!is_array($f)) { + $f = array($f); + } + if (!($fp = @fopen($f[0], 'rb'))) { + throw new HTTP_Request2_Exception("Cannot open file {$f[0]}"); + } + $fps[] = $fp; + $names[] = empty($f[1])? basename($f[0]): $f[1]; + $sizes[] = filesize($f[0]); + $types[] = empty($f[2])? self::detectMimeType($f[0]): $f[2]; + } + $this->uploads[$fieldName] = array( + 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types + ); + } + if (empty($this->headers['content-type']) || + 'application/x-www-form-urlencoded' == $this->headers['content-type'] + ) { + $this->setHeader('content-type', 'multipart/form-data'); + } + + return $this; + } + + /** + * Adds POST parameter(s) to the request. + * + * @param string|array parameter name or array ('name' => 'value') + * @param mixed parameter value (can be an array) + * @return HTTP_Request2 + */ + public function addPostParameter($name, $value = null) + { + if (!is_array($name)) { + $this->postParams[$name] = $value; + } else { + foreach ($name as $k => $v) { + $this->addPostParameter($k, $v); + } + } + if (empty($this->headers['content-type'])) { + $this->setHeader('content-type', 'application/x-www-form-urlencoded'); + } + + return $this; + } + + /** + * Attaches a new observer + * + * @param SplObserver + */ + public function attach(SplObserver $observer) + { + foreach ($this->observers as $attached) { + if ($attached === $observer) { + return; + } + } + $this->observers[] = $observer; + } + + /** + * Detaches an existing observer + * + * @param SplObserver + */ + public function detach(SplObserver $observer) + { + foreach ($this->observers as $key => $attached) { + if ($attached === $observer) { + unset($this->observers[$key]); + return; + } + } + } + + /** + * Notifies all observers + */ + public function notify() + { + foreach ($this->observers as $observer) { + $observer->update($this); + } + } + + /** + * Sets the last event + * + * Adapters should use this method to set the current state of the request + * and notify the observers. + * + * @param string event name + * @param mixed event data + */ + public function setLastEvent($name, $data = null) + { + $this->lastEvent = array( + 'name' => $name, + 'data' => $data + ); + $this->notify(); + } + + /** + * Returns the last event + * + * Observers should use this method to access the last change in request. + * The following event names are possible: + * <ul> + * <li>'connect' - after connection to remote server, + * data is the destination (string)</li> + * <li>'disconnect' - after disconnection from server</li> + * <li>'sentHeaders' - after sending the request headers, + * data is the headers sent (string)</li> + * <li>'sentBodyPart' - after sending a part of the request body, + * data is the length of that part (int)</li> + * <li>'receivedHeaders' - after receiving the response headers, + * data is HTTP_Request2_Response object</li> + * <li>'receivedBodyPart' - after receiving a part of the response + * body, data is that part (string)</li> + * <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still + * encoded by Content-Encoding</li> + * <li>'receivedBody' - after receiving the complete response + * body, data is HTTP_Request2_Response object</li> + * </ul> + * Different adapters may not send all the event types. Mock adapter does + * not send any events to the observers. + * + * @return array The array has two keys: 'name' and 'data' + */ + public function getLastEvent() + { + return $this->lastEvent; + } + + /** + * Sets the adapter used to actually perform the request + * + * You can pass either an instance of a class implementing HTTP_Request2_Adapter + * or a class name. The method will only try to include a file if the class + * name starts with HTTP_Request2_Adapter_, it will also try to prepend this + * prefix to the class name if it doesn't contain any underscores, so that + * <code> + * $request->setAdapter('curl'); + * </code> + * will work. + * + * @param string|HTTP_Request2_Adapter + * @return HTTP_Request2 + * @throws HTTP_Request2_Exception + */ + public function setAdapter($adapter) + { + if (is_string($adapter)) { + if (!class_exists($adapter, false)) { + if (false === strpos($adapter, '_')) { + $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter); + } + if (preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)) { + include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php'; + } + if (!class_exists($adapter, false)) { + throw new HTTP_Request2_Exception("Class {$adapter} not found"); + } + } + $adapter = new $adapter; + } + if (!$adapter instanceof HTTP_Request2_Adapter) { + throw new HTTP_Request2_Exception('Parameter is not a HTTP request adapter'); + } + $this->adapter = $adapter; + + return $this; + } + + /** + * Sends the request and returns the response + * + * @throws HTTP_Request2_Exception + * @return HTTP_Request2_Response + */ + public function send() + { + // Sanity check for URL + if (!$this->url instanceof Net_URL2) { + throw new HTTP_Request2_Exception('No URL given'); + } elseif (!$this->url->isAbsolute()) { + throw new HTTP_Request2_Exception('Absolute URL required'); + } elseif (!in_array(strtolower($this->url->getScheme()), array('https', 'http'))) { + throw new HTTP_Request2_Exception('Not a HTTP URL'); + } + if (empty($this->adapter)) { + $this->setAdapter($this->getConfig('adapter')); + } + // magic_quotes_runtime may break file uploads and chunked response + // processing; see bug #4543 + if ($magicQuotes = ini_get('magic_quotes_runtime')) { + ini_set('magic_quotes_runtime', false); + } + // force using single byte encoding if mbstring extension overloads + // strlen() and substr(); see bug #1781, bug #10605 + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('iso-8859-1'); + } + + try { + $response = $this->adapter->sendRequest($this); + } catch (Exception $e) { + } + // cleanup in either case (poor man's "finally" clause) + if ($magicQuotes) { + ini_set('magic_quotes_runtime', true); + } + if (!empty($oldEncoding)) { + mb_internal_encoding($oldEncoding); + } + // rethrow the exception + if (!empty($e)) { + throw $e; + } + return $response; + } + + /** + * Tries to detect MIME type of a file + * + * The method will try to use fileinfo extension if it is available, + * deprecated mime_content_type() function in the other case. If neither + * works, default 'application/octet-stream' MIME type is returned + * + * @param string filename + * @return string file MIME type + */ + protected static function detectMimeType($filename) + { + // finfo extension from PECL available + if (function_exists('finfo_open')) { + if (!isset(self::$_fileinfoDb)) { + self::$_fileinfoDb = @finfo_open(FILEINFO_MIME); + } + if (self::$_fileinfoDb) { + $info = finfo_file(self::$_fileinfoDb, $filename); + } + } + // (deprecated) mime_content_type function available + if (empty($info) && function_exists('mime_content_type')) { + return mime_content_type($filename); + } + return empty($info)? 'application/octet-stream': $info; + } +} +?> \ No newline at end of file diff --git a/lib/php/HTTP/Request2/Adapter.php b/lib/php/HTTP/Request2/Adapter.php new file mode 100644 index 0000000000000000000000000000000000000000..16ff131bd155a0bb596caa0d0756adddfcbed7d0 --- /dev/null +++ b/lib/php/HTTP/Request2/Adapter.php @@ -0,0 +1,154 @@ +<?php +/** + * Base class for HTTP_Request2 adapters + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Adapter.php 291118 2009-11-21 17:58:23Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Class representing a HTTP response + */ +require_once 'HTTP/Request2/Response.php'; + +/** + * Base class for HTTP_Request2 adapters + * + * HTTP_Request2 class itself only defines methods for aggregating the request + * data, all actual work of sending the request to the remote server and + * receiving its response is performed by adapters. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + */ +abstract class HTTP_Request2_Adapter +{ + /** + * A list of methods that MUST NOT have a request body, per RFC 2616 + * @var array + */ + protected static $bodyDisallowed = array('TRACE'); + + /** + * Methods having defined semantics for request body + * + * Content-Length header (indicating that the body follows, section 4.3 of + * RFC 2616) will be sent for these methods even if no body was added + * + * @var array + * @link http://pear.php.net/bugs/bug.php?id=12900 + * @link http://pear.php.net/bugs/bug.php?id=14740 + */ + protected static $bodyRequired = array('POST', 'PUT'); + + /** + * Request being sent + * @var HTTP_Request2 + */ + protected $request; + + /** + * Request body + * @var string|resource|HTTP_Request2_MultipartBody + * @see HTTP_Request2::getBody() + */ + protected $requestBody; + + /** + * Length of the request body + * @var integer + */ + protected $contentLength; + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + abstract public function sendRequest(HTTP_Request2 $request); + + /** + * Calculates length of the request body, adds proper headers + * + * @param array associative array of request headers, this method will + * add proper 'Content-Length' and 'Content-Type' headers + * to this array (or remove them if not needed) + */ + protected function calculateRequestLength(&$headers) + { + $this->requestBody = $this->request->getBody(); + + if (is_string($this->requestBody)) { + $this->contentLength = strlen($this->requestBody); + } elseif (is_resource($this->requestBody)) { + $stat = fstat($this->requestBody); + $this->contentLength = $stat['size']; + rewind($this->requestBody); + } else { + $this->contentLength = $this->requestBody->getLength(); + $headers['content-type'] = 'multipart/form-data; boundary=' . + $this->requestBody->getBoundary(); + $this->requestBody->rewind(); + } + + if (in_array($this->request->getMethod(), self::$bodyDisallowed) || + 0 == $this->contentLength + ) { + // No body: send a Content-Length header nonetheless (request #12900), + // but do that only for methods that require a body (bug #14740) + if (in_array($this->request->getMethod(), self::$bodyRequired)) { + $headers['content-length'] = 0; + } else { + unset($headers['content-length']); + // if the method doesn't require a body and doesn't have a + // body, don't send a Content-Type header. (request #16799) + unset($headers['content-type']); + } + } else { + if (empty($headers['content-type'])) { + $headers['content-type'] = 'application/x-www-form-urlencoded'; + } + $headers['content-length'] = $this->contentLength; + } + } +} +?> diff --git a/lib/php/HTTP/Request2/Adapter/Curl.php b/lib/php/HTTP/Request2/Adapter/Curl.php new file mode 100644 index 0000000000000000000000000000000000000000..77ab6fae02b026e6944c71ec663dcbb7c5cf5988 --- /dev/null +++ b/lib/php/HTTP/Request2/Adapter/Curl.php @@ -0,0 +1,461 @@ +<?php +/** + * Adapter for HTTP_Request2 wrapping around cURL extension + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Curl.php 291118 2009-11-21 17:58:23Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Adapter for HTTP_Request2 wrapping around cURL extension + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + */ +class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter +{ + /** + * Mapping of header names to cURL options + * @var array + */ + protected static $headerMap = array( + 'accept-encoding' => CURLOPT_ENCODING, + 'cookie' => CURLOPT_COOKIE, + 'referer' => CURLOPT_REFERER, + 'user-agent' => CURLOPT_USERAGENT + ); + + /** + * Mapping of SSL context options to cURL options + * @var array + */ + protected static $sslContextMap = array( + 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER, + 'ssl_cafile' => CURLOPT_CAINFO, + 'ssl_capath' => CURLOPT_CAPATH, + 'ssl_local_cert' => CURLOPT_SSLCERT, + 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD + ); + + /** + * Response being received + * @var HTTP_Request2_Response + */ + protected $response; + + /** + * Whether 'sentHeaders' event was sent to observers + * @var boolean + */ + protected $eventSentHeaders = false; + + /** + * Whether 'receivedHeaders' event was sent to observers + * @var boolean + */ + protected $eventReceivedHeaders = false; + + /** + * Position within request body + * @var integer + * @see callbackReadBody() + */ + protected $position = 0; + + /** + * Information about last transfer, as returned by curl_getinfo() + * @var array + */ + protected $lastInfo; + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + if (!extension_loaded('curl')) { + throw new HTTP_Request2_Exception('cURL extension not available'); + } + + $this->request = $request; + $this->response = null; + $this->position = 0; + $this->eventSentHeaders = false; + $this->eventReceivedHeaders = false; + + try { + if (false === curl_exec($ch = $this->createCurlHandle())) { + $errorMessage = 'Error sending request: #' . curl_errno($ch) . + ' ' . curl_error($ch); + } + } catch (Exception $e) { + } + $this->lastInfo = curl_getinfo($ch); + curl_close($ch); + + $response = $this->response; + unset($this->request, $this->requestBody, $this->response); + + if (!empty($e)) { + throw $e; + } elseif (!empty($errorMessage)) { + throw new HTTP_Request2_Exception($errorMessage); + } + + if (0 < $this->lastInfo['size_download']) { + $request->setLastEvent('receivedBody', $response); + } + return $response; + } + + /** + * Returns information about last transfer + * + * @return array associative array as returned by curl_getinfo() + */ + public function getInfo() + { + return $this->lastInfo; + } + + /** + * Creates a new cURL handle and populates it with data from the request + * + * @return resource a cURL handle, as created by curl_init() + * @throws HTTP_Request2_Exception + */ + protected function createCurlHandle() + { + $ch = curl_init(); + + curl_setopt_array($ch, array( + // setup write callbacks + CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'), + CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'), + // buffer size + CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'), + // connection timeout + CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'), + // save full outgoing headers, in case someone is interested + CURLINFO_HEADER_OUT => true, + // request url + CURLOPT_URL => $this->request->getUrl()->getUrl() + )); + + // set up redirects + if (!$this->request->getConfig('follow_redirects')) { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + } else { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects')); + // limit redirects to http(s), works in 5.2.10+ + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + // works sometime after 5.3.0, http://bugs.php.net/bug.php?id=49571 + if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR ')) { + curl_setopt($ch, CURLOPT_POSTREDIR, 3); + } + } + + // request timeout + if ($timeout = $this->request->getConfig('timeout')) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + + // set HTTP version + switch ($this->request->getConfig('protocol_version')) { + case '1.0': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + break; + case '1.1': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + } + + // set request method + switch ($this->request->getMethod()) { + case HTTP_Request2::METHOD_GET: + curl_setopt($ch, CURLOPT_HTTPGET, true); + break; + case HTTP_Request2::METHOD_POST: + curl_setopt($ch, CURLOPT_POST, true); + break; + case HTTP_Request2::METHOD_HEAD: + curl_setopt($ch, CURLOPT_NOBODY, true); + break; + default: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod()); + } + + // set proxy, if needed + if ($host = $this->request->getConfig('proxy_host')) { + if (!($port = $this->request->getConfig('proxy_port'))) { + throw new HTTP_Request2_Exception('Proxy port not provided'); + } + curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port); + if ($user = $this->request->getConfig('proxy_user')) { + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' . + $this->request->getConfig('proxy_password')); + switch ($this->request->getConfig('proxy_auth_scheme')) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST); + } + } + } + + // set authentication data + if ($auth = $this->request->getAuth()) { + curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']); + switch ($auth['scheme']) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + } + } + + // set SSL options + if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) { + foreach ($this->request->getConfig() as $name => $value) { + if ('ssl_verify_host' == $name && null !== $value) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0); + } elseif (isset(self::$sslContextMap[$name]) && null !== $value) { + curl_setopt($ch, self::$sslContextMap[$name], $value); + } + } + } + + $headers = $this->request->getHeaders(); + // make cURL automagically send proper header + if (!isset($headers['accept-encoding'])) { + $headers['accept-encoding'] = ''; + } + + // set headers having special cURL keys + foreach (self::$headerMap as $name => $option) { + if (isset($headers[$name])) { + curl_setopt($ch, $option, $headers[$name]); + unset($headers[$name]); + } + } + + $this->calculateRequestLength($headers); + if (isset($headers['content-length'])) { + $this->workaroundPhpBug47204($ch, $headers); + } + + // set headers not having special keys + $headersFmt = array(); + foreach ($headers as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $headersFmt[] = $canonicalName . ': ' . $value; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt); + + return $ch; + } + + /** + * Workaround for PHP bug #47204 that prevents rewinding request body + * + * The workaround consists of reading the entire request body into memory + * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large + * file uploads, use Socket adapter instead. + * + * @param resource cURL handle + * @param array Request headers + */ + protected function workaroundPhpBug47204($ch, &$headers) + { + // no redirects, no digest auth -> probably no rewind needed + if (!$this->request->getConfig('follow_redirects') + && (!($auth = $this->request->getAuth()) + || HTTP_Request2::AUTH_DIGEST != $auth['scheme']) + ) { + curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody')); + + // rewind may be needed, read the whole body into memory + } else { + if ($this->requestBody instanceof HTTP_Request2_MultipartBody) { + $this->requestBody = $this->requestBody->__toString(); + + } elseif (is_resource($this->requestBody)) { + $fp = $this->requestBody; + $this->requestBody = ''; + while (!feof($fp)) { + $this->requestBody .= fread($fp, 16384); + } + } + // curl hangs up if content-length is present + unset($headers['content-length']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody); + } + } + + /** + * Callback function called by cURL for reading the request body + * + * @param resource cURL handle + * @param resource file descriptor (not used) + * @param integer maximum length of data to return + * @return string part of the request body, up to $length bytes + */ + protected function callbackReadBody($ch, $fd, $length) + { + if (!$this->eventSentHeaders) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + $this->eventSentHeaders = true; + } + if (in_array($this->request->getMethod(), self::$bodyDisallowed) || + 0 == $this->contentLength || $this->position >= $this->contentLength + ) { + return ''; + } + if (is_string($this->requestBody)) { + $string = substr($this->requestBody, $this->position, $length); + } elseif (is_resource($this->requestBody)) { + $string = fread($this->requestBody, $length); + } else { + $string = $this->requestBody->read($length); + } + $this->request->setLastEvent('sentBodyPart', strlen($string)); + $this->position += strlen($string); + return $string; + } + + /** + * Callback function called by cURL for saving the response headers + * + * @param resource cURL handle + * @param string response header (with trailing CRLF) + * @return integer number of bytes saved + * @see HTTP_Request2_Response::parseHeaderLine() + */ + protected function callbackWriteHeader($ch, $string) + { + // we may receive a second set of headers if doing e.g. digest auth + if ($this->eventReceivedHeaders || !$this->eventSentHeaders) { + // don't bother with 100-Continue responses (bug #15785) + if (!$this->eventSentHeaders || + $this->response->getStatus() >= 200 + ) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + } + $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); + // if body wasn't read by a callback, send event with total body size + if ($upload > $this->position) { + $this->request->setLastEvent( + 'sentBodyPart', $upload - $this->position + ); + $this->position = $upload; + } + $this->eventSentHeaders = true; + // we'll need a new response object + if ($this->eventReceivedHeaders) { + $this->eventReceivedHeaders = false; + $this->response = null; + } + } + if (empty($this->response)) { + $this->response = new HTTP_Request2_Response($string, false); + } else { + $this->response->parseHeaderLine($string); + if ('' == trim($string)) { + // don't bother with 100-Continue responses (bug #15785) + if (200 <= $this->response->getStatus()) { + $this->request->setLastEvent('receivedHeaders', $this->response); + } + // for versions lower than 5.2.10, check the redirection URL protocol + if ($this->request->getConfig('follow_redirects') && !defined('CURLOPT_REDIR_PROTOCOLS') + && $this->response->isRedirect() + ) { + $redirectUrl = new Net_URL2($this->response->getHeader('location')); + if ($redirectUrl->isAbsolute() + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + ) { + return -1; + } + } + $this->eventReceivedHeaders = true; + } + } + return strlen($string); + } + + /** + * Callback function called by cURL for saving the response body + * + * @param resource cURL handle (not used) + * @param string part of the response body + * @return integer number of bytes saved + * @see HTTP_Request2_Response::appendBody() + */ + protected function callbackWriteBody($ch, $string) + { + // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if + // response doesn't start with proper HTTP status line (see bug #15716) + if (empty($this->response)) { + throw new HTTP_Request2_Exception("Malformed response: {$string}"); + } + if ($this->request->getConfig('store_body')) { + $this->response->appendBody($string); + } + $this->request->setLastEvent('receivedBodyPart', $string); + return strlen($string); + } +} +?> diff --git a/lib/php/HTTP/Request2/Adapter/Mock.php b/lib/php/HTTP/Request2/Adapter/Mock.php new file mode 100644 index 0000000000000000000000000000000000000000..2812ce93ea84b972a7a7230cf25cd3d97aef97a2 --- /dev/null +++ b/lib/php/HTTP/Request2/Adapter/Mock.php @@ -0,0 +1,171 @@ +<?php +/** + * Mock adapter intended for testing + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Mock.php 290192 2009-11-03 21:29:32Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Mock adapter intended for testing + * + * Can be used to test applications depending on HTTP_Request2 package without + * actually performing any HTTP requests. This adapter will return responses + * previously added via addResponse() + * <code> + * $mock = new HTTP_Request2_Adapter_Mock(); + * $mock->addResponse("HTTP/1.1 ... "); + * + * $request = new HTTP_Request2(); + * $request->setAdapter($mock); + * + * // This will return the response set above + * $response = $req->send(); + * </code> + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + */ +class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter +{ + /** + * A queue of responses to be returned by sendRequest() + * @var array + */ + protected $responses = array(); + + /** + * Returns the next response from the queue built by addResponse() + * + * If the queue is empty it will return default empty response with status 400, + * if an Exception object was added to the queue it will be thrown. + * + * @param HTTP_Request2 + * @return HTTP_Request2_Response + * @throws Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + if (count($this->responses) > 0) { + $response = array_shift($this->responses); + if ($response instanceof HTTP_Request2_Response) { + return $response; + } else { + // rethrow the exception + $class = get_class($response); + $message = $response->getMessage(); + $code = $response->getCode(); + throw new $class($message, $code); + } + } else { + return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n"); + } + } + + /** + * Adds response to the queue + * + * @param mixed either a string, a pointer to an open file, + * an instance of HTTP_Request2_Response or Exception + * @throws HTTP_Request2_Exception + */ + public function addResponse($response) + { + if (is_string($response)) { + $response = self::createResponseFromString($response); + } elseif (is_resource($response)) { + $response = self::createResponseFromFile($response); + } elseif (!$response instanceof HTTP_Request2_Response && + !$response instanceof Exception + ) { + throw new HTTP_Request2_Exception('Parameter is not a valid response'); + } + $this->responses[] = $response; + } + + /** + * Creates a new HTTP_Request2_Response object from a string + * + * @param string + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromString($str) + { + $parts = preg_split('!(\r?\n){2}!m', $str, 2); + $headerLines = explode("\n", $parts[0]); + $response = new HTTP_Request2_Response(array_shift($headerLines)); + foreach ($headerLines as $headerLine) { + $response->parseHeaderLine($headerLine); + } + $response->parseHeaderLine(''); + if (isset($parts[1])) { + $response->appendBody($parts[1]); + } + return $response; + } + + /** + * Creates a new HTTP_Request2_Response object from a file + * + * @param resource file pointer returned by fopen() + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromFile($fp) + { + $response = new HTTP_Request2_Response(fgets($fp)); + do { + $headerLine = fgets($fp); + $response->parseHeaderLine($headerLine); + } while ('' != trim($headerLine)); + + while (!feof($fp)) { + $response->appendBody(fread($fp, 8192)); + } + return $response; + } +} +?> \ No newline at end of file diff --git a/lib/php/HTTP/Request2/Adapter/Socket.php b/lib/php/HTTP/Request2/Adapter/Socket.php new file mode 100644 index 0000000000000000000000000000000000000000..6d1788c7d4f7a46465221b67a3c35094a29de30f --- /dev/null +++ b/lib/php/HTTP/Request2/Adapter/Socket.php @@ -0,0 +1,1046 @@ +<?php +/** + * Socket-based adapter for HTTP_Request2 + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Socket.php 290921 2009-11-18 17:31:58Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Socket-based adapter for HTTP_Request2 + * + * This adapter uses only PHP sockets and will work on almost any PHP + * environment. Code is based on original HTTP_Request PEAR package. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + */ +class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter +{ + /** + * Regular expression for 'token' rule from RFC 2616 + */ + const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+'; + + /** + * Regular expression for 'quoted-string' rule from RFC 2616 + */ + const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"'; + + /** + * Connected sockets, needed for Keep-Alive support + * @var array + * @see connect() + */ + protected static $sockets = array(); + + /** + * Data for digest authentication scheme + * + * The keys for the array are URL prefixes. + * + * The values are associative arrays with data (realm, nonce, nonce-count, + * opaque...) needed for digest authentication. Stored here to prevent making + * duplicate requests to digest-protected resources after we have already + * received the challenge. + * + * @var array + */ + protected static $challenges = array(); + + /** + * Connected socket + * @var resource + * @see connect() + */ + protected $socket; + + /** + * Challenge used for server digest authentication + * @var array + */ + protected $serverChallenge; + + /** + * Challenge used for proxy digest authentication + * @var array + */ + protected $proxyChallenge; + + /** + * Sum of start time and global timeout, exception will be thrown if request continues past this time + * @var integer + */ + protected $deadline = null; + + /** + * Remaining length of the current chunk, when reading chunked response + * @var integer + * @see readChunked() + */ + protected $chunkLength = 0; + + /** + * Remaining amount of redirections to follow + * + * Starts at 'max_redirects' configuration parameter and is reduced on each + * subsequent redirect. An Exception will be thrown once it reaches zero. + * + * @var integer + */ + protected $redirectCountdown = null; + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + $this->request = $request; + + // Use global request timeout if given, see feature requests #5735, #8964 + if ($timeout = $request->getConfig('timeout')) { + $this->deadline = time() + $timeout; + } else { + $this->deadline = null; + } + + try { + $keepAlive = $this->connect(); + $headers = $this->prepareHeaders(); + if (false === @fwrite($this->socket, $headers, strlen($headers))) { + throw new HTTP_Request2_Exception('Error writing request'); + } + // provide request headers to the observer, see request #7633 + $this->request->setLastEvent('sentHeaders', $headers); + $this->writeBody(); + + if ($this->deadline && time() > $this->deadline) { + throw new HTTP_Request2_Exception( + 'Request timed out after ' . + $request->getConfig('timeout') . ' second(s)' + ); + } + + $response = $this->readResponse(); + + if (!$this->canKeepAlive($keepAlive, $response)) { + $this->disconnect(); + } + + if ($this->shouldUseProxyDigestAuth($response)) { + return $this->sendRequest($request); + } + if ($this->shouldUseServerDigestAuth($response)) { + return $this->sendRequest($request); + } + if ($authInfo = $response->getHeader('authentication-info')) { + $this->updateChallenge($this->serverChallenge, $authInfo); + } + if ($proxyInfo = $response->getHeader('proxy-authentication-info')) { + $this->updateChallenge($this->proxyChallenge, $proxyInfo); + } + + } catch (Exception $e) { + $this->disconnect(); + } + + unset($this->request, $this->requestBody); + + if (!empty($e)) { + throw $e; + } + + if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) { + return $response; + } else { + return $this->handleRedirect($request, $response); + } + } + + /** + * Connects to the remote server + * + * @return bool whether the connection can be persistent + * @throws HTTP_Request2_Exception + */ + protected function connect() + { + $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https'); + $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + $headers = $this->request->getHeaders(); + $reqHost = $this->request->getUrl()->getHost(); + if (!($reqPort = $this->request->getUrl()->getPort())) { + $reqPort = $secure? 443: 80; + } + + if ($host = $this->request->getConfig('proxy_host')) { + if (!($port = $this->request->getConfig('proxy_port'))) { + throw new HTTP_Request2_Exception('Proxy port not provided'); + } + $proxy = true; + } else { + $host = $reqHost; + $port = $reqPort; + $proxy = false; + } + + if ($tunnel && !$proxy) { + throw new HTTP_Request2_Exception( + "Trying to perform CONNECT request without proxy" + ); + } + if ($secure && !in_array('ssl', stream_get_transports())) { + throw new HTTP_Request2_Exception( + 'Need OpenSSL support for https:// requests' + ); + } + + // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive + // connection token to a proxy server... + if ($proxy && !$secure && + !empty($headers['connection']) && 'Keep-Alive' == $headers['connection'] + ) { + $this->request->setHeader('connection'); + } + + $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && + empty($headers['connection'])) || + (!empty($headers['connection']) && + 'Keep-Alive' == $headers['connection']); + $host = ((!$secure || $proxy)? 'tcp://': 'ssl://') . $host; + + $options = array(); + if ($secure || $tunnel) { + foreach ($this->request->getConfig() as $name => $value) { + if ('ssl_' == substr($name, 0, 4) && null !== $value) { + if ('ssl_verify_host' == $name) { + if ($value) { + $options['CN_match'] = $reqHost; + } + } else { + $options[substr($name, 4)] = $value; + } + } + } + ksort($options); + } + + // Changing SSL context options after connection is established does *not* + // work, we need a new connection if options change + $remote = $host . ':' . $port; + $socketKey = $remote . (($secure && $proxy)? "->{$reqHost}:{$reqPort}": '') . + (empty($options)? '': ':' . serialize($options)); + unset($this->socket); + + // We use persistent connections and have a connected socket? + // Ensure that the socket is still connected, see bug #16149 + if ($keepAlive && !empty(self::$sockets[$socketKey]) && + !feof(self::$sockets[$socketKey]) + ) { + $this->socket =& self::$sockets[$socketKey]; + + } elseif ($secure && $proxy && !$tunnel) { + $this->establishTunnel(); + $this->request->setLastEvent( + 'connect', "ssl://{$reqHost}:{$reqPort} via {$host}:{$port}" + ); + self::$sockets[$socketKey] =& $this->socket; + + } else { + // Set SSL context options if doing HTTPS request or creating a tunnel + $context = stream_context_create(); + foreach ($options as $name => $value) { + if (!stream_context_set_option($context, 'ssl', $name, $value)) { + throw new HTTP_Request2_Exception( + "Error setting SSL context option '{$name}'" + ); + } + } + $this->socket = @stream_socket_client( + $remote, $errno, $errstr, + $this->request->getConfig('connect_timeout'), + STREAM_CLIENT_CONNECT, $context + ); + if (!$this->socket) { + throw new HTTP_Request2_Exception( + "Unable to connect to {$remote}. Error #{$errno}: {$errstr}" + ); + } + $this->request->setLastEvent('connect', $remote); + self::$sockets[$socketKey] =& $this->socket; + } + return $keepAlive; + } + + /** + * Establishes a tunnel to a secure remote server via HTTP CONNECT request + * + * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP + * sees that we are connected to a proxy server (duh!) rather than the server + * that presents its certificate. + * + * @link http://tools.ietf.org/html/rfc2817#section-5.2 + * @throws HTTP_Request2_Exception + */ + protected function establishTunnel() + { + $donor = new self; + $connect = new HTTP_Request2( + $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT, + array_merge($this->request->getConfig(), + array('adapter' => $donor)) + ); + $response = $connect->send(); + // Need any successful (2XX) response + if (200 > $response->getStatus() || 300 <= $response->getStatus()) { + throw new HTTP_Request2_Exception( + 'Failed to connect via HTTPS proxy. Proxy response: ' . + $response->getStatus() . ' ' . $response->getReasonPhrase() + ); + } + $this->socket = $donor->socket; + + $modes = array( + STREAM_CRYPTO_METHOD_TLS_CLIENT, + STREAM_CRYPTO_METHOD_SSLv3_CLIENT, + STREAM_CRYPTO_METHOD_SSLv23_CLIENT, + STREAM_CRYPTO_METHOD_SSLv2_CLIENT + ); + + foreach ($modes as $mode) { + if (stream_socket_enable_crypto($this->socket, true, $mode)) { + return; + } + } + throw new HTTP_Request2_Exception( + 'Failed to enable secure connection when connecting through proxy' + ); + } + + /** + * Checks whether current connection may be reused or should be closed + * + * @param boolean whether connection could be persistent + * in the first place + * @param HTTP_Request2_Response response object to check + * @return boolean + */ + protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response) + { + // Do not close socket on successful CONNECT request + if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() && + 200 <= $response->getStatus() && 300 > $response->getStatus() + ) { + return true; + } + + $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) || + null !== $response->getHeader('content-length'); + $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) || + (null === $response->getHeader('connection') && + '1.1' == $response->getVersion()); + return $requestKeepAlive && $lengthKnown && $persistent; + } + + /** + * Disconnects from the remote server + */ + protected function disconnect() + { + if (is_resource($this->socket)) { + fclose($this->socket); + $this->socket = null; + $this->request->setLastEvent('disconnect'); + } + } + + /** + * Handles HTTP redirection + * + * This method will throw an Exception if redirect to a non-HTTP(S) location + * is attempted, also if number of redirects performed already is equal to + * 'max_redirects' configuration parameter. + * + * @param HTTP_Request2 Original request + * @param HTTP_Request2_Response Response containing redirect + * @return HTTP_Request2_Response Response from a new location + * @throws HTTP_Request2_Exception + */ + protected function handleRedirect(HTTP_Request2 $request, + HTTP_Request2_Response $response) + { + if (is_null($this->redirectCountdown)) { + $this->redirectCountdown = $request->getConfig('max_redirects'); + } + if (0 == $this->redirectCountdown) { + // Copying cURL behaviour + throw new HTTP_Request2_Exception( + 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed' + ); + } + $redirectUrl = new Net_URL2( + $response->getHeader('location'), + array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets')) + ); + // refuse non-HTTP redirect + if ($redirectUrl->isAbsolute() + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + ) { + throw new HTTP_Request2_Exception( + 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString() + ); + } + // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30), + // but in practice it is often not + if (!$redirectUrl->isAbsolute()) { + $redirectUrl = $request->getUrl()->resolve($redirectUrl); + } + $redirect = clone $request; + $redirect->setUrl($redirectUrl); + if (303 == $response->getStatus() || (!$request->getConfig('strict_redirects') + && in_array($response->getStatus(), array(301, 302))) + ) { + $redirect->setMethod(HTTP_Request2::METHOD_GET); + $redirect->setBody(''); + } + + if (0 < $this->redirectCountdown) { + $this->redirectCountdown--; + } + return $this->sendRequest($redirect); + } + + /** + * Checks whether another request should be performed with server digest auth + * + * Several conditions should be satisfied for it to return true: + * - response status should be 401 + * - auth credentials should be set in the request object + * - response should contain WWW-Authenticate header with digest challenge + * - there is either no challenge stored for this URL or new challenge + * contains stale=true parameter (in other case we probably just failed + * due to invalid username / password) + * + * The method stores challenge values in $challenges static property + * + * @param HTTP_Request2_Response response to check + * @return boolean whether another request should be performed + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + */ + protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response) + { + // no sense repeating a request if we don't have credentials + if (401 != $response->getStatus() || !$this->request->getAuth()) { + return false; + } + if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) { + return false; + } + + $url = $this->request->getUrl(); + $scheme = $url->getScheme(); + $host = $scheme . '://' . $url->getHost(); + if ($port = $url->getPort()) { + if ((0 == strcasecmp($scheme, 'http') && 80 != $port) || + (0 == strcasecmp($scheme, 'https') && 443 != $port) + ) { + $host .= ':' . $port; + } + } + + if (!empty($challenge['domain'])) { + $prefixes = array(); + foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) { + // don't bother with different servers + if ('/' == substr($prefix, 0, 1)) { + $prefixes[] = $host . $prefix; + } + } + } + if (empty($prefixes)) { + $prefixes = array($host . '/'); + } + + $ret = true; + foreach ($prefixes as $prefix) { + if (!empty(self::$challenges[$prefix]) && + (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + ) { + // probably credentials are invalid + $ret = false; + } + self::$challenges[$prefix] =& $challenge; + } + return $ret; + } + + /** + * Checks whether another request should be performed with proxy digest auth + * + * Several conditions should be satisfied for it to return true: + * - response status should be 407 + * - proxy auth credentials should be set in the request object + * - response should contain Proxy-Authenticate header with digest challenge + * - there is either no challenge stored for this proxy or new challenge + * contains stale=true parameter (in other case we probably just failed + * due to invalid username / password) + * + * The method stores challenge values in $challenges static property + * + * @param HTTP_Request2_Response response to check + * @return boolean whether another request should be performed + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + */ + protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response) + { + if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) { + return false; + } + if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) { + return false; + } + + $key = 'proxy://' . $this->request->getConfig('proxy_host') . + ':' . $this->request->getConfig('proxy_port'); + + if (!empty(self::$challenges[$key]) && + (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + ) { + $ret = false; + } else { + $ret = true; + } + self::$challenges[$key] = $challenge; + return $ret; + } + + /** + * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value + * + * There is a problem with implementation of RFC 2617: several of the parameters + * are defined as quoted-string there and thus may contain backslash escaped + * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as + * just value of quoted-string X without surrounding quotes, it doesn't speak + * about removing backslash escaping. + * + * Now realm parameter is user-defined and human-readable, strange things + * happen when it contains quotes: + * - Apache allows quotes in realm, but apparently uses realm value without + * backslashes for digest computation + * - Squid allows (manually escaped) quotes there, but it is impossible to + * authorize with either escaped or unescaped quotes used in digest, + * probably it can't parse the response (?) + * - Both IE and Firefox display realm value with backslashes in + * the password popup and apparently use the same value for digest + * + * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in + * quoted-string handling, unfortunately that means failure to authorize + * sometimes + * + * @param string value of WWW-Authenticate or Proxy-Authenticate header + * @return mixed associative array with challenge parameters, false if + * no challenge is present in header value + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + */ + protected function parseDigestChallenge($headerValue) + { + $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')'; + $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!"; + if (!preg_match($challenge, $headerValue, $matches)) { + return false; + } + + preg_match_all('!' . $authParam . '!', $matches[0], $params); + $paramsAry = array(); + $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale', + 'algorithm', 'qop'); + for ($i = 0; $i < count($params[0]); $i++) { + // section 3.2.1: Any unrecognized directive MUST be ignored. + if (in_array($params[1][$i], $knownParams)) { + if ('"' == substr($params[2][$i], 0, 1)) { + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + } else { + $paramsAry[$params[1][$i]] = $params[2][$i]; + } + } + } + // we only support qop=auth + if (!empty($paramsAry['qop']) && + !in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))) + ) { + throw new HTTP_Request2_Exception( + "Only 'auth' qop is currently supported in digest authentication, " . + "server requested '{$paramsAry['qop']}'" + ); + } + // we only support algorithm=MD5 + if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) { + throw new HTTP_Request2_Exception( + "Only 'MD5' algorithm is currently supported in digest authentication, " . + "server requested '{$paramsAry['algorithm']}'" + ); + } + + return $paramsAry; + } + + /** + * Parses [Proxy-]Authentication-Info header value and updates challenge + * + * @param array challenge to update + * @param string value of [Proxy-]Authentication-Info header + * @todo validate server rspauth response + */ + protected function updateChallenge(&$challenge, $headerValue) + { + $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!'; + $paramsAry = array(); + + preg_match_all($authParam, $headerValue, $params); + for ($i = 0; $i < count($params[0]); $i++) { + if ('"' == substr($params[2][$i], 0, 1)) { + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + } else { + $paramsAry[$params[1][$i]] = $params[2][$i]; + } + } + // for now, just update the nonce value + if (!empty($paramsAry['nextnonce'])) { + $challenge['nonce'] = $paramsAry['nextnonce']; + $challenge['nc'] = 1; + } + } + + /** + * Creates a value for [Proxy-]Authorization header when using digest authentication + * + * @param string user name + * @param string password + * @param string request URL + * @param array digest challenge parameters + * @return string value of [Proxy-]Authorization request header + * @link http://tools.ietf.org/html/rfc2617#section-3.2.2 + */ + protected function createDigestResponse($user, $password, $url, &$challenge) + { + if (false !== ($q = strpos($url, '?')) && + $this->request->getConfig('digest_compat_ie') + ) { + $url = substr($url, 0, $q); + } + + $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password); + $a2 = md5($this->request->getMethod() . ':' . $url); + + if (empty($challenge['qop'])) { + $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2); + } else { + $challenge['cnonce'] = 'Req2.' . rand(); + if (empty($challenge['nc'])) { + $challenge['nc'] = 1; + } + $nc = sprintf('%08x', $challenge['nc']++); + $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . + $challenge['cnonce'] . ':auth:' . $a2); + } + return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' . + 'realm="' . $challenge['realm'] . '", ' . + 'nonce="' . $challenge['nonce'] . '", ' . + 'uri="' . $url . '", ' . + 'response="' . $digest . '"' . + (!empty($challenge['opaque'])? + ', opaque="' . $challenge['opaque'] . '"': + '') . + (!empty($challenge['qop'])? + ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"': + ''); + } + + /** + * Adds 'Authorization' header (if needed) to request headers array + * + * @param array request headers + * @param string request host (needed for digest authentication) + * @param string request URL (needed for digest authentication) + * @throws HTTP_Request2_Exception + */ + protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl) + { + if (!($auth = $this->request->getAuth())) { + return; + } + switch ($auth['scheme']) { + case HTTP_Request2::AUTH_BASIC: + $headers['authorization'] = + 'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']); + break; + + case HTTP_Request2::AUTH_DIGEST: + unset($this->serverChallenge); + $fullUrl = ('/' == $requestUrl[0])? + $this->request->getUrl()->getScheme() . '://' . + $requestHost . $requestUrl: + $requestUrl; + foreach (array_keys(self::$challenges) as $key) { + if ($key == substr($fullUrl, 0, strlen($key))) { + $headers['authorization'] = $this->createDigestResponse( + $auth['user'], $auth['password'], + $requestUrl, self::$challenges[$key] + ); + $this->serverChallenge =& self::$challenges[$key]; + break; + } + } + break; + + default: + throw new HTTP_Request2_Exception( + "Unknown HTTP authentication scheme '{$auth['scheme']}'" + ); + } + } + + /** + * Adds 'Proxy-Authorization' header (if needed) to request headers array + * + * @param array request headers + * @param string request URL (needed for digest authentication) + * @throws HTTP_Request2_Exception + */ + protected function addProxyAuthorizationHeader(&$headers, $requestUrl) + { + if (!$this->request->getConfig('proxy_host') || + !($user = $this->request->getConfig('proxy_user')) || + (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) && + HTTP_Request2::METHOD_CONNECT != $this->request->getMethod()) + ) { + return; + } + + $password = $this->request->getConfig('proxy_password'); + switch ($this->request->getConfig('proxy_auth_scheme')) { + case HTTP_Request2::AUTH_BASIC: + $headers['proxy-authorization'] = + 'Basic ' . base64_encode($user . ':' . $password); + break; + + case HTTP_Request2::AUTH_DIGEST: + unset($this->proxyChallenge); + $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') . + ':' . $this->request->getConfig('proxy_port'); + if (!empty(self::$challenges[$proxyUrl])) { + $headers['proxy-authorization'] = $this->createDigestResponse( + $user, $password, + $requestUrl, self::$challenges[$proxyUrl] + ); + $this->proxyChallenge =& self::$challenges[$proxyUrl]; + } + break; + + default: + throw new HTTP_Request2_Exception( + "Unknown HTTP authentication scheme '" . + $this->request->getConfig('proxy_auth_scheme') . "'" + ); + } + } + + + /** + * Creates the string with the Request-Line and request headers + * + * @return string + * @throws HTTP_Request2_Exception + */ + protected function prepareHeaders() + { + $headers = $this->request->getHeaders(); + $url = $this->request->getUrl(); + $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + $host = $url->getHost(); + + $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80; + if (($port = $url->getPort()) && $port != $defaultPort || $connect) { + $host .= ':' . (empty($port)? $defaultPort: $port); + } + // Do not overwrite explicitly set 'Host' header, see bug #16146 + if (!isset($headers['host'])) { + $headers['host'] = $host; + } + + if ($connect) { + $requestUrl = $host; + + } else { + if (!$this->request->getConfig('proxy_host') || + 0 == strcasecmp($url->getScheme(), 'https') + ) { + $requestUrl = ''; + } else { + $requestUrl = $url->getScheme() . '://' . $host; + } + $path = $url->getPath(); + $query = $url->getQuery(); + $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query); + } + + if ('1.1' == $this->request->getConfig('protocol_version') && + extension_loaded('zlib') && !isset($headers['accept-encoding']) + ) { + $headers['accept-encoding'] = 'gzip, deflate'; + } + + $this->addAuthorizationHeader($headers, $host, $requestUrl); + $this->addProxyAuthorizationHeader($headers, $requestUrl); + $this->calculateRequestLength($headers); + + $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' . + $this->request->getConfig('protocol_version') . "\r\n"; + foreach ($headers as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $headersStr .= $canonicalName . ': ' . $value . "\r\n"; + } + return $headersStr . "\r\n"; + } + + /** + * Sends the request body + * + * @throws HTTP_Request2_Exception + */ + protected function writeBody() + { + if (in_array($this->request->getMethod(), self::$bodyDisallowed) || + 0 == $this->contentLength + ) { + return; + } + + $position = 0; + $bufferSize = $this->request->getConfig('buffer_size'); + while ($position < $this->contentLength) { + if (is_string($this->requestBody)) { + $str = substr($this->requestBody, $position, $bufferSize); + } elseif (is_resource($this->requestBody)) { + $str = fread($this->requestBody, $bufferSize); + } else { + $str = $this->requestBody->read($bufferSize); + } + if (false === @fwrite($this->socket, $str, strlen($str))) { + throw new HTTP_Request2_Exception('Error writing request'); + } + // Provide the length of written string to the observer, request #7630 + $this->request->setLastEvent('sentBodyPart', strlen($str)); + $position += strlen($str); + } + } + + /** + * Reads the remote server's response + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + protected function readResponse() + { + $bufferSize = $this->request->getConfig('buffer_size'); + + do { + $response = new HTTP_Request2_Response($this->readLine($bufferSize), true); + do { + $headerLine = $this->readLine($bufferSize); + $response->parseHeaderLine($headerLine); + } while ('' != $headerLine); + } while (in_array($response->getStatus(), array(100, 101))); + + $this->request->setLastEvent('receivedHeaders', $response); + + // No body possible in such responses + if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() || + (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() && + 200 <= $response->getStatus() && 300 > $response->getStatus()) || + in_array($response->getStatus(), array(204, 304)) + ) { + return $response; + } + + $chunked = 'chunked' == $response->getHeader('transfer-encoding'); + $length = $response->getHeader('content-length'); + $hasBody = false; + if ($chunked || null === $length || 0 < intval($length)) { + // RFC 2616, section 4.4: + // 3. ... If a message is received with both a + // Transfer-Encoding header field and a Content-Length header field, + // the latter MUST be ignored. + $toRead = ($chunked || null === $length)? null: $length; + $this->chunkLength = 0; + + while (!feof($this->socket) && (is_null($toRead) || 0 < $toRead)) { + if ($chunked) { + $data = $this->readChunked($bufferSize); + } elseif (is_null($toRead)) { + $data = $this->fread($bufferSize); + } else { + $data = $this->fread(min($toRead, $bufferSize)); + $toRead -= strlen($data); + } + if ('' == $data && (!$this->chunkLength || feof($this->socket))) { + break; + } + + $hasBody = true; + if ($this->request->getConfig('store_body')) { + $response->appendBody($data); + } + if (!in_array($response->getHeader('content-encoding'), array('identity', null))) { + $this->request->setLastEvent('receivedEncodedBodyPart', $data); + } else { + $this->request->setLastEvent('receivedBodyPart', $data); + } + } + } + + if ($hasBody) { + $this->request->setLastEvent('receivedBody', $response); + } + return $response; + } + + /** + * Reads until either the end of the socket or a newline, whichever comes first + * + * Strips the trailing newline from the returned data, handles global + * request timeout. Method idea borrowed from Net_Socket PEAR package. + * + * @param int buffer size to use for reading + * @return Available data up to the newline (not including newline) + * @throws HTTP_Request2_Exception In case of timeout + */ + protected function readLine($bufferSize) + { + $line = ''; + while (!feof($this->socket)) { + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + } + $line .= @fgets($this->socket, $bufferSize); + $info = stream_get_meta_data($this->socket); + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + $reason = $this->deadline + ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' + : 'due to default_socket_timeout php.ini setting'; + throw new HTTP_Request2_Exception("Request timed out {$reason}"); + } + if (substr($line, -1) == "\n") { + return rtrim($line, "\r\n"); + } + } + return $line; + } + + /** + * Wrapper around fread(), handles global request timeout + * + * @param int Reads up to this number of bytes + * @return Data read from socket + * @throws HTTP_Request2_Exception In case of timeout + */ + protected function fread($length) + { + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + } + $data = fread($this->socket, $length); + $info = stream_get_meta_data($this->socket); + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + $reason = $this->deadline + ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' + : 'due to default_socket_timeout php.ini setting'; + throw new HTTP_Request2_Exception("Request timed out {$reason}"); + } + return $data; + } + + /** + * Reads a part of response body encoded with chunked Transfer-Encoding + * + * @param int buffer size to use for reading + * @return string + * @throws HTTP_Request2_Exception + */ + protected function readChunked($bufferSize) + { + // at start of the next chunk? + if (0 == $this->chunkLength) { + $line = $this->readLine($bufferSize); + if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + throw new HTTP_Request2_Exception( + "Cannot decode chunked response, invalid chunk length '{$line}'" + ); + } else { + $this->chunkLength = hexdec($matches[1]); + // Chunk with zero length indicates the end + if (0 == $this->chunkLength) { + $this->readLine($bufferSize); + return ''; + } + } + } + $data = $this->fread(min($this->chunkLength, $bufferSize)); + $this->chunkLength -= strlen($data); + if (0 == $this->chunkLength) { + $this->readLine($bufferSize); // Trailing CRLF + } + return $data; + } +} + +?> \ No newline at end of file diff --git a/lib/php/HTTP/Request2/Exception.php b/lib/php/HTTP/Request2/Exception.php new file mode 100644 index 0000000000000000000000000000000000000000..d86a14401f97693fc82f2ce3750288af6a43382b --- /dev/null +++ b/lib/php/HTTP/Request2/Exception.php @@ -0,0 +1,62 @@ +<?php +/** + * Exception class for HTTP_Request2 package + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Exception.php 290192 2009-11-03 21:29:32Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for exceptions in PEAR + */ +require_once 'PEAR/Exception.php'; + +/** + * Exception class for HTTP_Request2 package + * + * Such a class is required by the Exception RFC: + * http://pear.php.net/pepr/pepr-proposal-show.php?id=132 + * + * @category HTTP + * @package HTTP_Request2 + * @version Release: 0.5.1 + */ +class HTTP_Request2_Exception extends PEAR_Exception +{ +} +?> \ No newline at end of file diff --git a/lib/php/HTTP/Request2/MultipartBody.php b/lib/php/HTTP/Request2/MultipartBody.php new file mode 100644 index 0000000000000000000000000000000000000000..efc2e254ab889025729e5089bce0555b92d3bb39 --- /dev/null +++ b/lib/php/HTTP/Request2/MultipartBody.php @@ -0,0 +1,274 @@ +<?php +/** + * Helper class for building multipart/form-data request body + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: MultipartBody.php 290192 2009-11-03 21:29:32Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Class for building multipart/form-data request body + * + * The class helps to reduce memory consumption by streaming large file uploads + * from disk, it also allows monitoring of upload progress (see request #7630) + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + * @link http://tools.ietf.org/html/rfc1867 + */ +class HTTP_Request2_MultipartBody +{ + /** + * MIME boundary + * @var string + */ + private $_boundary; + + /** + * Form parameters added via {@link HTTP_Request2::addPostParameter()} + * @var array + */ + private $_params = array(); + + /** + * File uploads added via {@link HTTP_Request2::addUpload()} + * @var array + */ + private $_uploads = array(); + + /** + * Header for parts with parameters + * @var string + */ + private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n"; + + /** + * Header for parts with uploads + * @var string + */ + private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n"; + + /** + * Current position in parameter and upload arrays + * + * First number is index of "current" part, second number is position within + * "current" part + * + * @var array + */ + private $_pos = array(0, 0); + + + /** + * Constructor. Sets the arrays with POST data. + * + * @param array values of form fields set via {@link HTTP_Request2::addPostParameter()} + * @param array file uploads set via {@link HTTP_Request2::addUpload()} + * @param bool whether to append brackets to array variable names + */ + public function __construct(array $params, array $uploads, $useBrackets = true) + { + $this->_params = self::_flattenArray('', $params, $useBrackets); + foreach ($uploads as $fieldName => $f) { + if (!is_array($f['fp'])) { + $this->_uploads[] = $f + array('name' => $fieldName); + } else { + for ($i = 0; $i < count($f['fp']); $i++) { + $upload = array( + 'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName) + ); + foreach (array('fp', 'filename', 'size', 'type') as $key) { + $upload[$key] = $f[$key][$i]; + } + $this->_uploads[] = $upload; + } + } + } + } + + /** + * Returns the length of the body to use in Content-Length header + * + * @return integer + */ + public function getLength() + { + $boundaryLength = strlen($this->getBoundary()); + $headerParamLength = strlen($this->_headerParam) - 4 + $boundaryLength; + $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength; + $length = $boundaryLength + 6; + foreach ($this->_params as $p) { + $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2; + } + foreach ($this->_uploads as $u) { + $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) + + strlen($u['filename']) + $u['size'] + 2; + } + return $length; + } + + /** + * Returns the boundary to use in Content-Type header + * + * @return string + */ + public function getBoundary() + { + if (empty($this->_boundary)) { + $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime()); + } + return $this->_boundary; + } + + /** + * Returns next chunk of request body + * + * @param integer Amount of bytes to read + * @return string Up to $length bytes of data, empty string if at end + */ + public function read($length) + { + $ret = ''; + $boundary = $this->getBoundary(); + $paramCount = count($this->_params); + $uploadCount = count($this->_uploads); + while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) { + $oldLength = $length; + if ($this->_pos[0] < $paramCount) { + $param = sprintf($this->_headerParam, $boundary, + $this->_params[$this->_pos[0]][0]) . + $this->_params[$this->_pos[0]][1] . "\r\n"; + $ret .= substr($param, $this->_pos[1], $length); + $length -= min(strlen($param) - $this->_pos[1], $length); + + } elseif ($this->_pos[0] < $paramCount + $uploadCount) { + $pos = $this->_pos[0] - $paramCount; + $header = sprintf($this->_headerUpload, $boundary, + $this->_uploads[$pos]['name'], + $this->_uploads[$pos]['filename'], + $this->_uploads[$pos]['type']); + if ($this->_pos[1] < strlen($header)) { + $ret .= substr($header, $this->_pos[1], $length); + $length -= min(strlen($header) - $this->_pos[1], $length); + } + $filePos = max(0, $this->_pos[1] - strlen($header)); + if ($length > 0 && $filePos < $this->_uploads[$pos]['size']) { + $ret .= fread($this->_uploads[$pos]['fp'], $length); + $length -= min($length, $this->_uploads[$pos]['size'] - $filePos); + } + if ($length > 0) { + $start = $this->_pos[1] + ($oldLength - $length) - + strlen($header) - $this->_uploads[$pos]['size']; + $ret .= substr("\r\n", $start, $length); + $length -= min(2 - $start, $length); + } + + } else { + $closing = '--' . $boundary . "--\r\n"; + $ret .= substr($closing, $this->_pos[1], $length); + $length -= min(strlen($closing) - $this->_pos[1], $length); + } + if ($length > 0) { + $this->_pos = array($this->_pos[0] + 1, 0); + } else { + $this->_pos[1] += $oldLength; + } + } + return $ret; + } + + /** + * Sets the current position to the start of the body + * + * This allows reusing the same body in another request + */ + public function rewind() + { + $this->_pos = array(0, 0); + foreach ($this->_uploads as $u) { + rewind($u['fp']); + } + } + + /** + * Returns the body as string + * + * Note that it reads all file uploads into memory so it is a good idea not + * to use this method with large file uploads and rely on read() instead. + * + * @return string + */ + public function __toString() + { + $this->rewind(); + return $this->read($this->getLength()); + } + + + /** + * Helper function to change the (probably multidimensional) associative array + * into the simple one. + * + * @param string name for item + * @param mixed item's values + * @param bool whether to append [] to array variables' names + * @return array array with the following items: array('item name', 'item value'); + */ + private static function _flattenArray($name, $values, $useBrackets) + { + if (!is_array($values)) { + return array(array($name, $values)); + } else { + $ret = array(); + foreach ($values as $k => $v) { + if (empty($name)) { + $newName = $k; + } elseif ($useBrackets) { + $newName = $name . '[' . $k . ']'; + } else { + $newName = $name; + } + $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets)); + } + return $ret; + } + } +} +?> diff --git a/lib/php/HTTP/Request2/Observer/Log.php b/lib/php/HTTP/Request2/Observer/Log.php new file mode 100644 index 0000000000000000000000000000000000000000..e7a064b9603cb040a98342e56e6cd6efe56896c0 --- /dev/null +++ b/lib/php/HTTP/Request2/Observer/Log.php @@ -0,0 +1,215 @@ +<?php +/** + * An observer useful for debugging / testing. + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author David Jean Louis <izi@php.net> + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Log.php 290743 2009-11-14 13:27:49Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * A debug observer useful for debugging / testing. + * + * This observer logs to a log target data corresponding to the various request + * and response events, it logs by default to php://output but can be configured + * to log to a file or via the PEAR Log package. + * + * A simple example: + * <code> + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/Log.php'; + * + * $request = new HTTP_Request2('http://www.example.com'); + * $observer = new HTTP_Request2_Observer_Log(); + * $request->attach($observer); + * $request->send(); + * </code> + * + * A more complex example with PEAR Log: + * <code> + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/Log.php'; + * require_once 'Log.php'; + * + * $request = new HTTP_Request2('http://www.example.com'); + * // we want to log with PEAR log + * $observer = new HTTP_Request2_Observer_Log(Log::factory('console')); + * + * // we only want to log received headers + * $observer->events = array('receivedHeaders'); + * + * $request->attach($observer); + * $request->send(); + * </code> + * + * @category HTTP + * @package HTTP_Request2 + * @author David Jean Louis <izi@php.net> + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 0.5.1 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Observer_Log implements SplObserver +{ + // properties {{{ + + /** + * The log target, it can be a a resource or a PEAR Log instance. + * + * @var resource|Log $target + */ + protected $target = null; + + /** + * The events to log. + * + * @var array $events + */ + public $events = array( + 'connect', + 'sentHeaders', + 'sentBodyPart', + 'receivedHeaders', + 'receivedBody', + 'disconnect', + ); + + // }}} + // __construct() {{{ + + /** + * Constructor. + * + * @param mixed $target Can be a file path (default: php://output), a resource, + * or an instance of the PEAR Log class. + * @param array $events Array of events to listen to (default: all events) + * + * @return void + */ + public function __construct($target = 'php://output', array $events = array()) + { + if (!empty($events)) { + $this->events = $events; + } + if (is_resource($target) || $target instanceof Log) { + $this->target = $target; + } elseif (false === ($this->target = @fopen($target, 'w'))) { + throw new HTTP_Request2_Exception("Unable to open '{$target}'"); + } + } + + // }}} + // update() {{{ + + /** + * Called when the request notifies us of an event. + * + * @param HTTP_Request2 $subject The HTTP_Request2 instance + * + * @return void + */ + public function update(SplSubject $subject) + { + $event = $subject->getLastEvent(); + if (!in_array($event['name'], $this->events)) { + return; + } + + switch ($event['name']) { + case 'connect': + $this->log('* Connected to ' . $event['data']); + break; + case 'sentHeaders': + $headers = explode("\r\n", $event['data']); + array_pop($headers); + foreach ($headers as $header) { + $this->log('> ' . $header); + } + break; + case 'sentBodyPart': + $this->log('> ' . $event['data'] . ' byte(s) sent'); + break; + case 'receivedHeaders': + $this->log(sprintf('< HTTP/%s %s %s', + $event['data']->getVersion(), + $event['data']->getStatus(), + $event['data']->getReasonPhrase())); + $headers = $event['data']->getHeader(); + foreach ($headers as $key => $val) { + $this->log('< ' . $key . ': ' . $val); + } + $this->log('< '); + break; + case 'receivedBody': + $this->log($event['data']->getBody()); + break; + case 'disconnect': + $this->log('* Disconnected'); + break; + } + } + + // }}} + // log() {{{ + + /** + * Logs the given message to the configured target. + * + * @param string $message Message to display + * + * @return void + */ + protected function log($message) + { + if ($this->target instanceof Log) { + $this->target->debug($message); + } elseif (is_resource($this->target)) { + fwrite($this->target, $message . "\r\n"); + } + } + + // }}} +} + +?> \ No newline at end of file diff --git a/lib/php/HTTP/Request2/Response.php b/lib/php/HTTP/Request2/Response.php new file mode 100644 index 0000000000000000000000000000000000000000..217fbdf6b913e3fa29465539e86763b8a4d85a62 --- /dev/null +++ b/lib/php/HTTP/Request2/Response.php @@ -0,0 +1,559 @@ +<?php +/** + * Class representing a HTTP response + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Response.php 290520 2009-11-11 20:09:42Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Class representing a HTTP response + * + * The class is designed to be used in "streaming" scenario, building the + * response as it is being received: + * <code> + * $statusLine = read_status_line(); + * $response = new HTTP_Request2_Response($statusLine); + * do { + * $headerLine = read_header_line(); + * $response->parseHeaderLine($headerLine); + * } while ($headerLine != ''); + * + * while ($chunk = read_body()) { + * $response->appendBody($chunk); + * } + * + * var_dump($response->getHeader(), $response->getCookies(), $response->getBody()); + * </code> + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov <avb@php.net> + * @version Release: 0.5.1 + * @link http://tools.ietf.org/html/rfc2616#section-6 + */ +class HTTP_Request2_Response +{ + /** + * HTTP protocol version (e.g. 1.0, 1.1) + * @var string + */ + protected $version; + + /** + * Status code + * @var integer + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + */ + protected $code; + + /** + * Reason phrase + * @var string + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + */ + protected $reasonPhrase; + + /** + * Associative array of response headers + * @var array + */ + protected $headers = array(); + + /** + * Cookies set in the response + * @var array + */ + protected $cookies = array(); + + /** + * Name of last header processed by parseHederLine() + * + * Used to handle the headers that span multiple lines + * + * @var string + */ + protected $lastHeader = null; + + /** + * Response body + * @var string + */ + protected $body = ''; + + /** + * Whether the body is still encoded by Content-Encoding + * + * cURL provides the decoded body to the callback; if we are reading from + * socket the body is still gzipped / deflated + * + * @var bool + */ + protected $bodyEncoded; + + /** + * Associative array of HTTP status code / reason phrase. + * + * @var array + * @link http://tools.ietf.org/html/rfc2616#section-10 + */ + protected static $phrases = array( + + // 1xx: Informational - Request received, continuing process + 100 => 'Continue', + 101 => 'Switching Protocols', + + // 2xx: Success - The action was successfully received, understood and + // accepted + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + // 3xx: Redirection - Further action must be taken in order to complete + // the request + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // 1.1 + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + + // 4xx: Client Error - The request contains bad syntax or cannot be + // fulfilled + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + // 5xx: Server Error - The server failed to fulfill an apparently + // valid request + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 509 => 'Bandwidth Limit Exceeded', + + ); + + /** + * Constructor, parses the response status line + * + * @param string Response status line (e.g. "HTTP/1.1 200 OK") + * @param bool Whether body is still encoded by Content-Encoding + * @throws HTTP_Request2_Exception if status line is invalid according to spec + */ + public function __construct($statusLine, $bodyEncoded = true) + { + if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) { + throw new HTTP_Request2_Exception("Malformed response: {$statusLine}"); + } + $this->version = $m[1]; + $this->code = intval($m[2]); + if (!empty($m[3])) { + $this->reasonPhrase = trim($m[3]); + } elseif (!empty(self::$phrases[$this->code])) { + $this->reasonPhrase = self::$phrases[$this->code]; + } + $this->bodyEncoded = (bool)$bodyEncoded; + } + + /** + * Parses the line from HTTP response filling $headers array + * + * The method should be called after reading the line from socket or receiving + * it into cURL callback. Passing an empty string here indicates the end of + * response headers and triggers additional processing, so be sure to pass an + * empty string in the end. + * + * @param string Line from HTTP response + */ + public function parseHeaderLine($headerLine) + { + $headerLine = trim($headerLine, "\r\n"); + + // empty string signals the end of headers, process the received ones + if ('' == $headerLine) { + if (!empty($this->headers['set-cookie'])) { + $cookies = is_array($this->headers['set-cookie'])? + $this->headers['set-cookie']: + array($this->headers['set-cookie']); + foreach ($cookies as $cookieString) { + $this->parseCookie($cookieString); + } + unset($this->headers['set-cookie']); + } + foreach (array_keys($this->headers) as $k) { + if (is_array($this->headers[$k])) { + $this->headers[$k] = implode(', ', $this->headers[$k]); + } + } + + // string of the form header-name: header value + } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) { + $name = strtolower($m[1]); + $value = trim($m[2]); + if (empty($this->headers[$name])) { + $this->headers[$name] = $value; + } else { + if (!is_array($this->headers[$name])) { + $this->headers[$name] = array($this->headers[$name]); + } + $this->headers[$name][] = $value; + } + $this->lastHeader = $name; + + // continuation of a previous header + } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) { + if (!is_array($this->headers[$this->lastHeader])) { + $this->headers[$this->lastHeader] .= ' ' . trim($m[1]); + } else { + $key = count($this->headers[$this->lastHeader]) - 1; + $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]); + } + } + } + + /** + * Parses a Set-Cookie header to fill $cookies array + * + * @param string value of Set-Cookie header + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + */ + protected function parseCookie($cookieString) + { + $cookie = array( + 'expires' => null, + 'domain' => null, + 'path' => null, + 'secure' => false + ); + + // Only a name=value pair + if (!strpos($cookieString, ';')) { + $pos = strpos($cookieString, '='); + $cookie['name'] = trim(substr($cookieString, 0, $pos)); + $cookie['value'] = trim(substr($cookieString, $pos + 1)); + + // Some optional parameters are supplied + } else { + $elements = explode(';', $cookieString); + $pos = strpos($elements[0], '='); + $cookie['name'] = trim(substr($elements[0], 0, $pos)); + $cookie['value'] = trim(substr($elements[0], $pos + 1)); + + for ($i = 1; $i < count($elements); $i++) { + if (false === strpos($elements[$i], '=')) { + $elName = trim($elements[$i]); + $elValue = null; + } else { + list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); + } + $elName = strtolower($elName); + if ('secure' == $elName) { + $cookie['secure'] = true; + } elseif ('expires' == $elName) { + $cookie['expires'] = str_replace('"', '', $elValue); + } elseif ('path' == $elName || 'domain' == $elName) { + $cookie[$elName] = urldecode($elValue); + } else { + $cookie[$elName] = $elValue; + } + } + } + $this->cookies[] = $cookie; + } + + /** + * Appends a string to the response body + * @param string + */ + public function appendBody($bodyChunk) + { + $this->body .= $bodyChunk; + } + + /** + * Returns the status code + * @return integer + */ + public function getStatus() + { + return $this->code; + } + + /** + * Returns the reason phrase + * @return string + */ + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** + * Whether response is a redirect that can be automatically handled by HTTP_Request2 + * @return bool + */ + public function isRedirect() + { + return in_array($this->code, array(300, 301, 302, 303, 307)) + && isset($this->headers['location']); + } + + /** + * Returns either the named header or all response headers + * + * @param string Name of header to return + * @return string|array Value of $headerName header (null if header is + * not present), array of all response headers if + * $headerName is null + */ + public function getHeader($headerName = null) + { + if (null === $headerName) { + return $this->headers; + } else { + $headerName = strtolower($headerName); + return isset($this->headers[$headerName])? $this->headers[$headerName]: null; + } + } + + /** + * Returns cookies set in response + * + * @return array + */ + public function getCookies() + { + return $this->cookies; + } + + /** + * Returns the body of the response + * + * @return string + * @throws HTTP_Request2_Exception if body cannot be decoded + */ + public function getBody() + { + if (!$this->bodyEncoded || + !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate')) + ) { + return $this->body; + + } else { + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('iso-8859-1'); + } + + try { + switch (strtolower($this->getHeader('content-encoding'))) { + case 'gzip': + $decoded = self::decodeGzip($this->body); + break; + case 'deflate': + $decoded = self::decodeDeflate($this->body); + } + } catch (Exception $e) { + } + + if (!empty($oldEncoding)) { + mb_internal_encoding($oldEncoding); + } + if (!empty($e)) { + throw $e; + } + return $decoded; + } + } + + /** + * Get the HTTP version of the response + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Decodes the message-body encoded by gzip + * + * The real decoding work is done by gzinflate() built-in function, this + * method only parses the header and checks data for compliance with + * RFC 1952 + * + * @param string gzip-encoded data + * @return string decoded data + * @throws HTTP_Request2_Exception + * @link http://tools.ietf.org/html/rfc1952 + */ + public static function decodeGzip($data) + { + $length = strlen($data); + // If it doesn't look like gzip-encoded data, don't bother + if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { + return $data; + } + if (!function_exists('gzinflate')) { + throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available'); + } + $method = ord(substr($data, 2, 1)); + if (8 != $method) { + throw new HTTP_Request2_Exception('Error parsing gzip header: unknown compression method'); + } + $flags = ord(substr($data, 3, 1)); + if ($flags & 224) { + throw new HTTP_Request2_Exception('Error parsing gzip header: reserved bits are set'); + } + + // header is 10 bytes minimum. may be longer, though. + $headerLength = 10; + // extra fields, need to skip 'em + if ($flags & 4) { + if ($length - $headerLength - 2 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $extraLength = unpack('v', substr($data, 10, 2)); + if ($length - $headerLength - 2 - $extraLength[1] < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $headerLength += $extraLength[1] + 2; + } + // file name, need to skip that + if ($flags & 8) { + if ($length - $headerLength - 1 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $filenameLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $headerLength += $filenameLength + 1; + } + // comment, need to skip that also + if ($flags & 16) { + if ($length - $headerLength - 1 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $commentLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $headerLength += $commentLength + 1; + } + // have a CRC for header. let's check + if ($flags & 2) { + if ($length - $headerLength - 2 < 8) { + throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + } + $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); + $crcStored = unpack('v', substr($data, $headerLength, 2)); + if ($crcReal != $crcStored[1]) { + throw new HTTP_Request2_Exception('Header CRC check failed'); + } + $headerLength += 2; + } + // unpacked data CRC and size at the end of encoded data + $tmp = unpack('V2', substr($data, -8)); + $dataCrc = $tmp[1]; + $dataSize = $tmp[2]; + + // finally, call the gzinflate() function + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 + $unpacked = gzinflate(substr($data, $headerLength, -8)); + if (false === $unpacked) { + throw new HTTP_Request2_Exception('gzinflate() call failed'); + } elseif ($dataSize != strlen($unpacked)) { + throw new HTTP_Request2_Exception('Data size check failed'); + } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { + throw new HTTP_Request2_Exception('Data CRC check failed'); + } + return $unpacked; + } + + /** + * Decodes the message-body encoded by deflate + * + * @param string deflate-encoded data + * @return string decoded data + * @throws HTTP_Request2_Exception + */ + public static function decodeDeflate($data) + { + if (!function_exists('gzuncompress')) { + throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available'); + } + // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950, + // while many applications send raw deflate stream from RFC 1951. + // We should check for presence of zlib header and use gzuncompress() or + // gzinflate() as needed. See bug #15305 + $header = unpack('n', substr($data, 0, 2)); + return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data); + } +} +?> \ No newline at end of file diff --git a/lib/php/Net/URL2.php b/lib/php/Net/URL2.php new file mode 100644 index 0000000000000000000000000000000000000000..08cc7b11cb84e927aba0a96f67795e8789cce6e2 --- /dev/null +++ b/lib/php/Net/URL2.php @@ -0,0 +1,913 @@ +<?php +/** + * Net_URL2, a class representing a URL as per RFC 3986. + * + * PHP version 5 + * + * LICENSE: + * + * Copyright (c) 2007-2009, Peytz & Co. A/S + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * * Neither the name of the Net_URL2 nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Networking + * @package Net_URL2 + * @author Christian Schmidt <schmidt@php.net> + * @copyright 2007-2009 Peytz & Co. A/S + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: URL2.php 289017 2009-09-30 20:16:39Z schmidt $ + * @link http://www.rfc-editor.org/rfc/rfc3986.txt + */ + +/** + * Represents a URL as per RFC 3986. + * + * @category Networking + * @package Net_URL2 + * @author Christian Schmidt <schmidt@php.net> + * @copyright 2007-2009 Peytz & Co. A/S + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Net_URL2 + */ +class Net_URL2 +{ + /** + * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default + * is true. + */ + const OPTION_STRICT = 'strict'; + + /** + * Represent arrays in query using PHP's [] notation. Default is true. + */ + const OPTION_USE_BRACKETS = 'use_brackets'; + + /** + * URL-encode query variable keys. Default is true. + */ + const OPTION_ENCODE_KEYS = 'encode_keys'; + + /** + * Query variable separators when parsing the query string. Every character + * is considered a separator. Default is "&". + */ + const OPTION_SEPARATOR_INPUT = 'input_separator'; + + /** + * Query variable separator used when generating the query string. Default + * is "&". + */ + const OPTION_SEPARATOR_OUTPUT = 'output_separator'; + + /** + * Default options corresponds to how PHP handles $_GET. + */ + private $_options = array( + self::OPTION_STRICT => true, + self::OPTION_USE_BRACKETS => true, + self::OPTION_ENCODE_KEYS => true, + self::OPTION_SEPARATOR_INPUT => '&', + self::OPTION_SEPARATOR_OUTPUT => '&', + ); + + /** + * @var string|bool + */ + private $_scheme = false; + + /** + * @var string|bool + */ + private $_userinfo = false; + + /** + * @var string|bool + */ + private $_host = false; + + /** + * @var string|bool + */ + private $_port = false; + + /** + * @var string + */ + private $_path = ''; + + /** + * @var string|bool + */ + private $_query = false; + + /** + * @var string|bool + */ + private $_fragment = false; + + /** + * Constructor. + * + * @param string $url an absolute or relative URL + * @param array $options an array of OPTION_xxx constants + */ + public function __construct($url, $options = null) + { + if (is_array($options)) { + foreach ($options as $optionName => $value) { + $this->setOption($optionName, $value); + } + } + + // The regular expression is copied verbatim from RFC 3986, appendix B. + // The expression does not validate the URL but matches any string. + preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!', + $url, + $matches); + + // "path" is always present (possibly as an empty string); the rest + // are optional. + $this->_scheme = !empty($matches[1]) ? $matches[2] : false; + $this->setAuthority(!empty($matches[3]) ? $matches[4] : false); + $this->_path = $matches[5]; + $this->_query = !empty($matches[6]) ? $matches[7] : false; + $this->_fragment = !empty($matches[8]) ? $matches[9] : false; + } + + /** + * Magic Setter. + * + * This method will magically set the value of a private variable ($var) + * with the value passed as the args + * + * @param string $var The private variable to set. + * @param mixed $arg An argument of any type. + * @return void + */ + public function __set($var, $arg) + { + $method = 'set' . $var; + if (method_exists($this, $method)) { + $this->$method($arg); + } + } + + /** + * Magic Getter. + * + * This is the magic get method to retrieve the private variable + * that was set by either __set() or it's setter... + * + * @param string $var The property name to retrieve. + * @return mixed $this->$var Either a boolean false if the + * property is not set or the value + * of the private property. + */ + public function __get($var) + { + $method = 'get' . $var; + if (method_exists($this, $method)) { + return $this->$method(); + } + + return false; + } + + /** + * Returns the scheme, e.g. "http" or "urn", or false if there is no + * scheme specified, i.e. if this is a relative URL. + * + * @return string|bool + */ + public function getScheme() + { + return $this->_scheme; + } + + /** + * Sets the scheme, e.g. "http" or "urn". Specify false if there is no + * scheme specified, i.e. if this is a relative URL. + * + * @param string|bool $scheme e.g. "http" or "urn", or false if there is no + * scheme specified, i.e. if this is a relative + * URL + * + * @return void + * @see getScheme() + */ + public function setScheme($scheme) + { + $this->_scheme = $scheme; + } + + /** + * Returns the user part of the userinfo part (the part preceding the first + * ":"), or false if there is no userinfo part. + * + * @return string|bool + */ + public function getUser() + { + return $this->_userinfo !== false + ? preg_replace('@:.*$@', '', $this->_userinfo) + : false; + } + + /** + * Returns the password part of the userinfo part (the part after the first + * ":"), or false if there is no userinfo part (i.e. the URL does not + * contain "@" in front of the hostname) or the userinfo part does not + * contain ":". + * + * @return string|bool + */ + public function getPassword() + { + return $this->_userinfo !== false + ? substr(strstr($this->_userinfo, ':'), 1) + : false; + } + + /** + * Returns the userinfo part, or false if there is none, i.e. if the + * authority part does not contain "@". + * + * @return string|bool + */ + public function getUserinfo() + { + return $this->_userinfo; + } + + /** + * Sets the userinfo part. If two arguments are passed, they are combined + * in the userinfo part as username ":" password. + * + * @param string|bool $userinfo userinfo or username + * @param string|bool $password optional password, or false + * + * @return void + */ + public function setUserinfo($userinfo, $password = false) + { + $this->_userinfo = $userinfo; + if ($password !== false) { + $this->_userinfo .= ':' . $password; + } + } + + /** + * Returns the host part, or false if there is no authority part, e.g. + * relative URLs. + * + * @return string|bool a hostname, an IP address, or false + */ + public function getHost() + { + return $this->_host; + } + + /** + * Sets the host part. Specify false if there is no authority part, e.g. + * relative URLs. + * + * @param string|bool $host a hostname, an IP address, or false + * + * @return void + */ + public function setHost($host) + { + $this->_host = $host; + } + + /** + * Returns the port number, or false if there is no port number specified, + * i.e. if the default port is to be used. + * + * @return string|bool + */ + public function getPort() + { + return $this->_port; + } + + /** + * Sets the port number. Specify false if there is no port number specified, + * i.e. if the default port is to be used. + * + * @param string|bool $port a port number, or false + * + * @return void + */ + public function setPort($port) + { + $this->_port = $port; + } + + /** + * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or + * false if there is no authority. + * + * @return string|bool + */ + public function getAuthority() + { + if (!$this->_host) { + return false; + } + + $authority = ''; + + if ($this->_userinfo !== false) { + $authority .= $this->_userinfo . '@'; + } + + $authority .= $this->_host; + + if ($this->_port !== false) { + $authority .= ':' . $this->_port; + } + + return $authority; + } + + /** + * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify + * false if there is no authority. + * + * @param string|false $authority a hostname or an IP addresse, possibly + * with userinfo prefixed and port number + * appended, e.g. "foo:bar@example.org:81". + * + * @return void + */ + public function setAuthority($authority) + { + $this->_userinfo = false; + $this->_host = false; + $this->_port = false; + if (preg_match('@^(([^\@]*)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) { + if ($reg[1]) { + $this->_userinfo = $reg[2]; + } + + $this->_host = $reg[3]; + if (isset($reg[5])) { + $this->_port = $reg[5]; + } + } + } + + /** + * Returns the path part (possibly an empty string). + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Sets the path part (possibly an empty string). + * + * @param string $path a path + * + * @return void + */ + public function setPath($path) + { + $this->_path = $path; + } + + /** + * Returns the query string (excluding the leading "?"), or false if "?" + * is not present in the URL. + * + * @return string|bool + * @see self::getQueryVariables() + */ + public function getQuery() + { + return $this->_query; + } + + /** + * Sets the query string (excluding the leading "?"). Specify false if "?" + * is not present in the URL. + * + * @param string|bool $query a query string, e.g. "foo=1&bar=2" + * + * @return void + * @see self::setQueryVariables() + */ + public function setQuery($query) + { + $this->_query = $query; + } + + /** + * Returns the fragment name, or false if "#" is not present in the URL. + * + * @return string|bool + */ + public function getFragment() + { + return $this->_fragment; + } + + /** + * Sets the fragment name. Specify false if "#" is not present in the URL. + * + * @param string|bool $fragment a fragment excluding the leading "#", or + * false + * + * @return void + */ + public function setFragment($fragment) + { + $this->_fragment = $fragment; + } + + /** + * Returns the query string like an array as the variables would appear in + * $_GET in a PHP script. If the URL does not contain a "?", an empty array + * is returned. + * + * @return array + */ + public function getQueryVariables() + { + $pattern = '/[' . + preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') . + ']/'; + $parts = preg_split($pattern, $this->_query, -1, PREG_SPLIT_NO_EMPTY); + $return = array(); + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part, 2); + } else { + $key = $part; + $value = null; + } + + if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + $key = rawurldecode($key); + } + $value = rawurldecode($value); + + if ($this->getOption(self::OPTION_USE_BRACKETS) && + preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) { + + $key = $matches[1]; + $idx = $matches[2]; + + // Ensure is an array + if (empty($return[$key]) || !is_array($return[$key])) { + $return[$key] = array(); + } + + // Add data + if ($idx === '') { + $return[$key][] = $value; + } else { + $return[$key][$idx] = $value; + } + } elseif (!$this->getOption(self::OPTION_USE_BRACKETS) + && !empty($return[$key]) + ) { + $return[$key] = (array) $return[$key]; + $return[$key][] = $value; + } else { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Sets the query string to the specified variable in the query string. + * + * @param array $array (name => value) array + * + * @return void + */ + public function setQueryVariables(array $array) + { + if (!$array) { + $this->_query = false; + } else { + foreach ($array as $name => $value) { + if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + $name = self::urlencode($name); + } + + if (is_array($value)) { + foreach ($value as $k => $v) { + $parts[] = $this->getOption(self::OPTION_USE_BRACKETS) + ? sprintf('%s[%s]=%s', $name, $k, $v) + : ($name . '=' . $v); + } + } elseif (!is_null($value)) { + $parts[] = $name . '=' . self::urlencode($value); + } else { + $parts[] = $name; + } + } + $this->_query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT), + $parts); + } + } + + /** + * Sets the specified variable in the query string. + * + * @param string $name variable name + * @param mixed $value variable value + * + * @return array + */ + public function setQueryVariable($name, $value) + { + $array = $this->getQueryVariables(); + $array[$name] = $value; + $this->setQueryVariables($array); + } + + /** + * Removes the specifed variable from the query string. + * + * @param string $name a query string variable, e.g. "foo" in "?foo=1" + * + * @return void + */ + public function unsetQueryVariable($name) + { + $array = $this->getQueryVariables(); + unset($array[$name]); + $this->setQueryVariables($array); + } + + /** + * Returns a string representation of this URL. + * + * @return string + */ + public function getURL() + { + // See RFC 3986, section 5.3 + $url = ""; + + if ($this->_scheme !== false) { + $url .= $this->_scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== false) { + $url .= '//' . $authority; + } + $url .= $this->_path; + + if ($this->_query !== false) { + $url .= '?' . $this->_query; + } + + if ($this->_fragment !== false) { + $url .= '#' . $this->_fragment; + } + + return $url; + } + + /** + * Returns a string representation of this URL. + * + * @return string + * @see toString() + */ + public function __toString() + { + return $this->getURL(); + } + + /** + * Returns a normalized string representation of this URL. This is useful + * for comparison of URLs. + * + * @return string + */ + public function getNormalizedURL() + { + $url = clone $this; + $url->normalize(); + return $url->getUrl(); + } + + /** + * Returns a normalized Net_URL2 instance. + * + * @return Net_URL2 + */ + public function normalize() + { + // See RFC 3886, section 6 + + // Schemes are case-insensitive + if ($this->_scheme) { + $this->_scheme = strtolower($this->_scheme); + } + + // Hostnames are case-insensitive + if ($this->_host) { + $this->_host = strtolower($this->_host); + } + + // Remove default port number for known schemes (RFC 3986, section 6.2.3) + if ($this->_port && + $this->_scheme && + $this->_port == getservbyname($this->_scheme, 'tcp')) { + + $this->_port = false; + } + + // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) + foreach (array('_userinfo', '_host', '_path') as $part) { + if ($this->$part) { + $this->$part = preg_replace('/%[0-9a-f]{2}/ie', + 'strtoupper("\0")', + $this->$part); + } + } + + // Path segment normalization (RFC 3986, section 6.2.2.3) + $this->_path = self::removeDotSegments($this->_path); + + // Scheme based normalization (RFC 3986, section 6.2.3) + if ($this->_host && !$this->_path) { + $this->_path = '/'; + } + } + + /** + * Returns whether this instance represents an absolute URL. + * + * @return bool + */ + public function isAbsolute() + { + return (bool) $this->_scheme; + } + + /** + * Returns an Net_URL2 instance representing an absolute URL relative to + * this URL. + * + * @param Net_URL2|string $reference relative URL + * + * @return Net_URL2 + */ + public function resolve($reference) + { + if (!$reference instanceof Net_URL2) { + $reference = new self($reference); + } + if (!$this->isAbsolute()) { + throw new Exception('Base-URL must be absolute'); + } + + // A non-strict parser may ignore a scheme in the reference if it is + // identical to the base URI's scheme. + if (!$this->getOption(self::OPTION_STRICT) && $reference->_scheme == $this->_scheme) { + $reference->_scheme = false; + } + + $target = new self(''); + if ($reference->_scheme !== false) { + $target->_scheme = $reference->_scheme; + $target->setAuthority($reference->getAuthority()); + $target->_path = self::removeDotSegments($reference->_path); + $target->_query = $reference->_query; + } else { + $authority = $reference->getAuthority(); + if ($authority !== false) { + $target->setAuthority($authority); + $target->_path = self::removeDotSegments($reference->_path); + $target->_query = $reference->_query; + } else { + if ($reference->_path == '') { + $target->_path = $this->_path; + if ($reference->_query !== false) { + $target->_query = $reference->_query; + } else { + $target->_query = $this->_query; + } + } else { + if (substr($reference->_path, 0, 1) == '/') { + $target->_path = self::removeDotSegments($reference->_path); + } else { + // Merge paths (RFC 3986, section 5.2.3) + if ($this->_host !== false && $this->_path == '') { + $target->_path = '/' . $this->_path; + } else { + $i = strrpos($this->_path, '/'); + if ($i !== false) { + $target->_path = substr($this->_path, 0, $i + 1); + } + $target->_path .= $reference->_path; + } + $target->_path = self::removeDotSegments($target->_path); + } + $target->_query = $reference->_query; + } + $target->setAuthority($this->getAuthority()); + } + $target->_scheme = $this->_scheme; + } + + $target->_fragment = $reference->_fragment; + + return $target; + } + + /** + * Removes dots as described in RFC 3986, section 5.2.4, e.g. + * "/foo/../bar/baz" => "/bar/baz" + * + * @param string $path a path + * + * @return string a path + */ + public static function removeDotSegments($path) + { + $output = ''; + + // Make sure not to be trapped in an infinite loop due to a bug in this + // method + $j = 0; + while ($path && $j++ < 100) { + if (substr($path, 0, 2) == './') { + // Step 2.A + $path = substr($path, 2); + } elseif (substr($path, 0, 3) == '../') { + // Step 2.A + $path = substr($path, 3); + } elseif (substr($path, 0, 3) == '/./' || $path == '/.') { + // Step 2.B + $path = '/' . substr($path, 3); + } elseif (substr($path, 0, 4) == '/../' || $path == '/..') { + // Step 2.C + $path = '/' . substr($path, 4); + $i = strrpos($output, '/'); + $output = $i === false ? '' : substr($output, 0, $i); + } elseif ($path == '.' || $path == '..') { + // Step 2.D + $path = ''; + } else { + // Step 2.E + $i = strpos($path, '/'); + if ($i === 0) { + $i = strpos($path, '/', 1); + } + if ($i === false) { + $i = strlen($path); + } + $output .= substr($path, 0, $i); + $path = substr($path, $i); + } + } + + return $output; + } + + /** + * Percent-encodes all non-alphanumeric characters except these: _ . - ~ + * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP + * 5.2.x and earlier. + * + * @param $raw the string to encode + * @return string + */ + public static function urlencode($string) + { + $encoded = rawurlencode($string); + // This is only necessary in PHP < 5.3. + $encoded = str_replace('%7E', '~', $encoded); + return $encoded; + } + + /** + * Returns a Net_URL2 instance representing the canonical URL of the + * currently executing PHP script. + * + * @return string + */ + public static function getCanonical() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['PHP_SELF']); + $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + $url->_host = $_SERVER['SERVER_NAME']; + $port = $_SERVER['SERVER_PORT']; + if ($url->_scheme == 'http' && $port != 80 || + $url->_scheme == 'https' && $port != 443) { + + $url->_port = $port; + } + return $url; + } + + /** + * Returns the URL used to retrieve the current request. + * + * @return string + */ + public static function getRequestedURL() + { + return self::getRequested()->getUrl(); + } + + /** + * Returns a Net_URL2 instance representing the URL used to retrieve the + * current request. + * + * @return Net_URL2 + */ + public static function getRequested() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['REQUEST_URI']); + $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + // Set host and possibly port + $url->setAuthority($_SERVER['HTTP_HOST']); + return $url; + } + + /** + * Sets the specified option. + * + * @param string $optionName a self::OPTION_ constant + * @param mixed $value option value + * + * @return void + * @see self::OPTION_STRICT + * @see self::OPTION_USE_BRACKETS + * @see self::OPTION_ENCODE_KEYS + */ + function setOption($optionName, $value) + { + if (!array_key_exists($optionName, $this->_options)) { + return false; + } + $this->_options[$optionName] = $value; + } + + /** + * Returns the value of the specified option. + * + * @param string $optionName The name of the option to retrieve + * + * @return mixed + */ + function getOption($optionName) + { + return isset($this->_options[$optionName]) + ? $this->_options[$optionName] : false; + } +} diff --git a/lib/php/SimpleCAS.php b/lib/php/SimpleCAS.php new file mode 100644 index 0000000000000000000000000000000000000000..224a0fadc2136cae1442ab6b9791c30b822d7795 --- /dev/null +++ b/lib/php/SimpleCAS.php @@ -0,0 +1,247 @@ +<?php +/** + * This is a CAS client authentication library for PHP 5. + * + * <code> + * <?php + * $protocol = new SimpleCAS_Protocol_Version2('login.unl.edu', 443, 'cas'); + * $client = SimpleCAS::client($protocol); + * $client->forceAuthentication(); + * + * if (isset($_GET['logout'])) { + * $client->logout(); + * } + * + * if ($client->isAuthenticated()) { + * echo '<h1>Authentication Successful!</h1>'; + * echo '<p>The user\'s login is '.$client->getUsername().'</p>'; + * echo '<a href="?logout">Logout</a>'; + * } + * </code> + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +class SimpleCAS +{ + /** + * Version of the CAS library. + */ + const VERSION = '0.0.1'; + + /** + * Singleton CAS object + * + * @var CAS + */ + static private $_instance; + + /** + * Is user authenticated? + * + * @var bool + */ + private $_authenticated = false; + + /** + * Protocol for the server running the CAS service. + * + * @var SimpleCAS_Protocol + */ + protected $protocol; + + /** + * User's login name if authenticated. + * + * @var string + */ + protected $username; + + /** + * Construct a CAS client object. + * + * @param SimpleCAS_Protocol $protocol Protocol to use for authentication. + */ + private function __construct(SimpleCAS_Protocol $protocol) + { + $this->protocol = $protocol; + + if ($this->protocol instanceof SimpleCAS_SingleSignOut + && isset($_POST)) { + if ($ticket = $this->protocol->validateLogoutRequest($_POST)) { + $this->logout($ticket); + } + } + + if (session_id() == '') { + session_start(); + if (isset($_SESSION['__SIMPLECAS_TICKET'])) { + $this->_authenticated = true; + } + } + + if ($this->_authenticated == false + && isset($_GET['ticket'])) { + $this->validateTicket($_GET['ticket']); + } + } + + /** + * Checks a ticket to see if it is valid. + * + * If the CAS server verifies the ticket, a session is created and the user + * is marked as authenticated. + * + * @param string $ticket Ticket from the CAS Server + * + * @return bool + */ + protected function validateTicket($ticket) + { + if ($uid = $this->protocol->validateTicket($ticket, self::getURL())) { + $this->setAuthenticated($uid); + $this->redirect(self::getURL()); + return true; + } else { + return false; + } + } + + /** + * Marks the current session as authenticated. + * + * @param string $uid User name returned by the CAS server. + * + * @return void + */ + protected function setAuthenticated($uid) + { + $_SESSION['__SIMPLECAS_TICKET'] = true; + $_SESSION['__SIMPLECAS_UID'] = $uid; + $this->_authenticated = true; + } + + /** + * Return the authenticated user's login name. + * + * @return string + */ + public function getUsername() + { + return $_SESSION['__SIMPLECAS_UID']; + } + + /** + * Singleton interface, returns CAS object. + * + * @param CAS_Server $server CAS Server object + * + * @return CAS + */ + static public function client(SimpleCAS_Protocol $protocol) + { + if (!isset(self::$_instance)) { + self::$_instance = new self($protocol); + } + + return self::$_instance; + } + + /** + * If client is not authenticated, this will redirecting to login and exit. + * + * Otherwise, return the CAS object. + * + * @return CAS + */ + function forceAuthentication() + { + if (!$this->isAuthenticated()) { + self::redirect($this->protocol->getLoginURL(self::getURL())); + } + return $this; + } + + /** + * Check if this user has been authenticated or not. + * + * @return bool + */ + function isAuthenticated() + { + return $this->_authenticated; + } + + /** + * Destroys session data for this client, redirects to the server logout + * url. + * + * @param string $url URL to provide the client on logout. + * + * @return void + */ + public function logout($url = '') + { + session_destroy(); + $this->redirect($this->protocol->getLogoutURL(self::getURL())); + } + + /** + * Returns the current URL without CAS affecting parameters. + * + * @return string url + */ + static public function getURL() + { + if (isset($_SERVER['HTTPS']) + && !empty($_SERVER['HTTPS']) + && $_SERVER['HTTPS'] == 'on') { + $protocol = 'https'; + } else { + $protocol = 'http'; + } + + $url = $protocol.'://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; + + $replacements = array('/\?logout/' => '', + '/&ticket=[^&]*/' => '', + '/\?ticket=[^&;]*/' => '?', + '/\?%26/' => '?', + '/\?&/' => '?', + '/\?$/' => ''); + + $url = preg_replace(array_keys($replacements), + array_values($replacements), $url); + + return $url; + } + + /** + * Send a header to redirect the client to another URL. + * + * @param string $url URL to redirect the client to. + * + * @return void + */ + public static function redirect($url) + { + header("Location: $url"); + exit(); + } + + /** + * Get the version of the CAS library + * + * @return string + */ + static public function getVersion() + { + return self::VERSION; + } +} diff --git a/lib/php/SimpleCAS/Autoload.php b/lib/php/SimpleCAS/Autoload.php new file mode 100644 index 0000000000000000000000000000000000000000..b26f5a07131625c54f2c59ae6d65bfa9391284de --- /dev/null +++ b/lib/php/SimpleCAS/Autoload.php @@ -0,0 +1,62 @@ +<?php +function SimpleCAS_Autoload($class) +{ + if (substr($class, 0, 9) !== 'SimpleCAS') { + return false; + } + $fp = @fopen(str_replace('_', '/', $class) . '.php', 'r', true); + if ($fp) { + fclose($fp); + require str_replace('_', '/', $class) . '.php'; + if (!class_exists($class, false) && !interface_exists($class, false)) { + die(new Exception('Class ' . $class . ' was not present in ' . + str_replace('_', '/', $class) . '.php (include_path="' . get_include_path() . + '") [SimpleCAS_Autoload version 0.1.0]')); + } + return true; + } + $e = new Exception('Class ' . $class . ' could not be loaded from ' . + str_replace('_', '/', $class) . '.php, file does not exist (include_path="' . get_include_path() . + '") [SimpleCAS_Autoload version 0.1.0]'); + $trace = $e->getTrace(); + if (isset($trace[2]) && isset($trace[2]['function']) && + in_array($trace[2]['function'], array('class_exists', 'interface_exists'))) { + return false; + } + if (isset($trace[1]) && isset($trace[1]['function']) && + in_array($trace[1]['function'], array('class_exists', 'interface_exists'))) { + return false; + } + die ((string) $e); +} + +// set up __autoload +if (function_exists('spl_autoload_register')) { + if (!($_____t = spl_autoload_functions()) || !in_array('SimpleCAS_Autoload', spl_autoload_functions())) { + spl_autoload_register('SimpleCAS_Autoload'); + if (function_exists('__autoload') && ($_____t === false)) { + // __autoload() was being used, but now would be ignored, add + // it to the autoload stack + spl_autoload_register('__autoload'); + } + } + unset($_____t); +} elseif (!function_exists('__autoload')) { + function __autoload($class) { return SimpleCAS_Autoload($class); } +} + +// set up include_path if it doesn't register our current location +$____paths = explode(PATH_SEPARATOR, get_include_path()); +$____found = false; +foreach ($____paths as $____path) { + if ($____path == dirname(dirname(__FILE__))) { + $____found = true; + break; + } +} +if (!$____found) { + set_include_path(get_include_path() . PATH_SEPARATOR . dirname(dirname(__FILE__))); +} +unset($____paths); +unset($____path); +unset($____found); diff --git a/lib/php/SimpleCAS/Protocol.php b/lib/php/SimpleCAS/Protocol.php new file mode 100644 index 0000000000000000000000000000000000000000..adcfcc70ce914d1a696202743772a620a5aa05a7 --- /dev/null +++ b/lib/php/SimpleCAS/Protocol.php @@ -0,0 +1,79 @@ +<?php +/** + * Interface all CAS servers must implement. + * + * Each concrete class which implements this server interface must provide + * all the following functions. + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +abstract class SimpleCAS_Protocol +{ + protected $request; + + /** + * Returns the login URL for the cas server. + * + * @param string $service The URL to the service requesting authentication. + * + * @return string + */ + abstract function getLoginURL($service); + + /** + * Returns the logout url for the CAS server. + * + * @param string $service A URL to provide the user upon logout. + * + * @return string + */ + abstract function getLogoutURL($service = null); + + /** + * Returns the version of this cas server. + * + * @return string + */ + abstract function getVersion(); + + /** + * Function to validate a ticket and service combination. + * + * @param string $ticket Ticket given by the CAS Server + * @param string $service Service requesting authentication + * + * @return false|string False on failure, user name on success. + */ + abstract function validateTicket($ticket, $service); + + /** + * Get the HTTP_Request2 object. + * + * @return HTTP_Request + */ + function getRequest() + { + if (!$this->request instanceof HTTP_Request2) { + $this->request = new HTTP_Request2(); + } + return $this->request; + } + + /** + * Set the HTTP_Request object. + * + * @param HTTP_Request2 $request + */ + function setRequest(HTTP_Request2 $request) + { + $this->request = $request; + } +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/Protocol/Version1.php b/lib/php/SimpleCAS/Protocol/Version1.php new file mode 100644 index 0000000000000000000000000000000000000000..3d40390a9772fe9a46ad66d9ce750b802cc715a3 --- /dev/null +++ b/lib/php/SimpleCAS/Protocol/Version1.php @@ -0,0 +1,125 @@ +<?php +/** + * Class representing a CAS server which supports the CAS1 protocol. + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +class SimpleCAS_Protocol_Version1 extends SimpleCAS_Protocol +{ + const VERSION = '1.0'; + + protected $request; + + /** + * Construct a new SimpleCAS server object. + * + * <code> + * $options = array('hostname' => 'login.unl.edu', + * 'port' => 443, + * 'uri' => 'cas'); + * $protocol = new SimpleCAS_Protocol_Version1($options); + * </code> + * + * @param array() + */ + function __construct($options) + { + foreach ($options as $option=>$val) { + $this->$option = $val; + } + } + + /** + * Returns the URL used to validate a ticket. + * + * @param string $ticket Ticket to validate + * @param string $service URL to the service requesting authentication + * + * @return string + */ + function getValidationURL($ticket, $service) + { + return 'https://' . $this->hostname . '/' + . $this->uri . '/validate?' + . 'service=' . urlencode($service) + . '&ticket=' . $ticket; + } + + /** + * Returns the URL to login form for the CAS server. + * + * @param string $service Service url requesting authentication. + * + * @return string + */ + function getLoginURL($service) + { + return 'https://' . $this->hostname + . '/'.$this->uri + . '/login?service=' + . urlencode($service); + } + + /** + * Returns the URL to logout of the CAS server. + * + * @param string $service Service url provided to the user. + * + * @return string + */ + function getLogoutURL($service = '') + { + if (isset($service)) { + $service = '?url='.urlencode($service); + } + + return 'https://' . $this->hostname + . '/'.$this->uri + . '/logout' + . $service; + } + + /** + * Function to validate a ticket and service combination. + * + * @param string $ticket Ticket given by the CAS Server + * @param string $service Service requesting authentication + * + * @return false|string False on failure, user name on success. + */ + function validateTicket($ticket, $service) + { + $validation_url = $this->getValidationURL($ticket, $service); + + $http_request = clone $this->getRequest(); + + $http_request->setURL($validation_url); + + $response = $http_request->send(); + + if ($response->getStatus() == 200 + && substr($response->getBody(), 0, 3) == 'yes') { + list($message, $uid) = explode("\n", $response->getBody()); + return $uid; + } + return false; + } + + /** + * Returns the CAS server protocol this object implements. + * + * @return string + */ + function getVersion() + { + return self::VERSION; + } +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/Protocol/Version2.php b/lib/php/SimpleCAS/Protocol/Version2.php new file mode 100644 index 0000000000000000000000000000000000000000..5114493b2b03b5349b5513bfde446165283afbbb --- /dev/null +++ b/lib/php/SimpleCAS/Protocol/Version2.php @@ -0,0 +1,87 @@ +<?php +/** + * Class representing a CAS server which supports the CAS2 protocol. + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +class SimpleCAS_Protocol_Version2 extends SimpleCAS_Protocol_Version1 implements SimpleCAS_SingleSignOut, SimpleCAS_ProxyGranting +{ + const VERSION = '2.0'; + + /** + * Returns the URL used to validate a ticket. + * + * @param string $ticket Ticket to validate + * @param string $service URL to the service requesting authentication + * + * @return string + */ + function getValidationURL($ticket, $service, $pgtUrl = null) + { + return 'https://' . $this->hostname . '/' + . $this->uri . '/serviceValidate?' + . 'service=' . urlencode($service) + . '&ticket=' . $ticket + . '&pgtUrl=' . urlencode($pgtUrl); + } + + /** + * Function to validate a ticket and service combination. + * + * @param string $ticket Ticket given by the CAS Server + * @param string $service Service requesting authentication + * + * @return false|string False on failure, user name on success. + */ + function validateTicket($ticket, $service) + { + $validation_url = $this->getValidationURL($ticket, $service); + + $http_request = clone $this->getRequest(); + + $http_request->setURL($validation_url); + + $response = $http_request->send(); + + if ($response->getStatus() == 200) { + $validationResponse = new SimpleCAS_Protocol_Version2_ValidationResponse($response->getBody()); + if ($validationResponse->authenticationSuccess()) { + return $validationResponse->__toString(); + } + } + return false; + } + + /** + * Validates a single sign out logout request. + * + * @param mixed $post $_POST data + * + * @return bool + */ + function validateLogoutRequest($post) + { + if (false) { + return $ticket; + } + return false; + } + + function getProxyTicket() + { + throw new Exception('not implemented'); + } + + function validateProxyTicket($ticket) + { + throw new Exception('not implemented'); + } +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/Protocol/Version2/ValidationResponse.php b/lib/php/SimpleCAS/Protocol/Version2/ValidationResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..225a759b32680705d8b2c538e0927c61e0efae33 --- /dev/null +++ b/lib/php/SimpleCAS/Protocol/Version2/ValidationResponse.php @@ -0,0 +1,62 @@ +<?php +class SimpleCAS_Protocol_Version2_ValidationResponse +{ + protected $authenticationSuccess = false; + protected $user = false; + protected $pgtiou = false; + protected $proxies = array(); + + /** + * Construct a validation repsonse object from the CAS server's response. + * + * @param string $response + */ + function __construct($response) + { + $xml = new DOMDocument(); + if ($xml->loadXML($response)) { + if ($success = $xml->getElementsByTagName('authenticationSuccess')) { + if ($success->length > 0 + && $uid = $success->item(0)->getElementsByTagName('user')) { + // We have the user name, check for PGTIOU + if ($iou = $success->item(0)->getElementsByTagName('proxyGrantingTicket')) { + if ($iou->length) { + $this->pgtiou = $iou->item(0)->nodeValue; + } + } + $this->authenticationSuccess = true; + $this->user = $uid->item(0)->nodeValue; + } + } + } + } + + function authenticationSuccess() + { + return $this->authenticationSuccess; + } + + function getPGTIOU() + { + return $this->pgtiou; + } + + function getUser() + { + return $this->userid; + } + + function __toString() + { + if ($this->authenticationSuccess()) { + return $this->user; + } + throw new Exception('Validation was not successful'); + } + + function logout() + { + + } +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/ProxyGranting.php b/lib/php/SimpleCAS/ProxyGranting.php new file mode 100644 index 0000000000000000000000000000000000000000..3ab250ce5855859b82d0cd1a01c3ebd74afb3850 --- /dev/null +++ b/lib/php/SimpleCAS/ProxyGranting.php @@ -0,0 +1,31 @@ +<?php +/** + * Interface for servers that implement proxy granting tickets. + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +interface SimpleCAS_ProxyGranting +{ + + /** + * get a proxy ticket + * + * @return string + */ + function getProxyTicket(); + + /** + * try and validate a proxy ticket + * + * @param unknown_type $ticket + */ + function validateProxyTicket($ticket); +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/ProxyGranting/Storage.php b/lib/php/SimpleCAS/ProxyGranting/Storage.php new file mode 100644 index 0000000000000000000000000000000000000000..e5560410893ade9aefe2365605ae2fdee53f44e9 --- /dev/null +++ b/lib/php/SimpleCAS/ProxyGranting/Storage.php @@ -0,0 +1,7 @@ +<?php +interface SimpleCAS_ProxyGranting_Storage +{ + function saveIOU($iou); + function getProxyGrantingTicket($iou); +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/ProxyGranting/Storage/File.php b/lib/php/SimpleCAS/ProxyGranting/Storage/File.php new file mode 100644 index 0000000000000000000000000000000000000000..b345c7768e84a62d4f4c5e8cb4d460f73270942f --- /dev/null +++ b/lib/php/SimpleCAS/ProxyGranting/Storage/File.php @@ -0,0 +1,14 @@ +<?php +class SimpleCAS_ProxyGranting_Storage_File implements SimpleCAS_ProxyGranting_Storage +{ + function saveIOU($iou) + { + throw new Exception('not implemented'); + } + + function getProxyGrantingTicket($iou) + { + throw new Exception('not implemented'); + } +} +?> \ No newline at end of file diff --git a/lib/php/SimpleCAS/SingleSignOut.php b/lib/php/SimpleCAS/SingleSignOut.php new file mode 100644 index 0000000000000000000000000000000000000000..1b0e97f4399580eeb372c631ced1751228df5827 --- /dev/null +++ b/lib/php/SimpleCAS/SingleSignOut.php @@ -0,0 +1,24 @@ +<?php +/** + * Interface for servers that implement single sign out. + * + * PHP version 5 + * + * @category Authentication + * @package SimpleCAS + * @author Brett Bieber <brett.bieber@gmail.com> + * @copyright 2008 Regents of the University of Nebraska + * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License + * @link http://code.google.com/p/simplecas/ + */ +interface SimpleCAS_SingleSignOut +{ + /** + * Determines if the posted request is a valid single sign out request. + * + * @param mixed $post $_POST data sent to the service. + * + * @return bool + */ + function validateLogoutRequest($post); +} \ No newline at end of file