http://www.mediawiki.org/wiki/Special:Code/MediaWiki/94139
Revision: 94139 Author: ashley Date: 2011-08-09 23:37:26 +0000 (Tue, 09 Aug 2011) Log Message: ----------- SocialProfile: lots of miscellaneous cleanup to UserStatus, but the main big changes here are: *more documentation *changed us-link from an ID to a class because IDs must be unique *shut up a PHP warning about ush_likes *do escaping properly via all the appropriate MW functions and do escaping as close to the output as possible to avoid reviewer anxiety *simple checking in wfSaveStatus to make sure that the user is who they claim to be; would probably be best to use edit token stuff here *moved a lot of HTML building from JS to PHP *decoupled UserStatus and UserProfile by removing UserProfilePage::getStatus() and moving the logic to UserStatus_AjaxFunctions.php, function wfUserProfileStatusOutput *used Language::timeanddate() instead of custom DateTime building for status message dates I probably broke something, even though I tried to test this rather extensively. Modified Paths: -------------- trunk/extensions/SocialProfile/UserProfile/UserProfilePage.php trunk/extensions/SocialProfile/UserStatus/UserStatus.css trunk/extensions/SocialProfile/UserStatus/UserStatus.js trunk/extensions/SocialProfile/UserStatus/UserStatusClass.php trunk/extensions/SocialProfile/UserStatus/UserStatus_AjaxFunctions.php Modified: trunk/extensions/SocialProfile/UserProfile/UserProfilePage.php =================================================================== --- trunk/extensions/SocialProfile/UserProfile/UserProfilePage.php 2011-08-09 21:51:22 UTC (rev 94138) +++ trunk/extensions/SocialProfile/UserProfile/UserProfilePage.php 2011-08-09 23:37:26 UTC (rev 94139) @@ -1782,32 +1782,4 @@ return $output; } - function getStatus( $userId ) { - global $wgUser; - - $us_class = new UserStatusClass(); - $user_status_array = $us_class->getStatus( $userId ); - if ( empty( $user_status_array ) ) { - $buf = ''; - } else { - $buf = $user_status_array['us_status']; - } - - // Only owners of the page can change statuses - if ( $wgUser->getId() == $userId ) { - if ( $wgUser->isBlocked() ) { - return wfMsg( 'userstatus-blocked' ); - } - - // Database operations require write mode - if ( wfReadOnly() ) { - return wfMsg( 'userstatus-readonly' ); - } - - return "<script>UserStatus.toShowMode('$buf','$userId');</script>"; - } else { - return $buf."<script>UserStatus.publicHistoryButton('$userId');</script>"; - } - } - } Modified: trunk/extensions/SocialProfile/UserStatus/UserStatus.css =================================================================== --- trunk/extensions/SocialProfile/UserStatus/UserStatus.css 2011-08-09 21:51:22 UTC (rev 94138) +++ trunk/extensions/SocialProfile/UserStatus/UserStatus.css 2011-08-09 23:37:26 UTC (rev 94139) @@ -1,31 +1,31 @@ #status-box { - width : 440px; + width: 440px; padding: 0px; margin: 0px; } #status-box-top { - overflow:visible; + overflow: visible; margin-left: 34px; width: 30px; - height:24px; - background-color:orange; + height: 24px; + background-color: orange; background-image: url('../images/status-corner.png'); } #status-box-content { - background-color:orange; - padding:10px; + background-color: orange; + padding: 10px; border-radius: 22px; /*height: 38px;*/ } -#user-status-block { +#user-status-block, #status-edit-controls { text-align: center; color: white; } -#us-link { +.us-link { color: white; font-size: 8pt; text-decoration: underline; @@ -47,7 +47,7 @@ } #status-letter-count { - float:right; + float: right; margin-right: 20px; font-size: 8pt; } Modified: trunk/extensions/SocialProfile/UserStatus/UserStatus.js =================================================================== --- trunk/extensions/SocialProfile/UserStatus/UserStatus.js 2011-08-09 21:51:22 UTC (rev 94138) +++ trunk/extensions/SocialProfile/UserStatus/UserStatus.js 2011-08-09 23:37:26 UTC (rev 94139) @@ -1,106 +1,115 @@ var UserStatus = { maxStatusLength : 70, - - toShowMode: function( status, id ) { - var textNode = document.createTextNode (this.returnJS(status)); - var textContainer = document.getElementById( 'user-status-block' ); - textContainer.innerHTML = ""; - textContainer.appendChild (textNode); - textContainer.innerHTML += '<br> <a id="us-link" href="javascript:UserStatus.toEditMode(\'' + - status + '\',' + id + ');">'+_US_EDIT+'</a>'; + + /** + * Go back to viewing the status update from the editing mode. + */ + toShowMode: function() { + jQuery( '#status-edit-controls' ).hide(); + jQuery( '#user-status-block' ).show(); }, - - usLettersLeft: function() { - var len = this.maxStatusLength - document.getElementById('user-status-input').value.length; - if ( len < 0 ) { - var usBlock = document.getElementById('user-status-input'); - document.getElementById('user-status-input').value = usBlock.value.slice(0,this.maxStatusLength); + + /** + * Show the "X letters left" message when the user is typing a status + * update. + */ + usLettersLeft: function() { + var len = this.maxStatusLength - document.getElementById( 'user-status-input' ).value.length; + if ( len < 0 ) { + var usBlock = document.getElementById( 'user-status-input' ); + document.getElementById( 'user-status-input' ).value = usBlock.value.slice( 0, this.maxStatusLength ); len++; } - document.getElementById('status-letter-count').innerHTML =len + " "+_US_LETTERS; + document.getElementById( 'status-letter-count' ).innerHTML = len + ' ' + _US_LETTERS; }, - + publicHistoryButton: function( id ) { - document.getElementById( 'user-status-block' ).innerHTML += '<br> <a id="us-link" href="javascript:UserStatus.useHistory(' + id + ');">'+_US_HISTORY+'</a>'; + document.getElementById( 'user-status-block' ).innerHTML += + '<br /> <a id="us-link" href="javascript:UserStatus.useHistory(' + id + ');">' + _US_HISTORY + '</a>'; }, - - toEditMode: function( status, id ) { - var editbar = '<input id="user-status-input" type="text" size="50" value="' + - this.returnJS(status) + '" onkeyup="javascript:UserStatus.usLettersLeft();">'; - editbar += '<br> <div id="status-bar">'; - editbar += '<a id="us-link" href="javascript:UserStatus.saveStatus(' + id + ');">'+_US_SAVE+'</a>'; - editbar += ' <a id="us-link" href="javascript:UserStatus.useHistory(' + id + ');">'+_US_HISTORY+'</a>'; - editbar += ' <a id="us-link" href="javascript:UserStatus.toShowMode(\'' + status + '\',' + id + ');">'+_US_CANCEL+'</a>'; - editbar += '<span id="status-letter-count"></span></div>'; - document.getElementById( 'user-status-block' ).innerHTML = editbar; + + /** + * Enter the edit mode by hiding the current status message and displaying + * the hidden input field which allows the user to enter a new status + * update + some other editing tools. + */ + toEditMode: function() { + jQuery( '#user-status-block' ).hide(); + jQuery( '#status-edit-controls' ).show(); }, - - parseJS:function ( str ) { - var chars = Array( "<", ">", "\"", "'"); - var replacements = Array( "@l;", "@r;", "@dq;", "@q;"); - for (var i=0; i<chars.length; i++) { - var reg = new RegExp(chars[i], "gi"); - if(reg.test(str)) { - str = str.replace(reg, replacements[i]); - } - } - return str; - }, - - returnJS:function ( str ) { - var chars = Array( "<", ">", "\"", "'"); - var replacements = Array( "@l;", "@r;", "@dq;", "@q;"); - for (var i=0; i<chars.length; i++) { - var reg = new RegExp(replacements[i], "gi"); - if(reg.test(str)) { - str = str.replace(reg, chars[i]); - } - } - return str; - }, + /** + * Save a status message into the database. + * + * @param id Integer: user ID number + */ saveStatus: function( id ) { var div = document.getElementById( 'user-status-block' ); - var ustext = document.getElementById( 'user-status-input' ).value; - var ust = this.parseJS(ustext); - sajax_do_call( 'wfSaveStatus', [id, ust], div ); + var ustext = encodeURIComponent( document.getElementById( 'user-status-input' ).value ); + sajax_do_call( 'wfSaveStatus', [id, ustext], function( r ) { + div.innerHTML = r.responseText; + jQuery( '#status-edit-controls' ).hide(); + jQuery( '#user-status-block' ).show(); + }); }, - useHistory: function( id ){ - var historyBlock = document.getElementById('status-history-block'); - if(historyBlock===null) { - var statusBlock = document.getElementById('user-status-block'); - historyBlock = document.createElement('div'); + /** + * Show the user's past status messages when they click on the "history" + * link on their profile. + * + * @param id Integer: user ID number + */ + useHistory: function( id ) { + var historyBlock = document.getElementById( 'status-history-block' ); + if( historyBlock === null ) { + var statusBlock; + if ( document.getElementById( 'status-edit-controls' ) ) { + // This div is present only when you're viewing your own + // profile, it doesn't exist when you're viewing other people's + // profiles + statusBlock = document.getElementById( 'status-edit-controls' ); + } else { + statusBlock = document.getElementById( 'user-status-block' ); + } + historyBlock = document.createElement( 'div' ); historyBlock.id = 'status-history-block'; - statusBlock.appendChild(historyBlock); + statusBlock.appendChild( historyBlock ); sajax_do_call( 'wfGetHistory', [id], historyBlock ); } - - if (historyBlock.style.display == "block") { - historyBlock.style.display = "none"; + + if ( jQuery( '#status-history-block' ).is( 'visible' ) ) { + jQuery( '#status-history-block' ).hide(); } else { - historyBlock.style.display = "block"; + jQuery( '#status-history-block' ).show(); } }, - fromHistoryToStatus: function( str ) { - document.getElementById('user-status-input').value = this.returnJS(str); + /** + * Insert a previously used status message from the history list into the + * editing <input> whenever the user clicks on an archived status message. + * + * @param statusId Integer: status message ID, used in the HTML element as + * an ID + */ + insertStatusFromHistory: function( statusId ) { + document.getElementById( 'user-status-input' ).value = + jQuery( '#status-history-entry-' + statusId ).text(); }, - + specialGetHistory: function() { - var us_name = document.getElementById("us-name-input").value; - var block = document.getElementById("us-special"); + var us_name = document.getElementById( 'us-name-input' ).value; + var block = document.getElementById( 'us-special' ); sajax_do_call( 'SpecialGetStatusByName', [us_name], block ); }, - - specialHistoryDelete: function(id) { - var block = document.getElementById("us-special"); + + specialHistoryDelete: function( id ) { + var block = document.getElementById( 'us-special' ); sajax_do_call( 'SpecialHistoryDelete', [id], block ); this.specialGetHistory(); }, - - specialStatusDelete: function(id) { - var block = document.getElementById("us-special"); + + specialStatusDelete: function( id ) { + var block = document.getElementById( 'us-special' ); sajax_do_call( 'SpecialStatusDelete', [id], block ); this.specialGetHistory(); } Modified: trunk/extensions/SocialProfile/UserStatus/UserStatusClass.php =================================================================== --- trunk/extensions/SocialProfile/UserStatus/UserStatusClass.php 2011-08-09 21:51:22 UTC (rev 94138) +++ trunk/extensions/SocialProfile/UserStatus/UserStatusClass.php 2011-08-09 23:37:26 UTC (rev 94139) @@ -1,7 +1,7 @@ <?php /** * Class to manipulate user-specific status messages. - * + * * @file */ class UserStatusClass { @@ -30,17 +30,21 @@ $message = array( 'us_id' => $row->us_id, 'us_user_id' => $row->us_user_id, - 'us_status' => htmlspecialchars( $row->us_status ), + 'us_status' => $row->us_status, ); } } return $message; } - + public function removeStatus( $status_id ) { - $dbr = wfGetDB( DB_MASTER ); - $dbr->delete('user_status', array( 'us_id' => $status_id ), __METHOD__); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( + 'user_status', + array( 'us_id' => $status_id ), + __METHOD__ + ); return; } @@ -51,8 +55,8 @@ * @param $message String: user-supplied status message */ public function setStatus( $u_id, $message ) { - $message = trim($message); - if (( mb_strlen( $message ) > 90 ) || ( mb_strlen( $message ) < 1 )) { + $message = trim( $message ); + if ( ( mb_strlen( $message ) > 90 ) || ( mb_strlen( $message ) < 1 ) ) { // INFO. Letter limit is 70, but here is 90, for special characters. // ERROR. Message length is too long return; @@ -122,7 +126,7 @@ 'ush_user_id' => $row->ush_user_id, 'ush_timestamp' => $row->ush_timestamp, 'ush_status' => $row->ush_status, - 'ush_likes' => $row->ush_likes, + 'ush_likes' => ( isset( $row->ush_likes ) ? $row->ush_likes : 0 ), ); } @@ -132,7 +136,7 @@ if ( $mode == 'insert' ) { $currentStatus = $this->getStatus( $u_id ); - + if ( $i < 4 ) { $dbw->insert( 'user_status_history', @@ -158,22 +162,14 @@ return; } } - + public function removeHistoryStatus( $status_id ) { - $dbr = wfGetDB( DB_MASTER ); - $dbr->delete('user_status_history', array( 'ush_id' => $status_id ), __METHOD__); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( + 'user_status_history', + array( 'ush_id' => $status_id ), + __METHOD__ + ); return; } - - public function usHTMLcharacters( $str ) { - $regexp = array( "@q;", "@dq;", "@l;", "@r;" ); - $replacement = array ("'", "\"", "<", ">" ); - - $newstr=$str; - for ($i=0; $i<count($regexp);$i++) { - $newstr = str_replace($regexp[$i],$replacement[$i],$newstr); - } - - return $newstr; - } } \ No newline at end of file Modified: trunk/extensions/SocialProfile/UserStatus/UserStatus_AjaxFunctions.php =================================================================== --- trunk/extensions/SocialProfile/UserStatus/UserStatus_AjaxFunctions.php 2011-08-09 21:51:22 UTC (rev 94138) +++ trunk/extensions/SocialProfile/UserStatus/UserStatus_AjaxFunctions.php 2011-08-09 23:37:26 UTC (rev 94139) @@ -5,63 +5,104 @@ $wgAjaxExportList[] = 'wfSaveStatus'; function wfSaveStatus( $u_id, $status ) { - $us_class = new UserStatusClass(); - $us_class->setStatus( $u_id, $status ); - $user_status_array = $us_class->getStatus( $u_id ); - $us = htmlspecialchars($us_class->usHTMLcharacters($user_status_array['us_status'])); - $us .= "<br> <a id=\"us-link\" href=\"javascript:UserStatus.toEditMode('".($user_status_array['us_status'])."','$u_id');\">".wfMsg('userstatus-edit')."</a>"; + global $wgUser; + + $us = ''; + + // Would probably be best to pass an edit token here, like most other MW + // forms do + if ( $u_id == $wgUser->getId() ) { + // Decode what we encoded in JS, UserStatus.saveStatus; this is safe + // because the Database class that UserStatusClass uses for its DB queries + // will do all the escaping for us. + $status = urldecode( $status ); + + $us_class = new UserStatusClass(); + $us_class->setStatus( $u_id, $status ); + + $user_status_array = $us_class->getStatus( $u_id ); + + $us = htmlspecialchars( $user_status_array['us_status'] ); + $us .= '<br /> <a class="us-link" href="javascript:UserStatus.toEditMode();">' . + wfMsg( 'userstatus-edit' ) . '</a>'; + } + return $us; } $wgAjaxExportList[] = 'wfGetHistory'; function wfGetHistory( $u_id ) { + global $wgLang, $wgUser; + $us_class = new UserStatusClass(); - $historyArray = $us_class->useStatusHistory('select', $u_id); - $output='<table id="user-status-history">'; - foreach ($historyArray as $row ) { - $time = DateTime::createFromFormat('Y-m-d H:i:s',$row['ush_timestamp']); - $us = htmlspecialchars($us_class->usHTMLcharacters($row['ush_status'])); - - $output .= '<tr><td width="60" id="status-history-time">'.date_format($time, 'j M G:i').' </td>'; - $output .= '<td width="360"><a href="javascript:UserStatus.fromHistoryToStatus(\''.$us.'\');">' - .$us.'</a></td>'; + $historyArray = $us_class->useStatusHistory( 'select', $u_id ); + + $output = '<table id="user-status-history">'; + + if ( empty( $historyArray ) ) { + $output .= 'No status history.'; + } else { + foreach ( $historyArray as $row ) { + $us = htmlspecialchars( $row['ush_status'] ); + $statusId = intval( $row['ush_id'] ); + + $href = ''; + // We can only *view* other user's past status updates, we cannot + // do anything with them...so don't bother generating the href + // attribute when we're not viewing our own profile + if ( $u_id == $wgUser->getId() ) { + $href = ' href="javascript:UserStatus.insertStatusFromHistory(' . $statusId . + ');"'; + } + + $output .= '<tr> + <td width="60" id="status-history-time">' . + $wgLang->timeanddate( wfTimestamp( TS_MW, $row['ush_timestamp'] ), true ) . + '</td> + <td width="360"> + <a id="status-history-entry-' . $statusId . '"' . $href . '>'. + $us . '</a> + </td> + </tr>'; } - $output.='</table>'; + } + + $output .= '</table>'; + return $output; } $wgAjaxExportList[] = 'SpecialGetStatusByName'; function SpecialGetStatusByName( $user_name ) { - global $wgUser; - $output=""; - $user_id = $wgUser->idFromName($user_name); + $output = ''; + $user_id = User::idFromName( $user_name ); - if (empty ($user_id)) { - $output.="<div>Wrong name or user does not exist</div>"; + if ( empty( $user_id ) ) { + $output .= '<div>Wrong name or user does not exist</div>'; } else { $us_class = new UserStatusClass(); - $currentStatus = $us_class->getStatus($user_id); - $output .="<br><div>USER ID: $user_id, USERNAME: $user_name</div><br>"; + $currentStatus = $us_class->getStatus( $user_id ); + $output .= "<br /><div>USER ID: $user_id, USERNAME: $user_name</div><br>"; - if (!empty ($currentStatus)) { - $output .="CURRENT STATUS:<br> - <input id=\"ush_delete\" type=\"button\" value=\"Delete\" + if ( !empty( $currentStatus ) ) { + $output .="CURRENT STATUS:<br /> + <input id=\"ush_delete\" type=\"button\" value=\"Delete\" onclick=\"javascript:UserStatus.specialStatusDelete('".$currentStatus['us_id']."');\">" - .$currentStatus['us_status']."<br><br>"; + .$currentStatus['us_status'] . '<br /><br />'; } - $output .="HISTORY:<br>"; + $output .= 'HISTORY:<br />'; - $userHistory = $us_class->useStatusHistory('select', $user_id); - if(empty ($userHistory)) { - $output .= "No history"; + $userHistory = $us_class->useStatusHistory( 'select', $user_id ); + if( empty( $userHistory ) ) { + $output .= 'No history'; } else { foreach ( $userHistory as $row ) { - $output .= "<input id=\"ush_delete\" type=\"button\" value=\"Delete\" + $output .= "<input id=\"ush_delete\" type=\"button\" value=\"Delete\" onclick=\"javascript:UserStatus.specialHistoryDelete('".$row['ush_id']."');\">" - .$row['ush_timestamp']." - ".$row['ush_status']." <br>"; + .$row['ush_timestamp']." - ".$row['ush_status']." <br />"; } } } @@ -71,15 +112,15 @@ $wgAjaxExportList[] = 'SpecialHistoryDelete'; function SpecialHistoryDelete( $id ) { $us_class = new UserStatusClass(); - $us_class->removeHistoryStatus($id); - return ""; + $us_class->removeHistoryStatus( $id ); + return ''; } $wgAjaxExportList[] = 'SpecialStatusDelete'; function SpecialStatusDelete( $id ) { $us_class = new UserStatusClass(); - $us_class->removeStatus($id); - return ""; + $us_class->removeStatus( $id ); + return ''; } $wgHooks['MakeGlobalVariablesScript'][] = 'addJSGlobals'; @@ -96,17 +137,83 @@ $wgHooks['UserProfileBeginRight'][] = 'wfUserProfileStatusOutput'; +/** + * Show status updates in user profiles if this feature is enabled. + * + * @param $user_profile UserProfile: instance of UserProfile + * @return Boolean: true + */ function wfUserProfileStatusOutput( $user_profile ) { - global $wgOut , $wgEnableUserStatus; + global $wgOut, $wgUser, $wgEnableUserStatus; + if ( $wgEnableUserStatus ) { - $userStatus = $user_profile->getStatus( $user_profile->user_id ); + $us_class = new UserStatusClass(); + $user_status_array = $us_class->getStatus( $user_profile->user_id ); + + if ( empty( $user_status_array ) ) { + $userStatus = ''; + } else { + $userStatus = $user_status_array['us_status']; + } + + $editLink = ''; + // This is the version of the status message that we can safely use in + // JavaScript + $jsEncodedStatusMsg = Xml::escapeJsString( htmlspecialchars( $userStatus ) ); + + // If the user is viewing their own profile, we can show the "edit" + // link, provided that the user isn't blocked and that the database is + // writable + if ( $user_profile->user_id == $wgUser->getId() ) { + if ( $wgUser->isBlocked() ) { + $userStatus = wfMsg( 'userstatus-blocked' ); + } + + // Database operations require write mode + if ( wfReadOnly() ) { + $userStatus = wfMsg( 'userstatus-readonly' ); + } + + if ( !$wgUser->isBlocked() && !wfReadOnly() ) { + $editLink = '<br /><a class="us-link" href="javascript:UserStatus.toEditMode();">' . + wfMsg( 'userstatus-edit' ) . '</a>'; + } + } + $output = '<div id="status-box"> <div id="status-box-top"></div> <div id="status-box-content"> - <div id="user-status-block">' . $userStatus . '</div> - </div> + <div id="user-status-block">' . + htmlspecialchars( $userStatus ) . $editLink . + '</div>'; + + // No need to show the editing controls to anyone else except the owner + // of the profile + if ( $user_profile->user_id == $wgUser->getId() ) { + $output .= '<div id="status-edit-controls" style="display: none;"> + <input id="user-status-input" type="text" size="50" value="' . + htmlspecialchars( $userStatus ) . + '" onkeyup="javascript:UserStatus.usLettersLeft();" /> + <br /> + <div id="status-bar"> + <a class="us-link" href="javascript:UserStatus.saveStatus(\'' . $user_profile->user_id . '\');">' . + wfMsg( 'userstatus-save' ) . '</a> + <a class="us-link" href="javascript:UserStatus.useHistory(\'' . $user_profile->user_id . '\');">' . + wfMsg( 'userstatus-history' ) . '</a> + <a class="us-link" href="javascript:UserStatus.toShowMode();">' . + wfMsg( 'userstatus-cancel' ) . '</a> + <span id="status-letter-count"></span> + </div> + </div><!-- #status-edit-controls -->'; + } else { + // Public history link to the masses + $output .= "<script>UserStatus.publicHistoryButton('{$user_profile->user_id}');</script>"; + } + + $output .= '</div> </div>'; - $wgOut->addHTML($output); + $wgOut->addHTML( $output ); } + return true; } \ No newline at end of file _______________________________________________ MediaWiki-CVS mailing list MediaWiki-CVS@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs