// This class handles all content: editing, inserting, and deleting.
require_once("common.php");
require_once("loggedinuser.php");
class ContentHandler {
// Return values for certain methods...
public static $CONTENT_POSTED = 1;
public static $CONTENT_QUEUED = 2;
public static $QUEUE_ENTRY_NEW = 0;
public static $QUEUE_ENTRY_EDIT = 1;
public static $CONTENT_NO_CHANGE = 3;
public static $CONTENT_FAILURE = 4;
public static $CONTENT_SUCCESS = 5;
// Regenerates an index for a particular language. Indices need to be
// rebuilt when a content file is added or removed so the UI properly
// reflects the change.
public static function regenerateIndex($lid, $langName) {
global $CMS_CONTENT_FS_PATH;
global $CMS_CONTENT_URL_PATH;
global $INDEX_FILE_HEADER;
global $INDEX_FILE_FOOTER;
$indexFilename = $CMS_CONTENT_FS_PATH . $langName . "/index.html";
$contents = "";
$db = getDBConnection();
$result = mysql_query("SELECT path FROM History WHERE type=0 AND LID=" . $lid, $db);
while($row = mysql_fetch_row($result)) {
$path = $row[ 0 ];
$contents = $contents . "" . getTitleFromFile($path) . "
";
}
mysql_close($db);
$hIndexFile = fopen($indexFilename, "w");
if ($hIndexFile == FALSE) {
die("Error while updating index!: " . $indexFilename);
}
fwrite($hIndexFile, $INDEX_FILE_HEADER);
fwrite($hIndexFile, $contents);
fwrite($hIndexFile, $INDEX_FILE_FOOTER);
fclose($hIndexFile);
}
// Attempts to delete a content file. Only the global admin or high-level
// language administrator can successfully do this. All other users will
// be denied.
public static function deleteFile($loggedInUser, $file) {
global $ACL_ACCESS_ADMIN_HIGH;
global $CMS_CONTENT_FS_PATH;
global $RM_PATH;
require_once("languages.php");
// Make sure nobody is trying something funny here. Relative paths are
// rejected outright.
$slashPos = strpos($file, "/");
if (($slashPos == FALSE) || (strpos($file, "..") !== FALSE)) {
die("Invalid file argument: $file");
}
$langName = substr($file, 0, $slashPos);
$lid = Language::lookupLID($langName);
$accessLevel = $loggedInUser->getAccessLevel($lid);
if (($accessLevel == $ACL_ACCESS_ADMIN_HIGH) || isAdminUser()) {
// Do nothing.
} else {
die("Sorry, you do not have sufficient rights to delete this file.");
}
// Delete any changes in the PendingQueue that refer to this file.
$db = getDBConnection();
$result = mysql_query("DELETE FROM PendingQueue WHERE path='" . $file . "'", $db);
if (!$result)
die("Error while deleting file!");
// Delete all the history associated with this file.
$result = mysql_query("DELETE FROM History WHERE path='" . $file . "'", $db);
if (!$result)
die("Error while deleting file!");
// Delete the file from the filesystem.
exec($RM_PATH . " -rf " . $CMS_CONTENT_FS_PATH . $file);
// Regenerate the index so that the now-broken link is gone.
ContentHandler::regenerateIndex($lid, $langName);
}
// Submit new content. Anonymous users, unprivileged registered users, and
// low-level administrators' content will be queued. High-level
// administrators will have their content posted instantly.
public static function submitNew($loggedInUser, $ip, $title, $lid, $content){
global $ACL_ACCESS_NOBODY;
global $ACL_ACCESS_NOBODY_VERIFIED;
global $ACL_ACCESS_ADMIN_LOW;
global $ACL_ACCESS_ADMIN_HIGH;
global $CMS_CONTENT_PATH;
$retVal = -1;
$accessLevel = $ACL_ACCESS_NOBODY;
if (isset($loggedInUser) && ($loggedInUser != null)) {
$accessLevel = $loggedInUser->getAccessLevel($lid);
}
if ($accessLevel == $ACL_ACCESS_ADMIN_HIGH) {
$file = Language::lookupName($lid) . '/' . $title . '.html';
if (ContentHandler::writeNewFile($loggedInUser,$lid,$file,stripslashes($content)) &&
ContentHandler::addHistory($file,
$loggedInUser->getUID(), $lid,
$content,ContentHandler::$QUEUE_ENTRY_NEW,
$loggedInUser)) {
$retVal = ContentHandler::$CONTENT_POSTED;
} else
$retVal = ContentHandler::$CONTENT_FAILURE;
} else {
$file = Language::lookupName($lid) . '/' . $title . '.html';
if (ContentHandler::addQueue($lid, $file, $content,
ContentHandler::$QUEUE_ENTRY_NEW,
$loggedInUser, $ip))
$retVal = ContentHandler::$CONTENT_QUEUED;
else
$retVal = ContentHandler::$CONTENT_FAILURE;
}
return $retVal;
}
// Submit a change to a content file. Anonymous and unprivileged registered
// users will have their changes queued. Low- and high-level admins will
// have their content posted instantly.
public static function submitEdit($loggedInUser, $ip, $file, $content) {
require_once("languages.php");
global $ACL_ACCESS_NOBODY;
global $ACL_ACCESS_NOBODY_VERIFIED;
global $ACL_ACCESS_ADMIN_LOW;
global $ACL_ACCESS_ADMIN_HIGH;
$retVal = -1;
// if (substr($content, strlen($content) - 1, 1) != "\n")
// $content .= "\n";
$diff = ContentHandler::makeDiff($file, stripslashes($content));
if ($diff == null)
return ContentHandler::$CONTENT_NO_CHANGE;
$langName = substr($file, 0, strpos($file, "/"));
$lid = Language::lookupLID($langName);
$accessLevel = $ACL_ACCESS_NOBODY;
if (isset($loggedInUser) && ($loggedInUser != null)) {
$accessLevel = $loggedInUser->getAccessLevel($lid);
}
// echo "Access level: $accessLevel
langid: $lid
";
if (($accessLevel == $ACL_ACCESS_ADMIN_LOW) ||
($accessLevel == $ACL_ACCESS_ADMIN_HIGH)) {
if (ContentHandler::applyDiff($file, $diff)) {
if (ContentHandler::addHistory($file, $loggedInUser->getUID(), $lid,
$diff,
ContentHandler::$QUEUE_ENTRY_EDIT,
$loggedInUser)) {
$retVal = ContentHandler::$CONTENT_POSTED;
} else {
die("error adding history");
$retVal = ContentHandler::$CONTENT_FAILURE;
}
} else {
die("error while applying diff.");
$retVal = ContentHandler::$CONTENT_FAILURE;
}
} else {
if (ContentHandler::addQueue($lid, $file, $diff,
ContentHandler::$QUEUE_ENTRY_EDIT,
$loggedInUser, $ip))
$retVal = ContentHandler::$CONTENT_QUEUED;
else
$retVal = ContentHandler::$CONTENT_FAILURE;
}
return $retVal;
}
// Approves a specific entry in the PendingQueue.
public static function approveQueueEntry($pid, $loggedInUser){
global $CMS_CONTENT_PATH;
settype($pid, "integer");
$retVal = ContentHandler::$CONTENT_FAILURE;
$db = getDBConnection();
$result = mysql_query("SELECT UID,LID,path,type,data FROM PendingQueue WHERE PID=" . $pid, $db);
if (!$result)
die("Error while approving queue entry!");
$row = mysql_fetch_row($result);
$uid = $row[ 0 ];
$lid = $row[ 1 ];
$path = $row[ 2 ];
$type = $row[ 3 ];
$data = addslashes($row[ 4 ]);
settype($uid, "integer");
settype($lid, "integer");
settype($type, "integer");
$accessLevel = $loggedInUser->getAccessLevel($lid);
if ($type == 0) {
if ($accessLevel != ACLs::$ACL_ACCESS_ADMIN_HIGH)
die("Error: You have insufficient privileges to approve new submissions!");
if (ContentHandler::writeNewFile($loggedInUser, $lid, $path, stripslashes($data)) &&
ContentHandler::addHistory($path,$uid,$lid,$data,$type,$loggedInUser) &&
ContentHandler::delQueue($pid)) {
$retVal = ContentHandler::$CONTENT_POSTED;
} else
$retVal = ContentHandler::$CONTENT_FAILURE;
} else if ($type == 1) {
if ($accessLevel < ACLs::$ACL_ACCESS_ADMIN_LOW)
die("Error: You have insufficient privileges to approve new submissions!");
$db = getDBConnection();
$result = mysql_query("SELECT UID,path,data FROM PendingQueue WHERE PID=" . $pid, $db);
if (!$result)
die("Error while retrieving from pending queue!: " . $pid);
$row = mysql_fetch_row($result);
$uid = $row[ 0 ];
$path = $row[ 1 ];
$diff = $row[ 2 ];
mysql_close($db);
if (ContentHandler::applyDiff($path, $diff) &&
ContentHandler::addHistory($path, $uid, $lid,
$diff,
ContentHandler::$QUEUE_ENTRY_EDIT,
$loggedInUser) &&
ContentHandler::delQueue($pid)) {
$retVal = ContentHandler::$CONTENT_POSTED;
} else
$retVal = ContentHandler::$CONTENT_FAILURE;
} else
die("Internal error!");
return $retVal;
}
// Rejects a queue entry.
// TODO: check the access level of the user!!!
public static function rejectQueueEntry($pid) {
if (ContentHandler::delQueue($pid))
return ContentHandler::$CONTENT_SUCCESS;
else
return ContentHandler::$CONTENT_FAILURE;
}
// Gets an unused PID for the PendingQueue. Current implementation can
// be greatly improved.
private static function getUnusedPID($db) {
$needToClose = false;
if ($db == null) {
$db = getDBConnection();
$needToClose = true;
}
$result = mysql_query("SELECT MAX(PID) FROM PendingQueue", $db);
if (!$result) {
die('Error getting unused PID: ' . mysql_error());
}
$retVal = mysql_result($result, 0);
settype($retVal, "integer");
$retVal++;
mysql_free_result($result);
if ($needToClose)
mysql_close($db);
return $retVal;
}
// Obtains an unused HID for the History table. Current implementation can
// be greatly improved.
private static function getUnusedHID($db) {
$needToClose = false;
if ($db == null) {
$db = getDBConnection();
$needToClose = true;
}
$result = mysql_query("SELECT MAX(HID) FROM History", $db);
if (!$result) {
die('Error getting unused HID: ' . mysql_error());
}
$retVal = mysql_result($result, 0);
settype($retVal, "integer");
$retVal++;
mysql_free_result($result);
if ($needToClose)
mysql_close($db);
return $retVal;
}
// Adds a content file to an index. Index will be updated to reflect the
// new file.
private static function addFileToIndex($loggedInUser, $langName,
$relativePath, $title) {
global $CMS_CONTENT_FS_PATH;
global $CMS_CONTENT_URL_PATH;
global $INDEX_FILE_HEADER;
global $INDEX_FILE_FOOTER;
$indexFilename = $CMS_CONTENT_FS_PATH . $langName . "/index.html";
$contents = file_get_contents($indexFilename);
$contents = extractBetweenTags($contents,
"", "");
$contents = $contents . "$title
";
$hIndexFile = fopen($indexFilename, "w");
if ($hIndexFile == FALSE) {
die("Error while updating index!: " . $hIndexFile);
}
fwrite($hIndexFile, $INDEX_FILE_HEADER);
fwrite($hIndexFile, $contents);
fwrite($hIndexFile, $INDEX_FILE_FOOTER);
fclose($hIndexFile);
}
// Creates a new content file on the filesystem. Caller must ensure that
// the user has proper access to do this.
private static function writeNewFile($loggedInUser, $lid, $file, $data) {
global $CMS_CONTENT_FS_PATH;
global $FILE_MODE;
global $CONTENT_HEADER;
global $CONTENT_FOOTER;
$title = getTitleFromFile($file);
#$title = normalizeString($title);
$langName = normalizeString(Language::lookupName($lid));
#$relativePath = $langName . '/' . $title . '.html';
$filename = $CMS_CONTENT_FS_PATH . $file;
if (!($hFile = fopen($filename, "w+")))
die("Error while creating new file " . $filename);
$footer = str_replace("%EDIT_URL%", $file, $CONTENT_FOOTER);
if (fwrite($hFile, $CONTENT_HEADER . htmlize($data) . $footer) === FALSE)
die("Error while writing to file " . $filename);
fclose($hFile);
$oldumask = umask(0000);
if (!chmod($filename, $FILE_MODE))
die("Error while chmod'ing file " . $filename);
umask($oldumask);
ContentHandler::addFileToIndex($loggedInUser, $langName, $file, $title);
return true;
}
// Creates a diff from a filename of original content and the text of
// something new. The text of that diff is returned.
private static function makeDiff($originalFileName, $newContent) {
global $CMS_CONTENT_FS_PATH;
global $DIFF_PATH;
$retVal = null;
$tempFileOld = ContentHandler::extractContent($originalFileName);
$tempFileNew = ContentHandler::tempFile($newContent);
$tempFileDiff = ContentHandler::tempFile("");
$command = $DIFF_PATH . ' -u ' . $tempFileOld . ' ' . $tempFileNew .
" > " . $tempFileDiff;
$diff = array();
$diffRetVal = null;
// echo "Executing: " . $command . "
";
exec($command, $diff, $diffRetVal);
// if ($diffRetVal == 1) {
// $retVal = '';
// for($i = 0; $i < count($diff); $i++) {
// $retVal .= ($diff[ $i ] . "\n");
// }
// }
$hDiffFile = fopen($tempFileDiff, "r");
$retVal = fread($hDiffFile, filesize($tempFileDiff));
fclose($hDiffFile);
if (unlink($tempFileOld) === false)
die("Error while deleting temporary file: " . $tempFileOld);
if (unlink($tempFileNew) === false)
die("Error while deleting temporary file: " . $tempFileNew);
if (unlink($tempFileDiff) === false)
die("Error while deleting temporary file: " . $tempFileDiff);
// echo 'Diff:
' . $retVal . '
';
return $retVal;
}
// Creates a temporary file to hold the specified data. That temporary
// file's name is returned.
private static function tempFile($data) {
global $TEMP_DIR;
$oldumask = umask(0077);
$randString = mt_rand();
settype($randString, "string");
$filename = $TEMP_DIR . "litcms" . $randString;
if (!($hFile = fopen($filename, "w+")))
die("Error while creating temporary file!");
fwrite($hFile, $data);
fclose($hFile);
umask($oldumask);
return $filename;
}
// Applies a diff to a specified filename. Returns true if successful,
// or false if otherwise (Uh-oh!! This probably means a collision occurred
// which definitely shouldn't be happening!!!)
private static function applyDiff($originalFileName, $diff) {
global $PATCH_PATH;
global $CMS_CONTENT_FS_PATH;
global $CONTENT_HEADER;
global $CONTENT_FOOTER;
// TODO: make backup of file before overwriting!
$contentTempFile = ContentHandler::extractContent($originalFileName);
//ContentHandler::dumpFile($contentTempFile);
$diffTempFile = ContentHandler::tempFile($diff);
//ContentHandler::dumpFile($diffTempFile);
//die("X");
$command = $PATCH_PATH . ' -f -l ' .
$contentTempFile . ' < ' . $diffTempFile;
$patchResult = array();
$patchRetVal = -1;
exec($command, $patchResult, $patchRetVal);
// if (unlink($diffTempFile) === false)
// my_log("Error while deleting temporary file: " . $diffTempFile);
if ($patchRetVal == 0) {
$hFile = null;
if (!($hFile = fopen($contentTempFile, "r")))
die("Error while reading temporary content file: " . $contentTempFile);
$newContent = fread($hFile, filesize($contentTempFile));
fclose($hFile);
// if (unlink($contentTempFile) === false)
// my_log("Error while deleting temporary file: " . $contentTempFile);
if (!($hFile = fopen($CMS_CONTENT_FS_PATH . $originalFileName, "w+"))) {
my_log("Error while applying the following diff file to $originalFileName:\n\n$diff\n\n");
} else {
$newContent = $CONTENT_HEADER . htmlize($newContent) .
str_replace("%EDIT_URL%", $originalFileName, $CONTENT_FOOTER);
fwrite($hFile, $newContent);
fclose($hFile);
$retVal = true;
}
} else {
my_log("Error while applying the following diff file to $originalFileName:\n\n$diff\n\nPatch result: " . $patchResult);
if (unlink($contentTempFile) === false)
my_log("Error while deleting temporary file: " . $contentTempFile);
$retVal = false;
}
return $retVal;
}
// Extracts the content from a content file (the header and footer data
// needs to be stripped--that's what this does). The text of the content
// is returned.
public static function extractContent($file) {
global $CMS_CONTENT_FS_PATH;
$hFile = fopen($CMS_CONTENT_FS_PATH . $file, "r");
$content = fread($hFile, filesize($CMS_CONTENT_FS_PATH . $file));
fclose($hFile);
$beginPos = strpos($content, "");
$endPos = strpos($content, "");
if (($beginPos === false) || ($endPos === false)) {
die("Cannot read file " . $file);
} else
$beginPos += 19;
$contentLen = $endPos - $beginPos;
$content = dehtmlize(substr($content, $beginPos, $contentLen));
$tempFileName = ContentHandler::tempFile($content);
return $tempFileName;
}
// Adds a row to the History table.
private static function addHistory($path, $uid, $lid, $data, $type,
$loggedInUser) {
global $TEMP_DIR;
if ($type == 1) {
$data = str_replace($TEMP_DIR, "/", $data);
}
$retVal = false;
$db = getDBConnection();
$unusedHID = ContentHandler::getUnusedHID($db);
$approvedbyUID = $loggedInUser->getUID();
$query = "INSERT INTO History VALUES($unusedHID,'$path',NOW(),$uid,$lid,'$data',$type,$approvedbyUID)";
$result = mysql_query($query, $db);
if ($result)
$retVal = true;
else
my_log("Error while executing: " . $query);
mysql_close($db);
return $retVal;
}
// Adds an entry to the PendingQueue table.
private static function addQueue($lid, $file, $content, $type,
$loggedInUser, $ip) {
$retVal = false;
$db = getDBConnection();
$uid = -1;
if ($loggedInUser != null)
$uid = $loggedInUser->getUID();
$query = "INSERT INTO PendingQueue VALUES(" . ContentHandler::getUnusedPID($db) . "," . $uid . "," . $lid . ",NOW(),'" . $ip . "','" . $file . "',$type,'" . $content . "')";
$result = mysql_query($query, $db);
if ($result)
$retVal = true;
else
my_log("Error while executing: " . $query);
mysql_close($db);
return $retVal;
}
// Deletes an entry from the PendingQueue. Caller is responsible for
// ensuring user has access to do this.
private static function delQueue($pid) {
settype($pid, "integer");
$retVal = false;
$db = getDBConnection();
$result = mysql_query("DELETE FROM PendingQueue WHERE PID=" . $pid, $db);
if (!$result || (mysql_affected_rows($db) != 1)) {
$retVal = false;
my_log("Error while deleting from pending queue!: " . $pid);
} else
$retVal = true;
mysql_close($db);
return $retVal;
}
// Creates a temporary file, modifies the argument variable to hold its
// filename, and returns an open handle to that file.
private static function makeTempFile(&$filename) {
global $TEMP_DIR;
$randString = mt_rand();
settype($randString, "string");
$filename = $TEMP_DIR . "litcms" . $randString;
if (!($hFile = fopen($filename, "w+")))
die("Error while creating temporary file!");
return $hFile;
}
// Applies a patch to a content file without permanently modifying it; the
// new content is returned as a string.
public static function applyPatchTemporary($originalTempFile, $diff) {
global $PATCH_PATH;
$diff_filename = "";
$hDiffFile = ContentHandler::makeTempFile($diff_filename);
fwrite($hDiffFile, $diff);
fclose($hDiffFile);
exec($PATCH_PATH . " " . $originalTempFile . " < " . $diff_filename);
clearstatcache();
$hFile = fopen($originalTempFile, "r");
$retval = fread($hFile, filesize($originalTempFile));
fclose($hFile);
unlink($diff_filename);
return $retval;
}
// For debugging only.
public static function dumpFile($filename) {
$hFile = fopen($filename, "r");
if ($hFile === FALSE) {
die("dumpFile: can't open $filename.");
}
$stuff = fread($hFile, filesize($filename));
fclose($hFile);
print "
DUMPING [$filename]: [$stuff]
";
}
}
?>