913
hesk/inc/admin_functions.inc.php
Normal file
913
hesk/inc/admin_functions.inc.php
Normal file
@@ -0,0 +1,913 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
// Possible fields to be displayed in ticket list
|
||||
$hesk_settings['possible_ticket_list'] = array(
|
||||
'id' => $hesklang['id'],
|
||||
'trackid' => $hesklang['trackID'],
|
||||
'dt' => $hesklang['submitted'],
|
||||
'lastchange' => $hesklang['last_update'],
|
||||
'category' => $hesklang['category'],
|
||||
'name' => $hesklang['name'],
|
||||
'email' => $hesklang['email'],
|
||||
'subject' => $hesklang['subject'],
|
||||
'status' => $hesklang['status'],
|
||||
'owner' => $hesklang['owner'],
|
||||
'replies' => $hesklang['replies'],
|
||||
'staffreplies' => $hesklang['replies'] . ' (' . $hesklang['staff'] .')',
|
||||
'lastreplier' => $hesklang['last_replier'],
|
||||
'time_worked' => $hesklang['ts'],
|
||||
);
|
||||
|
||||
define('HESK_NO_ROBOTS', true);
|
||||
|
||||
/*** FUNCTIONS ***/
|
||||
|
||||
|
||||
function hesk_show_column($column)
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
return in_array($column, $hesk_settings['ticket_list']) ? true : false;
|
||||
|
||||
} // END hesk_show_column()
|
||||
|
||||
|
||||
function hesk_getHHMMSS($in)
|
||||
{
|
||||
$in = hesk_getTime($in);
|
||||
return explode(':', $in);
|
||||
} // END hesk_getHHMMSS();
|
||||
|
||||
|
||||
function hesk_getTime($in)
|
||||
{
|
||||
$in = trim($in);
|
||||
|
||||
/* If everything is OK this simple check should return true */
|
||||
if ( preg_match('/^([0-9]{2,3}):([0-5][0-9]):([0-5][0-9])$/', $in) )
|
||||
{
|
||||
return $in;
|
||||
}
|
||||
|
||||
/* No joy, let's try to figure out the correct values to use... */
|
||||
$h = 0;
|
||||
$m = 0;
|
||||
$s = 0;
|
||||
|
||||
/* How many parts do we have? */
|
||||
$parts = substr_count($in, ':');
|
||||
|
||||
switch ($parts)
|
||||
{
|
||||
/* Only two parts, let's assume minutes and seconds */
|
||||
case 1:
|
||||
list($m, $s) = explode(':', $in);
|
||||
break;
|
||||
|
||||
/* Three parts, so explode to hours, minutes and seconds */
|
||||
case 2:
|
||||
list($h, $m, $s) = explode(':', $in);
|
||||
break;
|
||||
|
||||
/* Something other was entered, let's assume just minutes */
|
||||
default:
|
||||
$m = $in;
|
||||
}
|
||||
|
||||
/* Make sure all inputs are integers */
|
||||
$h = intval($h);
|
||||
$m = intval($m);
|
||||
$s = intval($s);
|
||||
|
||||
/* Convert seconds to minutes if 60 or more seconds */
|
||||
if ($s > 59)
|
||||
{
|
||||
$m = floor($s / 60) + $m;
|
||||
$s = intval($s % 60);
|
||||
}
|
||||
|
||||
/* Convert minutes to hours if 60 or more minutes */
|
||||
if ($m > 59)
|
||||
{
|
||||
$h = floor($m / 60) + $h;
|
||||
$m = intval($m % 60);
|
||||
}
|
||||
|
||||
/* MySQL accepts max time value of 838:59:59 */
|
||||
if ($h > 838)
|
||||
{
|
||||
return '838:59:59';
|
||||
}
|
||||
|
||||
/* That's it, let's send out formatted time string */
|
||||
return str_pad($h, 2, "0", STR_PAD_LEFT) . ':' . str_pad($m, 2, "0", STR_PAD_LEFT) . ':' . str_pad($s, 2, "0", STR_PAD_LEFT);
|
||||
|
||||
} // END hesk_getTime();
|
||||
|
||||
|
||||
function hesk_mergeTickets($merge_these, $merge_into)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $hesk_db_link;
|
||||
|
||||
/* Target ticket must not be in the "merge these" list */
|
||||
if ( in_array($merge_into, $merge_these) )
|
||||
{
|
||||
$merge_these = array_diff($merge_these, array( $merge_into ) );
|
||||
}
|
||||
|
||||
/* At least 1 ticket needs to be merged with target ticket */
|
||||
if ( count($merge_these) < 1 )
|
||||
{
|
||||
$_SESSION['error'] = $hesklang['merr1'];
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Make sure target ticket exists */
|
||||
$res = hesk_dbQuery("SELECT `id`,`trackid`,`category` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."tickets` WHERE `id`='".intval($merge_into)."' LIMIT 1");
|
||||
if (hesk_dbNumRows($res) != 1)
|
||||
{
|
||||
$_SESSION['error'] = $hesklang['merr2'];
|
||||
return false;
|
||||
}
|
||||
$ticket = hesk_dbFetchAssoc($res);
|
||||
|
||||
/* Make sure user has access to ticket category */
|
||||
if ( ! hesk_okCategory($ticket['category'], 0) )
|
||||
{
|
||||
$_SESSION['error'] = $hesklang['merr3'];
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Set some variables for later */
|
||||
$merge['attachments'] = '';
|
||||
$merge['replies'] = array();
|
||||
$merge['notes'] = array();
|
||||
$sec_worked = 0;
|
||||
$history = '';
|
||||
$merged = '';
|
||||
|
||||
/* Get messages, replies, notes and attachments of tickets that will be merged */
|
||||
foreach ($merge_these as $this_id)
|
||||
{
|
||||
/* Validate ID */
|
||||
if ( is_array($this_id) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$this_id = intval($this_id) or hesk_error($hesklang['id_not_valid']);
|
||||
|
||||
/* Get required ticket information */
|
||||
$res = hesk_dbQuery("SELECT `id`,`trackid`,`category`,`name`,`message`,`dt`,`time_worked`,`attachments` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."tickets` WHERE `id`='".intval($this_id)."' LIMIT 1");
|
||||
if (hesk_dbNumRows($res) != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$row = hesk_dbFetchAssoc($res);
|
||||
|
||||
/* Has this user access to the ticket category? */
|
||||
if ( ! hesk_okCategory($row['category'], 0) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Insert ticket message as a new reply to target ticket */
|
||||
hesk_dbQuery("INSERT INTO `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` (`replyto`,`name`,`message`,`dt`,`attachments`) VALUES ('".intval($ticket['id'])."','".hesk_dbEscape($row['name'])."','".hesk_dbEscape($row['message'])."','".hesk_dbEscape($row['dt'])."','".hesk_dbEscape($row['attachments'])."')");
|
||||
|
||||
/* Update attachments */
|
||||
hesk_dbQuery("UPDATE `".hesk_dbEscape($hesk_settings['db_pfix'])."attachments` SET `ticket_id`='".hesk_dbEscape($ticket['trackid'])."' WHERE `ticket_id`='".hesk_dbEscape($row['trackid'])."'");
|
||||
|
||||
/* Get old ticket replies and insert them as new replies */
|
||||
$res = hesk_dbQuery("SELECT * FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` WHERE `replyto`='".intval($row['id'])."' ORDER BY `id` ASC");
|
||||
while ( $reply = hesk_dbFetchAssoc($res) )
|
||||
{
|
||||
hesk_dbQuery("INSERT INTO `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` (`replyto`,`name`,`message`,`dt`,`attachments`,`staffid`,`rating`,`read`) VALUES ('".intval($ticket['id'])."','".hesk_dbEscape($reply['name'])."','".hesk_dbEscape($reply['message'])."','".hesk_dbEscape($reply['dt'])."','".hesk_dbEscape($reply['attachments'])."','".intval($reply['staffid'])."','".intval($reply['rating'])."','".intval($reply['read'])."')");
|
||||
}
|
||||
|
||||
/* Delete replies to the old ticket */
|
||||
hesk_dbQuery("DELETE FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` WHERE `replyto`='".intval($row['id'])."'");
|
||||
|
||||
/* Get old ticket notes and insert them as new notes */
|
||||
$res = hesk_dbQuery("SELECT * FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."notes` WHERE `ticket`='".intval($row['id'])."' ORDER BY `id` ASC");
|
||||
while ( $note = hesk_dbFetchAssoc($res) )
|
||||
{
|
||||
hesk_dbQuery("INSERT INTO `".hesk_dbEscape($hesk_settings['db_pfix'])."notes` (`ticket`,`who`,`dt`,`message`,`attachments`) VALUES ('".intval($ticket['id'])."','".intval($note['who'])."','".hesk_dbEscape($note['dt'])."','".hesk_dbEscape($note['message'])."','".hesk_dbEscape($note['attachments'])."')");
|
||||
}
|
||||
|
||||
/* Delete replies to the old ticket */
|
||||
hesk_dbQuery("DELETE FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."notes` WHERE `ticket`='".intval($row['id'])."'");
|
||||
|
||||
/* Delete old ticket */
|
||||
hesk_dbQuery("DELETE FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."tickets` WHERE `id`='".intval($row['id'])."'");
|
||||
|
||||
/* Log that ticket has been merged */
|
||||
$history .= sprintf($hesklang['thist13'],hesk_date(),$row['trackid'],$_SESSION['name'].' ('.$_SESSION['user'].')');
|
||||
|
||||
/* Add old ticket ID to target ticket "merged" field */
|
||||
$merged .= '#' . $row['trackid'];
|
||||
|
||||
/* Convert old ticket "time worked" to seconds and add to $sec_worked variable */
|
||||
list ($hr, $min, $sec) = explode(':', $row['time_worked']);
|
||||
$sec_worked += (((int)$hr) * 3600) + (((int)$min) * 60) + ((int)$sec);
|
||||
}
|
||||
|
||||
/* Convert seconds to HHH:MM:SS */
|
||||
$sec_worked = hesk_getTime('0:'.$sec_worked);
|
||||
|
||||
// Get number of replies
|
||||
$total = 0;
|
||||
$staffreplies = 0;
|
||||
|
||||
$res = hesk_dbQuery("SELECT COUNT(*) as `cnt`, (CASE WHEN `staffid` = 0 THEN 0 ELSE 1 END) AS `staffcnt` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` WHERE `replyto`=".intval($ticket['id'])." GROUP BY `staffcnt`");
|
||||
while ( $row = hesk_dbFetchAssoc($res) )
|
||||
{
|
||||
$total += $row['cnt'];
|
||||
$staffreplies += ($row['staffcnt'] ? $row['cnt'] : 0);
|
||||
}
|
||||
|
||||
$replies_sql = " `replies`={$total}, `staffreplies`={$staffreplies} , ";
|
||||
|
||||
// Get first staff reply
|
||||
if ($staffreplies)
|
||||
{
|
||||
$res = hesk_dbQuery("SELECT `dt`, `staffid` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."replies` WHERE `replyto`=".intval($ticket['id'])." AND `staffid`>0 ORDER BY `dt` ASC LIMIT 1");
|
||||
$reply = hesk_dbFetchAssoc($res);
|
||||
$replies_sql .= " `firstreply`='".hesk_dbEscape($reply['dt'])."', `firstreplyby`=".intval($reply['staffid'])." , ";
|
||||
}
|
||||
|
||||
/* Update history (log) and merged IDs of target ticket */
|
||||
hesk_dbQuery("UPDATE `".hesk_dbEscape($hesk_settings['db_pfix'])."tickets` SET $replies_sql `time_worked`=ADDTIME(`time_worked`, '".hesk_dbEscape($sec_worked)."'), `merged`=CONCAT(`merged`,'".hesk_dbEscape($merged . '#')."'), `history`=CONCAT(`history`,'".hesk_dbEscape($history)."') WHERE `id`='".intval($merge_into)."'");
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_mergeTickets()
|
||||
|
||||
|
||||
function hesk_updateStaffDefaults()
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Remove the part that forces saving as default - we don't need it every time
|
||||
$default_list = str_replace('&def=1','',$_SERVER['QUERY_STRING']);
|
||||
|
||||
// Update database
|
||||
$res = hesk_dbQuery("UPDATE `".hesk_dbEscape($hesk_settings['db_pfix'])."users` SET `default_list`='".hesk_dbEscape($default_list)."' WHERE `id`='".intval($_SESSION['id'])."'");
|
||||
|
||||
// Update session values so the changes take effect immediately
|
||||
$_SESSION['default_list'] = $default_list;
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_updateStaffDefaults()
|
||||
|
||||
|
||||
function hesk_makeJsString($in)
|
||||
{
|
||||
return addslashes(preg_replace("/\s+/",' ',$in));
|
||||
} // END hesk_makeJsString()
|
||||
|
||||
|
||||
function hesk_checkNewMail()
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
$res = hesk_dbQuery("SELECT COUNT(*) FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."mail` WHERE `to`='".intval($_SESSION['id'])."' AND `read`='0' AND `deletedby`!='".intval($_SESSION['id'])."' ");
|
||||
$num = hesk_dbResult($res,0,0);
|
||||
|
||||
return $num;
|
||||
} // END hesk_checkNewMail()
|
||||
|
||||
|
||||
function hesk_dateToString($dt, $returnName=1, $returnTime=0, $returnMonth=0, $from_database=false)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
$dt = strtotime($dt);
|
||||
|
||||
// Adjust MySQL time if different from PHP time
|
||||
if ($from_database)
|
||||
{
|
||||
if ( ! defined('MYSQL_TIME_DIFF') )
|
||||
{
|
||||
define('MYSQL_TIME_DIFF', time()-hesk_dbTime() );
|
||||
}
|
||||
|
||||
if (MYSQL_TIME_DIFF != 0)
|
||||
{
|
||||
$dt += MYSQL_TIME_DIFF;
|
||||
}
|
||||
}
|
||||
|
||||
list($y,$m,$n,$d,$G,$i,$s) = explode('-', date('Y-n-j-w-G-i-s', $dt) );
|
||||
|
||||
$m = $hesklang['m'.$m];
|
||||
$d = $hesklang['d'.$d];
|
||||
|
||||
if ($returnName)
|
||||
{
|
||||
return "$d, $m $n, $y";
|
||||
}
|
||||
|
||||
if ($returnTime)
|
||||
{
|
||||
return "$d, $m $n, $y $G:$i:$s";
|
||||
}
|
||||
|
||||
if ($returnMonth)
|
||||
{
|
||||
return "$m $y";
|
||||
}
|
||||
|
||||
return "$m $n, $y";
|
||||
} // End hesk_dateToString()
|
||||
|
||||
|
||||
function hesk_getCategoriesArray($kb = 0) {
|
||||
global $hesk_settings, $hesklang, $hesk_db_link;
|
||||
|
||||
$categories = array();
|
||||
if ($kb)
|
||||
{
|
||||
$result = hesk_dbQuery('SELECT `id`, `name` FROM `'.hesk_dbEscape($hesk_settings['db_pfix']).'kb_categories` ORDER BY `cat_order` ASC');
|
||||
}
|
||||
else
|
||||
{
|
||||
$result = hesk_dbQuery('SELECT `id`, `name` FROM `'.hesk_dbEscape($hesk_settings['db_pfix']).'categories` ORDER BY `cat_order` ASC');
|
||||
}
|
||||
|
||||
while ($row=hesk_dbFetchAssoc($result))
|
||||
{
|
||||
$categories[$row['id']] = $row['name'];
|
||||
}
|
||||
|
||||
return $categories;
|
||||
} // END hesk_getCategoriesArray()
|
||||
|
||||
|
||||
function hesk_getHTML($in)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
$replace_from = array("\t","<?","?>","$","<%","%>");
|
||||
$replace_to = array("","<?","?>","\$","<%","%>");
|
||||
|
||||
$in = trim($in);
|
||||
$in = str_replace($replace_from,$replace_to,$in);
|
||||
$in = preg_replace('/\<script(.*)\>(.*)\<\/script\>/Uis',"<script$1></script>",$in);
|
||||
$in = preg_replace('/\<\!\-\-(.*)\-\-\>/Uis',"<!-- comments have been removed -->",$in);
|
||||
|
||||
if (HESK_SLASH === true)
|
||||
{
|
||||
$in = addslashes($in);
|
||||
}
|
||||
$in = str_replace('\"','"',$in);
|
||||
|
||||
return $in;
|
||||
} // END hesk_getHTML()
|
||||
|
||||
|
||||
function hesk_activeSessionValidate($username, $password_hash, $tag)
|
||||
{
|
||||
// Salt and hash need to be separated by a |
|
||||
if ( ! strpos($tag, '|') )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get two parts of the tag
|
||||
list($salt, $hash) = explode('|', $tag, 2);
|
||||
|
||||
// Make sure the hash matches existing username and password
|
||||
if ($hash == sha1($salt . hesk_mb_strtolower($username) . $password_hash) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} // hesk_activeSessionValidate
|
||||
|
||||
|
||||
function hesk_activeSessionCreateTag($username, $password_hash)
|
||||
{
|
||||
$salt = uniqid(mt_rand(), true);
|
||||
return $salt . '|' . sha1($salt . hesk_mb_strtolower($username) . $password_hash);
|
||||
} // END hesk_activeSessionCreateTag()
|
||||
|
||||
|
||||
function hesk_autoLogin($noredirect=0)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $hesk_db_link;
|
||||
|
||||
if (!$hesk_settings['autologin'])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = hesk_htmlspecialchars( hesk_COOKIE('hesk_username') );
|
||||
$hash = hesk_htmlspecialchars( hesk_COOKIE('hesk_p') );
|
||||
define('HESK_USER', $user);
|
||||
|
||||
if (empty($user) || empty($hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Login cookies exist, now lets limit brute force attempts */
|
||||
hesk_limitBfAttempts();
|
||||
|
||||
// Admin login URL
|
||||
$url = $hesk_settings['hesk_url'] . '/' . $hesk_settings['admin_dir'] . '/index.php?a=login¬ice=1';
|
||||
|
||||
/* Check username */
|
||||
$result = hesk_dbQuery('SELECT * FROM `'.$hesk_settings['db_pfix']."users` WHERE `user` = '".hesk_dbEscape($user)."' LIMIT 1");
|
||||
if (hesk_dbNumRows($result) != 1)
|
||||
{
|
||||
hesk_setcookie('hesk_username', '');
|
||||
hesk_setcookie('hesk_p', '');
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
|
||||
$res=hesk_dbFetchAssoc($result);
|
||||
|
||||
/* Check password */
|
||||
if ($hash != hesk_Pass2Hash($res['pass'] . hesk_mb_strtolower($user) . $res['pass']) )
|
||||
{
|
||||
hesk_setcookie('hesk_username', '');
|
||||
hesk_setcookie('hesk_p', '');
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Set user details
|
||||
foreach ($res as $k=>$v)
|
||||
{
|
||||
$_SESSION[$k]=$v;
|
||||
}
|
||||
|
||||
/* Check if default password */
|
||||
if ($_SESSION['pass'] == '499d74967b28a841c98bb4baaabaad699ff3c079')
|
||||
{
|
||||
hesk_process_messages($hesklang['chdp'],'NOREDIRECT','NOTICE');
|
||||
}
|
||||
|
||||
// Set a tag that will be used to expire sessions after username or password change
|
||||
$_SESSION['session_verify'] = hesk_activeSessionCreateTag($user, $_SESSION['pass']);
|
||||
|
||||
// We don't need the password hash anymore
|
||||
unset($_SESSION['pass']);
|
||||
|
||||
/* Login successful, clean brute force attempts */
|
||||
hesk_cleanBfAttempts();
|
||||
|
||||
/* Regenerate session ID (security) */
|
||||
hesk_session_regenerate_id();
|
||||
|
||||
/* Get allowed categories */
|
||||
if (empty($_SESSION['isadmin']))
|
||||
{
|
||||
$_SESSION['categories']=explode(',',$_SESSION['categories']);
|
||||
}
|
||||
|
||||
/* Renew cookies */
|
||||
hesk_setcookie('hesk_username', "$user", strtotime('+1 year'));
|
||||
hesk_setcookie('hesk_p', "$hash", strtotime('+1 year'));
|
||||
|
||||
/* Close any old tickets here so Cron jobs aren't necessary */
|
||||
if ($hesk_settings['autoclose'])
|
||||
{
|
||||
$revision = sprintf($hesklang['thist3'],hesk_date(),$hesklang['auto']);
|
||||
$dt = date('Y-m-d H:i:s',time() - $hesk_settings['autoclose']*86400);
|
||||
|
||||
// Notify customer of closed ticket?
|
||||
if ($hesk_settings['notify_closed'])
|
||||
{
|
||||
// Get list of tickets
|
||||
$result = hesk_dbQuery("SELECT * FROM `".$hesk_settings['db_pfix']."tickets` WHERE `status` = '2' AND `lastchange` <= '".hesk_dbEscape($dt)."' ");
|
||||
if (hesk_dbNumRows($result) > 0)
|
||||
{
|
||||
global $ticket;
|
||||
|
||||
// Load required functions?
|
||||
if ( ! function_exists('hesk_notifyCustomer') )
|
||||
{
|
||||
require(HESK_PATH . 'inc/email_functions.inc.php');
|
||||
}
|
||||
|
||||
while ($ticket = hesk_dbFetchAssoc($result))
|
||||
{
|
||||
$ticket['dt'] = hesk_date($ticket['dt'], true);
|
||||
$ticket['lastchange'] = hesk_date($ticket['lastchange'], true);
|
||||
$ticket = hesk_ticketToPlain($ticket, 1, 0);
|
||||
hesk_notifyCustomer('ticket_closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update ticket statuses and history in database
|
||||
hesk_dbQuery("UPDATE `".$hesk_settings['db_pfix']."tickets` SET `status`='3', `closedat`=NOW(), `closedby`='-1', `history`=CONCAT(`history`,'".hesk_dbEscape($revision)."') WHERE `status` = '2' AND `lastchange` <= '".hesk_dbEscape($dt)."' ");
|
||||
}
|
||||
|
||||
/* If session expired while a HESK page is open just continue using it, don't redirect */
|
||||
if ($noredirect)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Redirect to the destination page */
|
||||
header('Location: ' . hesk_verifyGoto() );
|
||||
exit();
|
||||
} // END hesk_autoLogin()
|
||||
|
||||
|
||||
function hesk_isLoggedIn()
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
$referer = hesk_input($_SERVER['REQUEST_URI']);
|
||||
$referer = str_replace('&','&',$referer);
|
||||
|
||||
// Admin login URL
|
||||
$url = $hesk_settings['hesk_url'] . '/' . $hesk_settings['admin_dir'] . '/index.php?a=login¬ice=1&goto='.urlencode($referer);
|
||||
|
||||
if ( empty($_SESSION['id']) || empty($_SESSION['session_verify']) )
|
||||
{
|
||||
if ($hesk_settings['autologin'] && hesk_autoLogin(1) )
|
||||
{
|
||||
// Users online
|
||||
if ($hesk_settings['online'])
|
||||
{
|
||||
require(HESK_PATH . 'inc/users_online.inc.php');
|
||||
hesk_initOnline($_SESSION['id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
hesk_session_stop();
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_session_regenerate_id();
|
||||
|
||||
// Let's make sure access data is up-to-date
|
||||
$res = hesk_dbQuery( "SELECT `user`, `pass`, `isadmin`, `categories`, `heskprivileges` FROM `".$hesk_settings['db_pfix']."users` WHERE `id` = '".intval($_SESSION['id'])."' LIMIT 1" );
|
||||
|
||||
// Exit if user not found
|
||||
if (hesk_dbNumRows($res) != 1)
|
||||
{
|
||||
hesk_session_stop();
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch results from database
|
||||
$me = hesk_dbFetchAssoc($res);
|
||||
|
||||
// Verify this session is still valid
|
||||
if ( ! hesk_activeSessionValidate($me['user'], $me['pass'], $_SESSION['session_verify']) )
|
||||
{
|
||||
hesk_session_stop();
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Update session variables as needed
|
||||
if ($me['isadmin'] == 1)
|
||||
{
|
||||
$_SESSION['isadmin'] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$_SESSION['isadmin'] = 0;
|
||||
$_SESSION['categories'] = explode(',', $me['categories']);
|
||||
$_SESSION['heskprivileges'] = $me['heskprivileges'];
|
||||
}
|
||||
|
||||
// Users online
|
||||
if ($hesk_settings['online'])
|
||||
{
|
||||
require(HESK_PATH . 'inc/users_online.inc.php');
|
||||
hesk_initOnline($_SESSION['id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // END hesk_isLoggedIn()
|
||||
|
||||
|
||||
function hesk_verifyGoto()
|
||||
{
|
||||
// Default redirect URL
|
||||
$url_default = 'admin_main.php';
|
||||
|
||||
// If no "goto" parameter is set, redirect to the default page
|
||||
if ( ! hesk_isREQUEST('goto') )
|
||||
{
|
||||
return $url_default;
|
||||
}
|
||||
|
||||
// Get the "goto" parameter
|
||||
$url = hesk_REQUEST('goto');
|
||||
|
||||
// Fix encoded "&"
|
||||
$url = str_replace('&', '&', $url);
|
||||
|
||||
// Parse the URL for verification
|
||||
$url_parts = parse_url($url);
|
||||
|
||||
// The "path" part is required
|
||||
if ( ! isset($url_parts['path']) )
|
||||
{
|
||||
return $url_default;
|
||||
}
|
||||
|
||||
// Extract the file name from path
|
||||
$url = basename($url_parts['path']);
|
||||
|
||||
// Allowed files for redirect
|
||||
$OK_urls = array(
|
||||
'admin_main.php' => '',
|
||||
'admin_settings_general.php' => '',
|
||||
'admin_settings_help_desk.php' => '',
|
||||
'admin_settings_knowledgebase.php' => '',
|
||||
'admin_settings_email.php' => '',
|
||||
'admin_settings_ticket_list.php' => '',
|
||||
'admin_settings_misc.php' => '',
|
||||
'admin_settings_save.php' => 'admin_settings_general.php',
|
||||
'admin_ticket.php' => '',
|
||||
'archive.php' => '',
|
||||
'assign_owner.php' => '',
|
||||
'banned_emails.php' => '',
|
||||
'banned_ips.php' => '',
|
||||
'change_status.php' => '',
|
||||
'custom_fields.php' => '',
|
||||
'custom_statuses.php' => '',
|
||||
'edit_post.php' => '',
|
||||
'email_templates.php' => '',
|
||||
'export.php' => '',
|
||||
'find_tickets.php' => '',
|
||||
'generate_spam_question.php' => '',
|
||||
'knowledgebase_private.php' => '',
|
||||
'lock.php' => '',
|
||||
'mail.php' => '',
|
||||
'mail.php?a=read&id=1' => '',
|
||||
'manage_canned.php' => '',
|
||||
'manage_categories.php' => '',
|
||||
'manage_knowledgebase.php' => '',
|
||||
'manage_ticket_templates.php' => '',
|
||||
'manage_users.php' => '',
|
||||
'new_ticket.php' => '',
|
||||
'profile.php' => '',
|
||||
'reports.php' => '',
|
||||
'service_messages.php' => '',
|
||||
'show_tickets.php' => '',
|
||||
);
|
||||
|
||||
// URL must match one of the allowed ones
|
||||
if ( ! isset($OK_urls[$url]) )
|
||||
{
|
||||
return $url_default;
|
||||
}
|
||||
|
||||
// Modify redirect?
|
||||
if ( strlen($OK_urls[$url]) )
|
||||
{
|
||||
$url = $OK_urls[$url];
|
||||
}
|
||||
|
||||
// All OK, return the URL with query if set
|
||||
return isset($url_parts['query']) ? $url.'?'.$url_parts['query'] : $url;
|
||||
|
||||
} // END hesk_verifyGoto()
|
||||
|
||||
|
||||
function hesk_Pass2Hash($plaintext) {
|
||||
$majorsalt = '';
|
||||
$len = strlen($plaintext);
|
||||
for ($i=0;$i<$len;$i++)
|
||||
{
|
||||
$majorsalt .= sha1(substr($plaintext,$i,1));
|
||||
}
|
||||
$corehash = sha1($majorsalt);
|
||||
return $corehash;
|
||||
} // END hesk_Pass2Hash()
|
||||
|
||||
|
||||
function hesk_formatDate($dt, $from_database=true)
|
||||
{
|
||||
$dt=hesk_date($dt, $from_database);
|
||||
$dt=str_replace(' ','<br />',$dt);
|
||||
return $dt;
|
||||
} // End hesk_formatDate()
|
||||
|
||||
|
||||
function hesk_jsString($str)
|
||||
{
|
||||
$str = addslashes($str);
|
||||
$str = str_replace('<br />' , '' , $str);
|
||||
$from = array("/\r\n|\n|\r/", '/\<a href="mailto\:([^"]*)"\>([^\<]*)\<\/a\>/i', '/\<a href="([^"]*)" target="_blank"\>([^\<]*)\<\/a\>/i');
|
||||
$to = array("\\r\\n' + \r\n'", "$1", "$1");
|
||||
return preg_replace($from,$to,$str);
|
||||
} // END hesk_jsString()
|
||||
|
||||
|
||||
function hesk_myOwnership()
|
||||
{
|
||||
if ( ! empty($_SESSION['isadmin']) )
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
|
||||
$can_view_unassigned = hesk_checkPermission('can_view_unassigned',0);
|
||||
$can_view_ass_others = hesk_checkPermission('can_view_ass_others',0);
|
||||
$can_view_ass_by = hesk_checkPermission('can_view_ass_by', 0);
|
||||
|
||||
// Can view all
|
||||
if ($can_view_unassigned && $can_view_ass_others)
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
|
||||
$sql = '';
|
||||
|
||||
if ( ! $can_view_unassigned && ! $can_view_ass_others)
|
||||
{
|
||||
$sql .= "`owner`=" . intval($_SESSION['id']);
|
||||
}
|
||||
elseif ( ! $can_view_unassigned)
|
||||
{
|
||||
$sql .= "`owner` != 0 ";
|
||||
}
|
||||
elseif ( ! $can_view_ass_others)
|
||||
{
|
||||
$sql .= "`owner` IN (0, " . intval($_SESSION['id']) . ") ";
|
||||
}
|
||||
|
||||
// Include tickets he/she assigned to others?
|
||||
if ($can_view_ass_by)
|
||||
{
|
||||
return "(" . $sql . " OR `assignedby`=" . intval($_SESSION['id']) . ")";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
|
||||
} // END hesk_myOwnership()
|
||||
|
||||
|
||||
function hesk_myCategories($what='category')
|
||||
{
|
||||
if ( ! empty($_SESSION['isadmin']) )
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
else
|
||||
{
|
||||
return " `".hesk_dbEscape($what)."` IN ('" . implode("','", array_map('intval', $_SESSION['categories']) ) . "')";
|
||||
}
|
||||
} // END hesk_myCategories()
|
||||
|
||||
|
||||
function hesk_okCategory($cat,$error=1,$user_isadmin=false,$user_cat=false)
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
/* Checking for current user or someone else? */
|
||||
if ($user_isadmin === false)
|
||||
{
|
||||
$user_isadmin = $_SESSION['isadmin'];
|
||||
}
|
||||
|
||||
if ($user_cat === false)
|
||||
{
|
||||
$user_cat = $_SESSION['categories'];
|
||||
}
|
||||
|
||||
/* Is admin? */
|
||||
if ($user_isadmin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
/* Staff with access? */
|
||||
elseif (in_array($cat,$user_cat))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
/* No access */
|
||||
else
|
||||
{
|
||||
if ($error)
|
||||
{
|
||||
hesk_error($hesklang['not_authorized_tickets']);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // END hesk_okCategory()
|
||||
|
||||
|
||||
function hesk_checkPermission($feature,$showerror=1) {
|
||||
global $hesklang;
|
||||
|
||||
/* Admins have full access to all features */
|
||||
if ( isset($_SESSION['isadmin']) && $_SESSION['isadmin'])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Check other staff for permissions */
|
||||
if ( isset($_SESSION['heskprivileges']) && strpos($_SESSION['heskprivileges'], $feature) === false)
|
||||
{
|
||||
if ($showerror)
|
||||
{
|
||||
hesk_error($hesklang['no_permission'].'<p> </p><p align="center"><a href="index.php">'.$hesklang['click_login'].'</a>');
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // END hesk_checkPermission()
|
||||
|
||||
|
||||
function hesk_purge_cache($type = '', $expire_after_seconds = 0)
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
$cache_dir = dirname(dirname(__FILE__)).'/'.$hesk_settings['cache_dir'].'/';
|
||||
|
||||
if ( ! is_dir($cache_dir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($type)
|
||||
{
|
||||
case 'export':
|
||||
$files = glob($cache_dir.'hesk_export_*', GLOB_NOSORT);
|
||||
break;
|
||||
case 'status':
|
||||
$files = glob($cache_dir.'status_*', GLOB_NOSORT);
|
||||
break;
|
||||
case 'cf':
|
||||
$files = glob($cache_dir.'cf_*', GLOB_NOSORT);
|
||||
break;
|
||||
case 'kb':
|
||||
$files = array($cache_dir.'kb.cache.php');
|
||||
break;
|
||||
default:
|
||||
hesk_rrmdir(trim($cache_dir, '/'), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_array($files))
|
||||
{
|
||||
array_walk($files, 'hesk_unlink_callable', $expire_after_seconds);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_purge_cache()
|
||||
|
||||
|
||||
function hesk_rrmdir($dir, $keep_top_level=false)
|
||||
{
|
||||
$files = $keep_top_level ? array_diff(scandir($dir), array('.','..','index.htm')) : array_diff(scandir($dir), array('.','..'));
|
||||
|
||||
foreach ($files as $file)
|
||||
{
|
||||
(is_dir("$dir/$file")) ? hesk_rrmdir("$dir/$file") : @unlink("$dir/$file");
|
||||
}
|
||||
|
||||
return $keep_top_level ? true : @rmdir($dir);
|
||||
|
||||
} // END hesk_rrmdir()
|
||||
109
hesk/inc/assignment_search.inc.php
Normal file
109
hesk/inc/assignment_search.inc.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
/* Assignment */
|
||||
// -> SELF
|
||||
$s_my[$fid] = empty($_GET['s_my']) ? 0 : 1;
|
||||
// -> OTHERS
|
||||
$s_ot[$fid] = empty($_GET['s_ot']) ? 0 : 1;
|
||||
// -> UNASSIGNED
|
||||
$s_un[$fid] = empty($_GET['s_un']) ? 0 : 1;
|
||||
|
||||
// -> Setup SQL based on selected ticket assignments
|
||||
|
||||
/* Make sure at least one is chosen */
|
||||
if ( ! $s_my[$fid] && ! $s_ot[$fid] && ! $s_un[$fid])
|
||||
{
|
||||
$s_my[$fid] = 1;
|
||||
$s_ot[$fid] = 1;
|
||||
$s_un[$fid] = 1;
|
||||
if (!defined('MAIN_PAGE'))
|
||||
{
|
||||
hesk_show_notice($hesklang['e_nose']);
|
||||
}
|
||||
}
|
||||
|
||||
// Can view tickets assigned by him/her?
|
||||
$s_by[$fid] = hesk_checkPermission('can_view_ass_by',0) ? $s_ot[$fid] : 0;
|
||||
|
||||
/* If the user doesn't have permission to view assigned to others block those */
|
||||
if ( ! hesk_checkPermission('can_view_ass_others',0))
|
||||
{
|
||||
$s_ot[$fid] = 0;
|
||||
}
|
||||
|
||||
/* If the user doesn't have permission to view unassigned tickets block those */
|
||||
if ( ! hesk_checkPermission('can_view_unassigned',0))
|
||||
{
|
||||
$s_un[$fid] = 0;
|
||||
}
|
||||
|
||||
/* Process assignments */
|
||||
if ( ! $s_my[$fid] || ! $s_ot[$fid] || ! $s_un[$fid])
|
||||
{
|
||||
if ($s_my[$fid] && $s_ot[$fid])
|
||||
{
|
||||
// All but unassigned
|
||||
$sql .= " AND `owner` > 0 ";
|
||||
}
|
||||
elseif ( ! $s_ot[$fid] && $s_by[$fid])
|
||||
{
|
||||
// Can't view tickets assigned to others, but can see tickets he/she assigned to someone
|
||||
if ($s_my[$fid] && $s_un[$fid])
|
||||
{
|
||||
$sql .= " AND (`owner` IN ('0', '" . intval($_SESSION['id']) . "') OR `assignedby` = " . intval($_SESSION['id']) . ") ";
|
||||
}
|
||||
elseif($s_my[$fid])
|
||||
{
|
||||
$sql .= " AND (`owner` = '" . intval($_SESSION['id']) . "' OR `assignedby` = " . intval($_SESSION['id']) . ") ";
|
||||
}
|
||||
elseif($s_un[$fid])
|
||||
{
|
||||
$sql .= " AND (`owner` = 0 OR `assignedby` = " . intval($_SESSION['id']) . ") ";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sql .= " AND `assignedby` = " . intval($_SESSION['id']) . " ";
|
||||
}
|
||||
|
||||
$s_ot[$fid] = 1;
|
||||
}
|
||||
elseif ($s_my[$fid] && $s_un[$fid])
|
||||
{
|
||||
// My tickets + unassigned
|
||||
$sql .= " AND `owner` IN ('0', '" . intval($_SESSION['id']) . "') ";
|
||||
}
|
||||
elseif ($s_ot[$fid] && $s_un[$fid])
|
||||
{
|
||||
// Assigned to others + unassigned
|
||||
$sql .= " AND `owner` != '" . intval($_SESSION['id']) . "' ";
|
||||
}
|
||||
elseif ($s_my[$fid])
|
||||
{
|
||||
// Assigned to me only
|
||||
$sql .= " AND `owner` = '" . intval($_SESSION['id']) . "' ";
|
||||
}
|
||||
elseif ($s_ot[$fid])
|
||||
{
|
||||
// Assigned to others
|
||||
$sql .= " AND `owner` NOT IN ('0', '" . intval($_SESSION['id']) . "') ";
|
||||
}
|
||||
elseif ($s_un[$fid])
|
||||
{
|
||||
// Only unassigned
|
||||
$sql .= " AND `owner` = 0 ";
|
||||
}
|
||||
}
|
||||
112
hesk/inc/attachments.inc.php
Normal file
112
hesk/inc/attachments.inc.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
/***************************
|
||||
Function hesk_uploadFiles()
|
||||
***************************/
|
||||
function hesk_uploadFile($i)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $trackingID, $hesk_error_buffer;
|
||||
|
||||
/* Return if name is empty */
|
||||
if (empty($_FILES['attachment']['name'][$i])) {return '';}
|
||||
|
||||
/* Parse the name */
|
||||
$file_realname = hesk_cleanFileName($_FILES['attachment']['name'][$i]);
|
||||
|
||||
/* Check file extension */
|
||||
$ext = strtolower(strrchr($file_realname, "."));
|
||||
if ( ! in_array($ext,$hesk_settings['attachments']['allowed_types']))
|
||||
{
|
||||
return hesk_fileError(sprintf($hesklang['type_not_allowed'], $ext, $file_realname));
|
||||
}
|
||||
|
||||
/* Check file size */
|
||||
if ($_FILES['attachment']['size'][$i] > $hesk_settings['attachments']['max_size'])
|
||||
{
|
||||
return hesk_fileError(sprintf($hesklang['file_too_large'], $file_realname));
|
||||
}
|
||||
else
|
||||
{
|
||||
$file_size = $_FILES['attachment']['size'][$i];
|
||||
}
|
||||
|
||||
/* Generate a random file name */
|
||||
$useChars='AEUYBDGHJLMNPQRSTVWXZ123456789';
|
||||
$tmp = uniqid();
|
||||
for($j=1;$j<10;$j++)
|
||||
{
|
||||
$tmp .= $useChars[mt_rand(0,29)];
|
||||
}
|
||||
|
||||
if (defined('KB'))
|
||||
{
|
||||
$file_name = substr(md5($tmp . $file_realname), 0, 200) . $ext;
|
||||
}
|
||||
else
|
||||
{
|
||||
$file_name = substr($trackingID . '_' . md5($tmp . $file_realname), 0, 200) . $ext;
|
||||
}
|
||||
|
||||
// Does the temporary file exist? If not, probably server-side configuration limits have been reached
|
||||
// Uncomment this for debugging purposes
|
||||
/*
|
||||
if ( ! file_exists($_FILES['attachment']['tmp_name'][$i]) )
|
||||
{
|
||||
return hesk_fileError($hesklang['fnuscphp']);
|
||||
}
|
||||
*/
|
||||
|
||||
/* If upload was successful let's create the headers */
|
||||
if ( ! move_uploaded_file($_FILES['attachment']['tmp_name'][$i], dirname(dirname(__FILE__)).'/'.$hesk_settings['attach_dir'].'/'.$file_name))
|
||||
{
|
||||
return hesk_fileError($hesklang['cannot_move_tmp']);
|
||||
}
|
||||
|
||||
$info = array(
|
||||
'saved_name'=> $file_name,
|
||||
'real_name' => $file_realname,
|
||||
'size' => $file_size
|
||||
);
|
||||
|
||||
return $info;
|
||||
} // End hesk_uploadFile()
|
||||
|
||||
|
||||
function hesk_fileError($error)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $trackingID;
|
||||
global $hesk_error_buffer;
|
||||
|
||||
$hesk_error_buffer['attachments'] = $error;
|
||||
|
||||
return false;
|
||||
} // End hesk_fileError()
|
||||
|
||||
|
||||
function hesk_removeAttachments($attachments)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
$hesk_settings['server_path'] = dirname(dirname(__FILE__)).'/'.$hesk_settings['attach_dir'].'/';
|
||||
|
||||
foreach ($attachments as $myatt)
|
||||
{
|
||||
hesk_unlink($hesk_settings['server_path'].$myatt['saved_name']);
|
||||
}
|
||||
|
||||
return true;
|
||||
} // End hesk_removeAttachments()
|
||||
2394
hesk/inc/common.inc.php
Normal file
2394
hesk/inc/common.inc.php
Normal file
File diff suppressed because one or more lines are too long
247
hesk/inc/custom_fields.inc.php
Normal file
247
hesk/inc/custom_fields.inc.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
// Get and append custom fields setup to the settings
|
||||
hesk_load_custom_fields();
|
||||
|
||||
// Save number of custom fields
|
||||
$hesk_settings['num_custom_fields'] = count($hesk_settings['custom_fields']);
|
||||
|
||||
// Load custom fields for admin functions
|
||||
if (function_exists('hesk_checkPermission'))
|
||||
{
|
||||
foreach ($hesk_settings['custom_fields'] as $k => $v)
|
||||
{
|
||||
$hesk_settings['possible_ticket_list'][$k] = $hesk_settings['custom_fields'][$k]['title'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** FUNCTIONS ***/
|
||||
|
||||
|
||||
function hesk_load_custom_fields($category=0, $use_cache=1)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
// Do we have a cached version available
|
||||
$cache_dir = dirname(dirname(__FILE__)).'/'.$hesk_settings['cache_dir'].'/';
|
||||
$cache_file = $cache_dir . 'cf_' . sha1($hesk_settings['language']).'.cache.php';
|
||||
|
||||
if ($use_cache && file_exists($cache_file))
|
||||
{
|
||||
require($cache_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get custom fields from the database
|
||||
$hesk_settings['custom_fields'] = array();
|
||||
|
||||
// Make sure we have database connection
|
||||
hesk_load_database_functions();
|
||||
hesk_dbConnect();
|
||||
|
||||
$res = hesk_dbQuery("SELECT * FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."custom_fields` WHERE `use` IN ('1', '2') ORDER BY `place` ASC, `order` ASC");
|
||||
while ($row = hesk_dbFetchAssoc($res))
|
||||
{
|
||||
$id = 'custom' . $row['id'];
|
||||
unset($row['id']);
|
||||
|
||||
// Let's set field name for current language (or the first one we find)
|
||||
$names = json_decode($row['name'], true);
|
||||
$row['name'] = (isset($names[$hesk_settings['language']])) ? $names[$hesk_settings['language']] : reset($names);
|
||||
|
||||
// Name for display in ticket list; punctuation removed and shortened
|
||||
$row['title'] = hesk_remove_punctuation($row['name']);
|
||||
$row['title'] = hesk_mb_strlen($row['title']) > 30 ? hesk_mb_substr($row['title'], 0, 30) . '...' : $row['title'];
|
||||
|
||||
// A version with forced punctuation
|
||||
$row['name:'] = in_array(substr($row['name'], -1), array(':', '?', '!', '.') ) ? $row['name'] : $row['name'] . ':';
|
||||
|
||||
// Decode categories
|
||||
$row['category'] = strlen($row['category']) ? json_decode($row['category'], true) : array();
|
||||
|
||||
// Decode options
|
||||
$row['value'] = json_decode($row['value'], true);
|
||||
|
||||
// Add to custom_fields array
|
||||
$hesk_settings['custom_fields'][$id] = $row;
|
||||
}
|
||||
|
||||
// Try to cache results
|
||||
if ($use_cache && (is_dir($cache_dir) || ( @mkdir($cache_dir, 0777) && is_writable($cache_dir) ) ) )
|
||||
{
|
||||
// Is there an index.htm file?
|
||||
if ( ! file_exists($cache_dir.'index.htm'))
|
||||
{
|
||||
@file_put_contents($cache_dir.'index.htm', '');
|
||||
}
|
||||
|
||||
// Write data
|
||||
@file_put_contents($cache_file, '<?php if (!defined(\'IN_SCRIPT\')) {die();} $hesk_settings[\'custom_fields\']=' . var_export($hesk_settings['custom_fields'], true) . ';' );
|
||||
}
|
||||
|
||||
return true;
|
||||
} // END hesk_load_custom_fields()
|
||||
|
||||
|
||||
function hesk_is_custom_field_in_category($custom_id, $category_id)
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
return (
|
||||
empty($hesk_settings['custom_fields'][$custom_id]['category']) ||
|
||||
in_array($category_id, $hesk_settings['custom_fields'][$custom_id]['category'])
|
||||
) ? true : false;
|
||||
} // END hesk_is_custom_field_in_category()
|
||||
|
||||
|
||||
function hesk_custom_field_type($type)
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
switch ($type)
|
||||
{
|
||||
case 'text':
|
||||
return $hesklang['stf'];
|
||||
case 'textarea':
|
||||
return $hesklang['stb'];
|
||||
case 'radio':
|
||||
return $hesklang['srb'];
|
||||
case 'select':
|
||||
return $hesklang['ssb'];
|
||||
case 'checkbox':
|
||||
return $hesklang['scb'];
|
||||
case 'email':
|
||||
return $hesklang['email'];
|
||||
case 'date':
|
||||
return $hesklang['date'];
|
||||
case 'hidden':
|
||||
return $hesklang['sch'];
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
} // END hesk_custom_field_type()
|
||||
|
||||
|
||||
function hesk_custom_date_display_format($timestamp, $format = 'F j, Y')
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
if ($timestamp == '')
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! is_int($timestamp))
|
||||
{
|
||||
$timestamp = $timestamp * 1;
|
||||
}
|
||||
|
||||
if ($hesklang['LANGUAGE']=='English')
|
||||
{
|
||||
return gmdate($format, $timestamp);
|
||||
}
|
||||
|
||||
// Attempt to translate date for non-English users
|
||||
|
||||
$translate_months = array(
|
||||
'January' => $hesklang['m1'],
|
||||
'February' => $hesklang['m2'],
|
||||
'March' => $hesklang['m3'],
|
||||
'April' => $hesklang['m4'],
|
||||
'May' => $hesklang['m5'],
|
||||
'June' => $hesklang['m6'],
|
||||
'July' => $hesklang['m7'],
|
||||
'August' => $hesklang['m8'],
|
||||
'September' => $hesklang['m9'],
|
||||
'October' => $hesklang['m10'],
|
||||
'November' => $hesklang['m11'],
|
||||
'December' => $hesklang['m12']
|
||||
);
|
||||
|
||||
$translate_months_short = array(
|
||||
'Jan' => $hesklang['ms01'],
|
||||
'Feb' => $hesklang['ms02'],
|
||||
'Mar' => $hesklang['ms03'],
|
||||
'Apr' => $hesklang['ms04'],
|
||||
'May' => $hesklang['ms05'],
|
||||
'Jun' => $hesklang['ms06'],
|
||||
'Jul' => $hesklang['ms07'],
|
||||
'Aug' => $hesklang['ms08'],
|
||||
'Sep' => $hesklang['ms09'],
|
||||
'Oct' => $hesklang['ms10'],
|
||||
'Nov' => $hesklang['ms11'],
|
||||
'Dec' => $hesklang['ms12']
|
||||
);
|
||||
|
||||
$translate_days = array(
|
||||
'Monday' => $hesklang['d1'],
|
||||
'Tuesday' => $hesklang['d2'],
|
||||
'Wednesday' => $hesklang['d3'],
|
||||
'Thursday' => $hesklang['d4'],
|
||||
'Friday' => $hesklang['d5'],
|
||||
'Saturday' => $hesklang['d6'],
|
||||
'Sunday' => $hesklang['d0']
|
||||
);
|
||||
|
||||
$translate_days_short = array(
|
||||
'Mon' => $hesklang['mo'],
|
||||
'Tuw' => $hesklang['tu'],
|
||||
'Wes' => $hesklang['we'],
|
||||
'Thu' => $hesklang['th'],
|
||||
'Fri' => $hesklang['fr'],
|
||||
'Sat' => $hesklang['sa'],
|
||||
'Sun' => $hesklang['su']
|
||||
);
|
||||
|
||||
$date_translate = array();
|
||||
|
||||
if (strpos($format, 'F') !== false)
|
||||
{
|
||||
$date_translate = array_merge($date_translate, $translate_months);
|
||||
}
|
||||
|
||||
if (strpos($format, 'M') !== false)
|
||||
{
|
||||
$date_translate = array_merge($date_translate, $translate_months_short);
|
||||
}
|
||||
|
||||
if (strpos($format, 'l') !== false)
|
||||
{
|
||||
$date_translate = array_merge($date_translate, $translate_days);
|
||||
}
|
||||
|
||||
if (strpos($format, 'D') !== false)
|
||||
{
|
||||
$date_translate = array_merge($date_translate, $translate_days_short);
|
||||
}
|
||||
|
||||
if (count($date_translate))
|
||||
{
|
||||
return str_replace( array_keys($date_translate), array_values($date_translate), gmdate($format, $timestamp));
|
||||
}
|
||||
|
||||
return gmdate($format, $timestamp);
|
||||
|
||||
} // END hesk_custom_date_display_format()
|
||||
|
||||
|
||||
function hesk_remove_punctuation($in)
|
||||
{
|
||||
return rtrim($in, ':?!.');
|
||||
} // END hesk_remove_punctuation()
|
||||
259
hesk/inc/database.inc.php
Normal file
259
hesk/inc/database.inc.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
|
||||
function hesk_dbCollate()
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
// MySQL vesions prior to 5.6 don't support some collations
|
||||
if ( in_array($hesklang['_COLLATE'], array('utf8_croatian_ci', 'utf8_german2_ci', 'utf8_vietnamese_ci')) )
|
||||
{
|
||||
if ( version_compare( hesk_dbResult( hesk_dbQuery('SELECT VERSION() AS version') ), '5.6', '<') )
|
||||
{
|
||||
$hesklang['_COLLATE'] = 'utf8_general_ci';
|
||||
}
|
||||
}
|
||||
|
||||
return hesk_dbEscape($hesklang['_COLLATE']);
|
||||
|
||||
} // END hesk_dbCollate()
|
||||
|
||||
|
||||
function hesk_dbSetNames()
|
||||
{
|
||||
global $hesk_settings, $hesk_db_link;
|
||||
|
||||
if ($hesk_settings['db_vrsn'])
|
||||
{
|
||||
mysql_set_charset('utf8', $hesk_db_link);
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_dbQuery("SET NAMES 'utf8'");
|
||||
}
|
||||
|
||||
} // END hesk_dbSetNames()
|
||||
|
||||
|
||||
function hesk_dbFormatEmail($email, $field = 'email')
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
$email = hesk_dbLike($email);
|
||||
|
||||
if ($hesk_settings['multi_eml'])
|
||||
{
|
||||
return " (`".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email)."' OR `".hesk_dbEscape($field)."` LIKE '%,".hesk_dbEscape($email)."' OR `".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email).",%' OR `".hesk_dbEscape($field)."` LIKE '%,".hesk_dbEscape($email).",%') ";
|
||||
}
|
||||
else
|
||||
{
|
||||
return " `".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email)."' ";
|
||||
}
|
||||
|
||||
} // END hesk_dbFormatEmail()
|
||||
|
||||
|
||||
function hesk_dbTime()
|
||||
{
|
||||
$res = hesk_dbQuery("SELECT NOW()");
|
||||
return strtotime(hesk_dbResult($res,0,0));
|
||||
} // END hesk_dbTime()
|
||||
|
||||
|
||||
function hesk_dbSetTimezone()
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
hesk_dbQuery('SET time_zone = "'.hesk_timeToHHMM(date('Z')).'"');
|
||||
|
||||
return true;
|
||||
} // END hesk_dbSetTimezone()
|
||||
|
||||
|
||||
function hesk_dbEscape($in)
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
$in = mysql_real_escape_string(stripslashes($in), $hesk_db_link);
|
||||
$in = str_replace('`','`',$in);
|
||||
|
||||
return $in;
|
||||
} // END hesk_dbEscape()
|
||||
|
||||
|
||||
function hesk_dbLike($in)
|
||||
{
|
||||
return str_replace( array('\\', '_', '%'), array('\\\\', '\\\\_', '\\\\%'), $in); // '
|
||||
} // END hesk_dbLike()
|
||||
|
||||
|
||||
function hesk_dbConnect()
|
||||
{
|
||||
global $hesk_settings;
|
||||
global $hesk_db_link;
|
||||
global $hesklang;
|
||||
|
||||
// Do we have an existing active link?
|
||||
if ($hesk_db_link)
|
||||
{
|
||||
return $hesk_db_link;
|
||||
}
|
||||
|
||||
// Is mysql supported?
|
||||
if ( ! function_exists('mysql_connect') )
|
||||
{
|
||||
die($hesklang['emp']);
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
$hesk_db_link = @mysql_connect($hesk_settings['db_host'], $hesk_settings['db_user'], $hesk_settings['db_pass']);
|
||||
|
||||
// Errors?
|
||||
if ( ! $hesk_db_link)
|
||||
{
|
||||
if ($hesk_settings['debug_mode'])
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[mysql_said]:<br />".mysql_error()."</p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[contact_webmsater] <a href=\"mailto:$hesk_settings[webmaster_mail]\">$hesk_settings[webmaster_mail]</a></p>");
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! @mysql_select_db($hesk_settings['db_name'], $hesk_db_link))
|
||||
{
|
||||
if ($hesk_settings['debug_mode'])
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[mysql_said]:<br />".mysql_error()."</p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[contact_webmsater] <a href=\"mailto:$hesk_settings[webmaster_mail]\">$hesk_settings[webmaster_mail]</a></p>");
|
||||
}
|
||||
}
|
||||
|
||||
// Check MySQL/PHP version and set encoding to utf8
|
||||
hesk_dbSetNames();
|
||||
|
||||
// Set the correct timezone
|
||||
hesk_dbSetTimezone();
|
||||
|
||||
return $hesk_db_link;
|
||||
|
||||
} // END hesk_dbConnect()
|
||||
|
||||
|
||||
function hesk_dbClose()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
return @mysql_close($hesk_db_link);
|
||||
|
||||
} // END hesk_dbClose()
|
||||
|
||||
|
||||
function hesk_dbQuery($query)
|
||||
{
|
||||
global $hesk_last_query;
|
||||
global $hesk_db_link;
|
||||
global $hesklang, $hesk_settings;
|
||||
|
||||
if ( ! $hesk_db_link && ! hesk_dbConnect())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$hesk_last_query = $query;
|
||||
|
||||
#echo "<p>EXPLAIN $query</p>\n";
|
||||
|
||||
if ($res = @mysql_query($query, $hesk_db_link))
|
||||
{
|
||||
return $res;
|
||||
}
|
||||
elseif ($hesk_settings['debug_mode'])
|
||||
{
|
||||
hesk_error("$hesklang[cant_sql]: $query</p><p>$hesklang[mysql_said]:<br />".mysql_error()."</p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error("$hesklang[cant_sql]</p><p>$hesklang[contact_webmsater] <a href=\"mailto:$hesk_settings[webmaster_mail]\">$hesk_settings[webmaster_mail]</a></p>");
|
||||
}
|
||||
|
||||
} // END hesk_dbQuery()
|
||||
|
||||
|
||||
function hesk_dbFetchAssoc($res)
|
||||
{
|
||||
|
||||
return @mysql_fetch_assoc($res);
|
||||
|
||||
} // END hesk_FetchAssoc()
|
||||
|
||||
|
||||
function hesk_dbFetchRow($res)
|
||||
{
|
||||
|
||||
return @mysql_fetch_row($res);
|
||||
|
||||
} // END hesk_FetchRow()
|
||||
|
||||
|
||||
function hesk_dbResult($res, $row = 0, $column = 0)
|
||||
{
|
||||
|
||||
return @mysql_result($res, $row, $column);
|
||||
|
||||
} // END hesk_dbResult()
|
||||
|
||||
|
||||
function hesk_dbInsertID()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
if ($lastid = @mysql_insert_id($hesk_db_link))
|
||||
{
|
||||
return $lastid;
|
||||
}
|
||||
|
||||
} // END hesk_dbInsertID()
|
||||
|
||||
|
||||
function hesk_dbFreeResult($res)
|
||||
{
|
||||
|
||||
return mysql_free_result($res);
|
||||
|
||||
} // END hesk_dbFreeResult()
|
||||
|
||||
|
||||
function hesk_dbNumRows($res)
|
||||
{
|
||||
|
||||
return @mysql_num_rows($res);
|
||||
|
||||
} // END hesk_dbNumRows()
|
||||
|
||||
|
||||
function hesk_dbAffectedRows()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
return @mysql_affected_rows($hesk_db_link);
|
||||
|
||||
} // END hesk_dbAffectedRows()
|
||||
266
hesk/inc/database_mysqli.inc.php
Normal file
266
hesk/inc/database_mysqli.inc.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
|
||||
function hesk_dbCollate()
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
// MySQL vesions prior to 5.6 don't support some collations
|
||||
if ( in_array($hesklang['_COLLATE'], array('utf8_croatian_ci', 'utf8_german2_ci', 'utf8_vietnamese_ci')) )
|
||||
{
|
||||
if ( version_compare( hesk_dbResult( hesk_dbQuery('SELECT VERSION() AS version') ), '5.6', '<') )
|
||||
{
|
||||
$hesklang['_COLLATE'] = 'utf8_general_ci';
|
||||
}
|
||||
}
|
||||
|
||||
return hesk_dbEscape($hesklang['_COLLATE']);
|
||||
|
||||
} // END hesk_dbCollate()
|
||||
|
||||
|
||||
function hesk_dbSetNames()
|
||||
{
|
||||
global $hesk_settings, $hesk_db_link;
|
||||
|
||||
if ($hesk_settings['db_vrsn'])
|
||||
{
|
||||
mysqli_set_charset($hesk_db_link, 'utf8');
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_dbQuery("SET NAMES 'utf8'");
|
||||
}
|
||||
|
||||
} // END hesk_dbSetNames()
|
||||
|
||||
|
||||
function hesk_dbFormatEmail($email, $field = 'email')
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
$email = hesk_dbLike($email);
|
||||
|
||||
if ($hesk_settings['multi_eml'])
|
||||
{
|
||||
return " (`".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email)."' OR `".hesk_dbEscape($field)."` LIKE '%,".hesk_dbEscape($email)."' OR `".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email).",%' OR `".hesk_dbEscape($field)."` LIKE '%,".hesk_dbEscape($email).",%') ";
|
||||
}
|
||||
else
|
||||
{
|
||||
return " `".hesk_dbEscape($field)."` LIKE '".hesk_dbEscape($email)."' ";
|
||||
}
|
||||
|
||||
} // END hesk_dbFormatEmail()
|
||||
|
||||
|
||||
function hesk_dbTime()
|
||||
{
|
||||
$res = hesk_dbQuery("SELECT NOW()");
|
||||
return strtotime(hesk_dbResult($res,0,0));
|
||||
} // END hesk_dbTime()
|
||||
|
||||
|
||||
function hesk_dbSetTimezone()
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
hesk_dbQuery('SET time_zone = "'.hesk_timeToHHMM(date('Z')).'"');
|
||||
|
||||
return true;
|
||||
} // END hesk_dbSetTimezone()
|
||||
|
||||
|
||||
function hesk_dbEscape($in)
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
$in = mysqli_real_escape_string($hesk_db_link, stripslashes($in));
|
||||
$in = str_replace('`','`',$in);
|
||||
|
||||
return $in;
|
||||
} // END hesk_dbEscape()
|
||||
|
||||
|
||||
function hesk_dbLike($in)
|
||||
{
|
||||
return str_replace( array('\\', '_', '%'), array('\\\\', '\\\\_', '\\\\%'), $in); // '
|
||||
} // END hesk_dbLike()
|
||||
|
||||
|
||||
function hesk_dbConnect()
|
||||
{
|
||||
global $hesk_settings;
|
||||
global $hesk_db_link;
|
||||
global $hesklang;
|
||||
|
||||
// Do we have an existing active link?
|
||||
if ($hesk_db_link)
|
||||
{
|
||||
return $hesk_db_link;
|
||||
}
|
||||
|
||||
// Is mysqli supported?
|
||||
if ( ! function_exists('mysqli_connect') )
|
||||
{
|
||||
die($hesklang['emp']);
|
||||
}
|
||||
|
||||
// Do we need a special port? Check and connect to the database
|
||||
if ( strpos($hesk_settings['db_host'], ':') )
|
||||
{
|
||||
list($hesk_settings['db_host_no_port'], $hesk_settings['db_port']) = explode(':', $hesk_settings['db_host']);
|
||||
$hesk_db_link = @mysqli_connect($hesk_settings['db_host_no_port'], $hesk_settings['db_user'], $hesk_settings['db_pass'], $hesk_settings['db_name'], intval($hesk_settings['db_port']) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$hesk_db_link = @mysqli_connect($hesk_settings['db_host'], $hesk_settings['db_user'], $hesk_settings['db_pass'], $hesk_settings['db_name']);
|
||||
}
|
||||
|
||||
// Errors?
|
||||
if ( ! $hesk_db_link)
|
||||
{
|
||||
if ($hesk_settings['debug_mode'])
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[mysql_said]:<br />(".mysqli_connect_errno().") ".mysqli_connect_error()."</p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error("$hesklang[cant_connect_db]</p><p>$hesklang[contact_webmsater] <a href=\"mailto:$hesk_settings[webmaster_mail]\">$hesk_settings[webmaster_mail]</a></p>");
|
||||
}
|
||||
}
|
||||
|
||||
// Check MySQL/PHP version and set encoding to utf8
|
||||
hesk_dbSetNames();
|
||||
|
||||
// Set the correct timezone
|
||||
hesk_dbSetTimezone();
|
||||
|
||||
return $hesk_db_link;
|
||||
|
||||
} // END hesk_dbConnect()
|
||||
|
||||
|
||||
function hesk_dbClose()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
return @mysqli_close($hesk_db_link);
|
||||
|
||||
} // END hesk_dbClose()
|
||||
|
||||
|
||||
function hesk_dbQuery($query)
|
||||
{
|
||||
global $hesk_last_query;
|
||||
global $hesk_db_link;
|
||||
global $hesklang, $hesk_settings;
|
||||
|
||||
if ( ! $hesk_db_link && ! hesk_dbConnect())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$hesk_last_query = $query;
|
||||
|
||||
# echo "<p>EXPLAIN $query</p>\n";
|
||||
|
||||
if ($res = @mysqli_query($hesk_db_link, $query))
|
||||
{
|
||||
return $res;
|
||||
}
|
||||
elseif ($hesk_settings['debug_mode'])
|
||||
{
|
||||
hesk_error("$hesklang[cant_sql]: $query</p><p>$hesklang[mysql_said]:<br />".mysqli_error($hesk_db_link)."</p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error("$hesklang[cant_sql]</p><p>$hesklang[contact_webmsater] <a href=\"mailto:$hesk_settings[webmaster_mail]\">$hesk_settings[webmaster_mail]</a></p>");
|
||||
}
|
||||
|
||||
} // END hesk_dbQuery()
|
||||
|
||||
|
||||
function hesk_dbFetchAssoc($res)
|
||||
{
|
||||
|
||||
return @mysqli_fetch_assoc($res);
|
||||
|
||||
} // END hesk_FetchAssoc()
|
||||
|
||||
|
||||
function hesk_dbFetchRow($res)
|
||||
{
|
||||
|
||||
return @mysqli_fetch_row($res);
|
||||
|
||||
} // END hesk_FetchRow()
|
||||
|
||||
|
||||
function hesk_dbResult($res, $row = 0, $column = 0)
|
||||
{
|
||||
$i=0;
|
||||
$res->data_seek(0);
|
||||
|
||||
while ($tmp = @mysqli_fetch_array($res, MYSQLI_NUM))
|
||||
{
|
||||
if ($i==$row)
|
||||
{
|
||||
return $tmp[$column];
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
} // END hesk_dbResult()
|
||||
|
||||
|
||||
function hesk_dbInsertID()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
if ($lastid = @mysqli_insert_id($hesk_db_link))
|
||||
{
|
||||
return $lastid;
|
||||
}
|
||||
|
||||
} // END hesk_dbInsertID()
|
||||
|
||||
|
||||
function hesk_dbFreeResult($res)
|
||||
{
|
||||
|
||||
return @mysqli_free_result($res);
|
||||
|
||||
} // END hesk_dbFreeResult()
|
||||
|
||||
|
||||
function hesk_dbNumRows($res)
|
||||
{
|
||||
|
||||
return @mysqli_num_rows($res);
|
||||
|
||||
} // END hesk_dbNumRows()
|
||||
|
||||
|
||||
function hesk_dbAffectedRows()
|
||||
{
|
||||
global $hesk_db_link;
|
||||
|
||||
return @mysqli_affected_rows($hesk_db_link);
|
||||
|
||||
} // END hesk_dbAffectedRows()
|
||||
669
hesk/inc/email_functions.inc.php
Normal file
669
hesk/inc/email_functions.inc.php
Normal file
@@ -0,0 +1,669 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
// Make sure custom fields are loaded
|
||||
require_once(HESK_PATH . 'inc/custom_fields.inc.php');
|
||||
|
||||
// Make sure statuses are loaded
|
||||
require_once(HESK_PATH . 'inc/statuses.inc.php');
|
||||
|
||||
/* Get includes for SMTP */
|
||||
if ($hesk_settings['smtp'])
|
||||
{
|
||||
require(HESK_PATH . 'inc/mail/smtp.php');
|
||||
if (strlen($hesk_settings['smtp_user']) || strlen($hesk_settings['smtp_password']))
|
||||
{
|
||||
require_once(HESK_PATH . 'inc/mail/sasl/sasl.php');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function hesk_notifyCustomer($email_template = 'new_ticket')
|
||||
{
|
||||
global $hesk_settings, $hesklang, $ticket;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// No customer email
|
||||
if ($ticket['email'] == '')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make sure customer gets response in correct language
|
||||
if ( isset($ticket['language']) )
|
||||
{
|
||||
hesk_setLanguage($ticket['language']);
|
||||
}
|
||||
|
||||
// Format email subject and message
|
||||
$subject = hesk_getEmailSubject($email_template,$ticket);
|
||||
$message = hesk_getEmailMessage($email_template,$ticket);
|
||||
|
||||
// Send e-mail
|
||||
hesk_mail($ticket['email'], $subject, $message);
|
||||
|
||||
// Reset language if needed
|
||||
hesk_resetLanguage();
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_notifyCustomer()
|
||||
|
||||
|
||||
function hesk_notifyAssignedStaff($autoassign_owner, $email_template, $type = 'notify_assigned')
|
||||
{
|
||||
global $hesk_settings, $hesklang, $ticket;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$ticket['owner'] = intval($ticket['owner']);
|
||||
|
||||
/* Need to lookup owner info from the database? */
|
||||
if ($autoassign_owner === false)
|
||||
{
|
||||
$res = hesk_dbQuery("SELECT `name`, `email`,`language`,`notify_assigned`,`notify_reply_my` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."users` WHERE `id`='" . $ticket['owner'] . "' LIMIT 1");
|
||||
|
||||
$autoassign_owner = hesk_dbFetchAssoc($res);
|
||||
$hesk_settings['user_data'][$ticket['owner']] = $autoassign_owner;
|
||||
|
||||
/* If owner selected not to be notified or invalid stop here */
|
||||
if ( empty($autoassign_owner[$type]) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set new language if required */
|
||||
hesk_setLanguage($autoassign_owner['language']);
|
||||
|
||||
/* Format email subject and message for staff */
|
||||
$subject = hesk_getEmailSubject($email_template,$ticket);
|
||||
$message = hesk_getEmailMessage($email_template,$ticket,1);
|
||||
|
||||
/* Send email to staff */
|
||||
hesk_mail($autoassign_owner['email'], $subject, $message);
|
||||
|
||||
/* Reset language to original one */
|
||||
hesk_resetLanguage();
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_notifyAssignedStaff()
|
||||
|
||||
|
||||
function hesk_notifyStaff($email_template,$sql_where,$is_ticket=1)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $ticket;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$admins = array();
|
||||
|
||||
$res = hesk_dbQuery("SELECT `email`,`language`,`isadmin`,`categories` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."users` WHERE $sql_where ORDER BY `language`");
|
||||
while ($myuser = hesk_dbFetchAssoc($res))
|
||||
{
|
||||
/* Is this an administrator? */
|
||||
if ($myuser['isadmin'])
|
||||
{
|
||||
$admins[] = array('email' => $myuser['email'], 'language' => $myuser['language']);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Not admin, is he/she allowed this category? */
|
||||
$myuser['categories']=explode(',',$myuser['categories']);
|
||||
if (in_array($ticket['category'],$myuser['categories']))
|
||||
{
|
||||
$admins[] = array('email' => $myuser['email'], 'language' => $myuser['language']);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($admins) > 0)
|
||||
{
|
||||
/* Make sure each user gets email in his/her preferred language */
|
||||
$current_language = 'NONE';
|
||||
$recipients = array();
|
||||
|
||||
/* Loop through staff */
|
||||
foreach ($admins as $admin)
|
||||
{
|
||||
/* If admin language is NULL force default HESK language */
|
||||
if ( ! $admin['language'] || ! isset($hesk_settings['languages'][$admin['language']]) )
|
||||
{
|
||||
$admin['language'] = HESK_DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
/* Generate message or add email to the list of recepients */
|
||||
if ($admin['language'] == $current_language)
|
||||
{
|
||||
/* We already have the message, just add email to the recipients list */
|
||||
$recipients[] = $admin['email'];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Send email messages in previous languages (if required) */
|
||||
if ($current_language != 'NONE')
|
||||
{
|
||||
/* Send e-mail to staff */
|
||||
hesk_mail(implode(',',$recipients), $subject, $message );
|
||||
|
||||
/* Reset list of email addresses */
|
||||
$recipients = array();
|
||||
}
|
||||
|
||||
/* Set new language */
|
||||
hesk_setLanguage($admin['language']);
|
||||
|
||||
/* Format staff email subject and message for this language */
|
||||
$subject = hesk_getEmailSubject($email_template,$ticket);
|
||||
$message = hesk_getEmailMessage($email_template,$ticket,$is_ticket);
|
||||
|
||||
/* Add email to the recipients list */
|
||||
$recipients[] = $admin['email'];
|
||||
|
||||
/* Remember the last processed language */
|
||||
$current_language = $admin['language'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Send email messages to the remaining staff */
|
||||
hesk_mail(implode(',',$recipients), $subject, $message );
|
||||
|
||||
/* Reset language to original one */
|
||||
hesk_resetLanguage();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_notifyStaff()
|
||||
|
||||
|
||||
function hesk_validEmails()
|
||||
{
|
||||
global $hesklang;
|
||||
|
||||
return array(
|
||||
|
||||
/*** Emails sent to CLIENT ***/
|
||||
|
||||
// --> Send reminder about existing tickets
|
||||
'forgot_ticket_id' => $hesklang['forgot_ticket_id'],
|
||||
|
||||
// --> Staff replied to a ticket
|
||||
'new_reply_by_staff' => $hesklang['new_reply_by_staff'],
|
||||
|
||||
// --> New ticket submitted
|
||||
'new_ticket' => $hesklang['ticket_received'],
|
||||
|
||||
// --> Ticket closed
|
||||
'ticket_closed' => $hesklang['ticket_closed'],
|
||||
|
||||
/*** Emails sent to STAFF ***/
|
||||
|
||||
// --> Ticket moved to a new category
|
||||
'category_moved' => $hesklang['category_moved'],
|
||||
|
||||
// --> Client replied to a ticket
|
||||
'new_reply_by_customer' => $hesklang['new_reply_by_customer'],
|
||||
|
||||
// --> New ticket submitted
|
||||
'new_ticket_staff' => $hesklang['new_ticket_staff'],
|
||||
|
||||
// --> New ticket assigned to staff
|
||||
'ticket_assigned_to_you'=> $hesklang['ticket_assigned_to_you'],
|
||||
|
||||
// --> New private message
|
||||
'new_pm' => $hesklang['new_pm'],
|
||||
|
||||
// --> New note by someone to a ticket assigned to you
|
||||
'new_note' => $hesklang['new_note'],
|
||||
|
||||
// --> Staff password reset email
|
||||
'reset_password' => $hesklang['reset_password'],
|
||||
|
||||
);
|
||||
} // END hesk_validEmails()
|
||||
|
||||
|
||||
function hesk_mail($to,$subject,$message)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty recipient?
|
||||
if ($to == '')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stop if we find anything suspicious in the headers
|
||||
if ( preg_match("/\n|\r|\t|%0A|%0D|%08|%09/", $to . $subject) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encode subject to UTF-8
|
||||
$subject = hesk_encodeIfNotAscii( hesk_html_entity_decode($subject) );
|
||||
|
||||
// Setup "name <email>" for headers
|
||||
if ($hesk_settings['noreply_name'])
|
||||
{
|
||||
$hesk_settings['from_header'] = hesk_encodeIfNotAscii( hesk_html_entity_decode($hesk_settings['noreply_name']) ) . " <" . $hesk_settings['noreply_mail'] . ">";
|
||||
}
|
||||
else
|
||||
{
|
||||
$hesk_settings['from_header'] = $hesk_settings['noreply_mail'];
|
||||
}
|
||||
|
||||
// Uncomment for debugging
|
||||
# echo "<p>TO: $to<br >SUBJECT: $subject<br >MSG: $message</p>";
|
||||
# return true;
|
||||
|
||||
// Remove duplicate recipients
|
||||
$to_arr = array_unique(explode(',', $to));
|
||||
$to_arr = array_values($to_arr);
|
||||
$to = implode(',', $to_arr);
|
||||
|
||||
// Use PHP's mail function
|
||||
if ( ! $hesk_settings['smtp'])
|
||||
{
|
||||
// Set additional headers
|
||||
$headers = "From: $hesk_settings[from_header]\n";
|
||||
$headers.= "Reply-To: $hesk_settings[from_header]\n";
|
||||
$headers.= "Return-Path: $hesk_settings[webmaster_mail]\n";
|
||||
$headers.= "Date: " . date(DATE_RFC2822) . "\n";
|
||||
$headers.= "Message-ID: " . hesk_generateMessageID() . "\n";
|
||||
$headers.= "MIME-Version: 1.0\n";
|
||||
$headers.= "Content-Type: text/plain; charset=" . $hesklang['ENCODING'];
|
||||
|
||||
// Send using PHP mail() function
|
||||
ob_start();
|
||||
mail($to,$subject,$message,$headers);
|
||||
$tmp = trim(ob_get_contents());
|
||||
ob_end_clean();
|
||||
|
||||
return (strlen($tmp)) ? $tmp : true;
|
||||
}
|
||||
|
||||
// Use a SMTP server directly instead
|
||||
$smtp = new smtp_class;
|
||||
$smtp->host_name = $hesk_settings['smtp_host_name'];
|
||||
$smtp->host_port = $hesk_settings['smtp_host_port'];
|
||||
$smtp->timeout = $hesk_settings['smtp_timeout'];
|
||||
$smtp->ssl = $hesk_settings['smtp_ssl'];
|
||||
$smtp->start_tls = $hesk_settings['smtp_tls'];
|
||||
$smtp->user = $hesk_settings['smtp_user'];
|
||||
$smtp->password = hesk_htmlspecialchars_decode($hesk_settings['smtp_password']);
|
||||
$smtp->debug = 1;
|
||||
|
||||
// Start output buffering so that any errors don't break headers
|
||||
ob_start();
|
||||
|
||||
// Send the e-mail using SMTP
|
||||
if ( ! $smtp->SendMessage($hesk_settings['noreply_mail'], $to_arr, array(
|
||||
"From: $hesk_settings[from_header]",
|
||||
"To: $to",
|
||||
"Reply-To: $hesk_settings[from_header]",
|
||||
"Return-Path: $hesk_settings[webmaster_mail]",
|
||||
"Subject: " . $subject,
|
||||
"Date: " . date(DATE_RFC2822),
|
||||
"Message-ID: " . hesk_generateMessageID(),
|
||||
"MIME-Version: 1.0",
|
||||
"Content-Type: text/plain; charset=" . $hesklang['ENCODING']
|
||||
), $message))
|
||||
{
|
||||
// Suppress errors unless we are in debug mode
|
||||
if ($hesk_settings['debug_mode'])
|
||||
{
|
||||
$error = $hesklang['cnsm'] . ' ' . $to . '<br /><br />' .
|
||||
$hesklang['error'] . ': ' . htmlspecialchars($smtp->error). '<br /><br />' .
|
||||
'<textarea name="smtp_log" rows="10" cols="60">' . ob_get_contents() . '</textarea>';
|
||||
ob_end_clean();
|
||||
hesk_error($error);
|
||||
}
|
||||
else
|
||||
{
|
||||
$_SESSION['HESK_2ND_NOTICE'] = true;
|
||||
$_SESSION['HESK_2ND_MESSAGE'] = $hesklang['esf'] . ' ' . $hesklang['contact_webmsater'] . ' <a href="mailto:' . $hesk_settings['webmaster_mail'] . '">' . $hesk_settings['webmaster_mail'] . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
ob_end_clean();
|
||||
|
||||
return true;
|
||||
|
||||
} // END hesk_mail()
|
||||
|
||||
|
||||
function hesk_getEmailSubject($eml_file, $ticket='', $is_ticket=1, $strip=0)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/* Get list of valid emails */
|
||||
$valid_emails = hesk_validEmails();
|
||||
|
||||
/* Verify this is a valid email include */
|
||||
if ( ! isset($valid_emails[$eml_file]))
|
||||
{
|
||||
hesk_error($hesklang['inve']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$msg = $valid_emails[$eml_file];
|
||||
}
|
||||
|
||||
/* If not a ticket-related email return subject as is */
|
||||
if ( ! $ticket )
|
||||
{
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/* Strip slashes from the subject only if it's a new ticket */
|
||||
if ($strip)
|
||||
{
|
||||
$ticket['subject'] = stripslashes($ticket['subject']);
|
||||
}
|
||||
|
||||
/* Not a ticket, but has some info in the $ticket array */
|
||||
if ( ! $is_ticket)
|
||||
{
|
||||
return str_replace('%%SUBJECT%%', $ticket['subject'], $msg);
|
||||
}
|
||||
|
||||
/* Set category title */
|
||||
$ticket['category'] = hesk_msgToPlain(hesk_getCategoryName($ticket['category']), 1);
|
||||
|
||||
/* Get priority */
|
||||
switch ($ticket['priority'])
|
||||
{
|
||||
case 0:
|
||||
$ticket['priority'] = $hesklang['critical'];
|
||||
break;
|
||||
case 1:
|
||||
$ticket['priority'] = $hesklang['high'];
|
||||
break;
|
||||
case 2:
|
||||
$ticket['priority'] = $hesklang['medium'];
|
||||
break;
|
||||
default:
|
||||
$ticket['priority'] = $hesklang['low'];
|
||||
}
|
||||
|
||||
/* Set status */
|
||||
$ticket['status'] = hesk_get_status_name($ticket['status']);
|
||||
|
||||
/* Replace all special tags */
|
||||
$msg = str_replace('%%SUBJECT%%', $ticket['subject'], $msg);
|
||||
$msg = str_replace('%%TRACK_ID%%', $ticket['trackid'], $msg);
|
||||
$msg = str_replace('%%CATEGORY%%', $ticket['category'], $msg);
|
||||
$msg = str_replace('%%PRIORITY%%', $ticket['priority'], $msg);
|
||||
$msg = str_replace('%%STATUS%%', $ticket['status'], $msg);
|
||||
|
||||
return $msg;
|
||||
|
||||
} // hesk_getEmailSubject()
|
||||
|
||||
|
||||
function hesk_getEmailMessage($eml_file, $ticket, $is_admin=0, $is_ticket=1, $just_message=0)
|
||||
{
|
||||
global $hesk_settings, $hesklang;
|
||||
|
||||
// Demo mode
|
||||
if ( defined('HESK_DEMO') )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/* Get list of valid emails */
|
||||
$valid_emails = hesk_validEmails();
|
||||
|
||||
/* Verify this is a valid email include */
|
||||
if ( ! isset($valid_emails[$eml_file]))
|
||||
{
|
||||
hesk_error($hesklang['inve']);
|
||||
}
|
||||
|
||||
/* Get email template */
|
||||
$eml_file = 'language/' . $hesk_settings['languages'][$hesk_settings['language']]['folder'] . '/emails/' . $eml_file . '.txt';
|
||||
|
||||
if (file_exists(HESK_PATH . $eml_file))
|
||||
{
|
||||
$msg = file_get_contents(HESK_PATH . $eml_file);
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error($hesklang['emfm'].': '.$eml_file);
|
||||
}
|
||||
|
||||
/* Return just the message without any processing? */
|
||||
if ($just_message)
|
||||
{
|
||||
return $msg;
|
||||
}
|
||||
|
||||
// Convert any entities in site title to plain text
|
||||
$hesk_settings['site_title'] = hesk_msgToPlain($hesk_settings['site_title'], 1);
|
||||
|
||||
/* If it's not a ticket-related mail (like "a new PM") just process quickly */
|
||||
if ( ! $is_ticket)
|
||||
{
|
||||
$trackingURL = $hesk_settings['hesk_url'] . '/' . $hesk_settings['admin_dir'] . '/mail.php?a=read&id=' . intval($ticket['id']);
|
||||
|
||||
$msg = str_replace('%%NAME%%', $ticket['name'] ,$msg);
|
||||
$msg = str_replace('%%SUBJECT%%', $ticket['subject'] ,$msg);
|
||||
$msg = str_replace('%%TRACK_URL%%', $trackingURL ,$msg);
|
||||
$msg = str_replace('%%SITE_TITLE%%',$hesk_settings['site_title'] ,$msg);
|
||||
$msg = str_replace('%%SITE_URL%%', $hesk_settings['site_url'] ,$msg);
|
||||
$msg = str_replace('%%FIRST_NAME%%',hesk_full_name_to_first_name($ticket['name']),$msg);
|
||||
|
||||
if ( isset($ticket['message']) )
|
||||
{
|
||||
return str_replace('%%MESSAGE%%', $ticket['message'], $msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
// Is email required to view ticket (for customers only)?
|
||||
$hesk_settings['e_param'] = $hesk_settings['email_view_ticket'] ? '&e=' . rawurlencode($ticket['email']) : '';
|
||||
|
||||
/* Generate the ticket URLs */
|
||||
$trackingURL = $hesk_settings['hesk_url'];
|
||||
$trackingURL.= $is_admin ? '/' . $hesk_settings['admin_dir'] . '/admin_ticket.php' : '/ticket.php';
|
||||
$trackingURL.= '?track='.$ticket['trackid'].($is_admin ? '' : $hesk_settings['e_param']).'&Refresh='.rand(10000,99999);
|
||||
|
||||
/* Set category title */
|
||||
$ticket['category'] = hesk_msgToPlain(hesk_getCategoryName($ticket['category']), 1, 0);
|
||||
|
||||
/* Set priority title */
|
||||
switch ($ticket['priority'])
|
||||
{
|
||||
case 0:
|
||||
$ticket['priority'] = $hesklang['critical'];
|
||||
break;
|
||||
case 1:
|
||||
$ticket['priority'] = $hesklang['high'];
|
||||
break;
|
||||
case 2:
|
||||
$ticket['priority'] = $hesklang['medium'];
|
||||
break;
|
||||
default:
|
||||
$ticket['priority'] = $hesklang['low'];
|
||||
}
|
||||
|
||||
/* Get owner name */
|
||||
$ticket['owner'] = hesk_msgToPlain( hesk_getOwnerName($ticket['owner']), 1);
|
||||
|
||||
/* Set status */
|
||||
$ticket['status'] = hesk_get_status_name($ticket['status']);
|
||||
|
||||
// Get name of the person who posted the last message
|
||||
if ( ! isset($ticket['last_reply_by']))
|
||||
{
|
||||
$ticket['last_reply_by'] = hesk_getReplierName($ticket);
|
||||
}
|
||||
|
||||
/* Replace all special tags */
|
||||
$msg = str_replace('%%NAME%%', $ticket['name'] ,$msg);
|
||||
$msg = str_replace('%%SUBJECT%%', $ticket['subject'] ,$msg);
|
||||
$msg = str_replace('%%TRACK_ID%%', $ticket['trackid'] ,$msg);
|
||||
$msg = str_replace('%%TRACK_URL%%', $trackingURL ,$msg);
|
||||
$msg = str_replace('%%SITE_TITLE%%',$hesk_settings['site_title'],$msg);
|
||||
$msg = str_replace('%%SITE_URL%%', $hesk_settings['site_url'] ,$msg);
|
||||
$msg = str_replace('%%CATEGORY%%', $ticket['category'] ,$msg);
|
||||
$msg = str_replace('%%PRIORITY%%', $ticket['priority'] ,$msg);
|
||||
$msg = str_replace('%%OWNER%%', $ticket['owner'] ,$msg);
|
||||
$msg = str_replace('%%STATUS%%', $ticket['status'] ,$msg);
|
||||
$msg = str_replace('%%EMAIL%%', $ticket['email'] ,$msg);
|
||||
$msg = str_replace('%%CREATED%%', $ticket['dt'] ,$msg);
|
||||
$msg = str_replace('%%UPDATED%%', $ticket['lastchange'] ,$msg);
|
||||
$msg = str_replace('%%ID%%', $ticket['id'] ,$msg);
|
||||
$msg = str_replace('%%TIME_WORKED%%', $ticket['time_worked'] ,$msg);
|
||||
$msg = str_replace('%%LAST_REPLY_BY%%',$ticket['last_reply_by'] ,$msg);
|
||||
$msg = str_replace('%%FIRST_NAME%%',hesk_full_name_to_first_name($ticket['name']),$msg);
|
||||
|
||||
/* All custom fields */
|
||||
for ($i=1; $i<=50; $i++)
|
||||
{
|
||||
$k = 'custom'.$i;
|
||||
|
||||
if (isset($hesk_settings['custom_fields'][$k]))
|
||||
{
|
||||
$v = $hesk_settings['custom_fields'][$k];
|
||||
|
||||
switch ($v['type'])
|
||||
{
|
||||
case 'checkbox':
|
||||
$ticket[$k] = str_replace("<br />","\n",$ticket[$k]);
|
||||
break;
|
||||
case 'date':
|
||||
$ticket[$k] = hesk_custom_date_display_format($ticket[$k], $v['value']['date_format']);
|
||||
break;
|
||||
}
|
||||
|
||||
$msg = str_replace('%%'.strtoupper($k).'%%',stripslashes($ticket[$k]),$msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
$msg = str_replace('%%'.strtoupper($k).'%%','',$msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Is message tag in email template?
|
||||
if (strpos($msg, '%%MESSAGE%%') !== false)
|
||||
{
|
||||
// Replace message
|
||||
$msg = str_replace('%%MESSAGE%%',$ticket['message'],$msg);
|
||||
|
||||
// Add direct links to any attachments at the bottom of the email message
|
||||
if ($hesk_settings['attachments']['use'] && isset($ticket['attachments']) && strlen($ticket['attachments']) )
|
||||
{
|
||||
$msg .= "\n\n\n" . $hesklang['fatt'];
|
||||
|
||||
$att = explode(',', substr($ticket['attachments'], 0, -1));
|
||||
foreach ($att as $myatt)
|
||||
{
|
||||
list($att_id, $att_name) = explode('#', $myatt);
|
||||
$msg .= "\n\n" . $att_name . "\n" . $hesk_settings['hesk_url'] . '/download_attachment.php?att_id='.$att_id.'&track='.$ticket['trackid'].$hesk_settings['e_param'];
|
||||
}
|
||||
}
|
||||
|
||||
// For customer notifications: if we allow email piping/pop 3 fetching and
|
||||
// stripping quoted replies add an "reply above this line" tag
|
||||
if ( ! $is_admin && ($hesk_settings['email_piping'] || $hesk_settings['pop3']) && $hesk_settings['strip_quoted'])
|
||||
{
|
||||
$msg = $hesklang['EMAIL_HR'] . "\n\n" . $msg;
|
||||
}
|
||||
}
|
||||
|
||||
return $msg;
|
||||
|
||||
} // END hesk_getEmailMessage
|
||||
|
||||
|
||||
function hesk_encodeIfNotAscii($str)
|
||||
{
|
||||
// Match anything outside of ASCII range
|
||||
if (preg_match('/[^\x00-\x7F]/', $str))
|
||||
{
|
||||
return "=?UTF-8?B?" . base64_encode($str) . "?=";
|
||||
}
|
||||
|
||||
return $str;
|
||||
} // END hesk_encodeIfNotAscii()
|
||||
|
||||
|
||||
function hesk_generateMessageID()
|
||||
{
|
||||
if (function_exists('openssl_random_pseudo_bytes'))
|
||||
{
|
||||
$id = base_convert(bin2hex(openssl_random_pseudo_bytes(8)), 16, 36);
|
||||
}
|
||||
else
|
||||
{
|
||||
$id = uniqid('', true);
|
||||
}
|
||||
|
||||
// If run from CLI, set the Hesk URL as host name
|
||||
if (isset($_SERVER['SERVER_NAME']))
|
||||
{
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
}
|
||||
else
|
||||
{
|
||||
global $hesk_settings;
|
||||
|
||||
$parts = parse_url($hesk_settings['hesk_url']);
|
||||
|
||||
if (empty($parts['host']))
|
||||
{
|
||||
$host = gethostname();
|
||||
$host = str_replace('>', '', $host);
|
||||
}
|
||||
else
|
||||
{
|
||||
$host = $parts['host'];
|
||||
}
|
||||
}
|
||||
|
||||
return '<' . $id . '.' . gmdate('YmdHis') . '@' . $host . '>';
|
||||
} // END hesk_generateMessageID()
|
||||
420
hesk/inc/export_functions.inc.php
Normal file
420
hesk/inc/export_functions.inc.php
Normal file
@@ -0,0 +1,420 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
/*** FUNCTIONS ***/
|
||||
|
||||
function hesk_export_to_XML($sql, $export_selected = false)
|
||||
{
|
||||
global $hesk_settings, $hesklang, $ticket, $my_cat;
|
||||
|
||||
// We'll need HH:MM:SS format for hesk_date() here
|
||||
$hesk_settings['timeformat'] = 'H:i:s';
|
||||
|
||||
// Get staff names
|
||||
$admins = array();
|
||||
$result = hesk_dbQuery("SELECT `id`,`name` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."users` ORDER BY `name` ASC");
|
||||
while ($row=hesk_dbFetchAssoc($result))
|
||||
{
|
||||
$admins[$row['id']]=$row['name'];
|
||||
}
|
||||
|
||||
// Get category names
|
||||
if ( ! isset($my_cat))
|
||||
{
|
||||
$my_cat = array();
|
||||
$res2 = hesk_dbQuery("SELECT `id`, `name` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` WHERE " . hesk_myCategories('id') . " ORDER BY `cat_order` ASC");
|
||||
while ($row=hesk_dbFetchAssoc($res2))
|
||||
{
|
||||
$my_cat[$row['id']] = hesk_msgToPlain($row['name'], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// This will be the export directory
|
||||
$export_dir = HESK_PATH.$hesk_settings['cache_dir'].'/';
|
||||
|
||||
// This will be the name of the export and the XML file
|
||||
$export_name = 'hesk_export_'.date('Y-m-d_H-i-s').'_'.mt_rand(100000,999999);
|
||||
$save_to = $export_dir . $export_name . '.xml';
|
||||
|
||||
// Do we have the export directory?
|
||||
if ( is_dir($export_dir) || ( @mkdir($export_dir, 0777) && is_writable($export_dir) ) )
|
||||
{
|
||||
// Is there an index.htm file?
|
||||
if ( ! file_exists($export_dir.'index.htm'))
|
||||
{
|
||||
@file_put_contents($export_dir.'index.htm', '');
|
||||
}
|
||||
|
||||
// Cleanup old files
|
||||
hesk_purge_cache('export', 86400);
|
||||
}
|
||||
else
|
||||
{
|
||||
hesk_error($hesklang['ede']);
|
||||
}
|
||||
|
||||
// Make sure the file can be saved and written to
|
||||
@file_put_contents($save_to, '');
|
||||
if ( ! file_exists($save_to) )
|
||||
{
|
||||
hesk_error($hesklang['eef']);
|
||||
}
|
||||
|
||||
// Start generating the report message and generating the export
|
||||
$success_msg = '';
|
||||
$flush_me = '<br /><br />';
|
||||
$flush_me .= hesk_date() . " | {$hesklang['inite']} ";
|
||||
|
||||
// Is this export of a date or date range?
|
||||
if ($export_selected === false)
|
||||
{
|
||||
global $date_from, $date_to;
|
||||
|
||||
if ($date_from == $date_to)
|
||||
{
|
||||
$flush_me .= "(" . hesk_dateToString($date_from,0) . ")";
|
||||
}
|
||||
else
|
||||
{
|
||||
$flush_me .= "(" . hesk_dateToString($date_from,0) . " - " . hesk_dateToString($date_to,0) . ")";
|
||||
}
|
||||
}
|
||||
|
||||
$flush_me .= "<br />\n";
|
||||
|
||||
// Start generating file contents
|
||||
$tmp = '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?mso-application progid="Excel.Sheet"?>
|
||||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:html="http://www.w3.org/TR/REC-html40">
|
||||
<OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
|
||||
<AllowPNG/>
|
||||
</OfficeDocumentSettings>
|
||||
<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
|
||||
<WindowHeight>8250</WindowHeight>
|
||||
<WindowWidth>16275</WindowWidth>
|
||||
<WindowTopX>360</WindowTopX>
|
||||
<WindowTopY>90</WindowTopY>
|
||||
<ProtectStructure>False</ProtectStructure>
|
||||
<ProtectWindows>False</ProtectWindows>
|
||||
</ExcelWorkbook>
|
||||
<Styles>
|
||||
<Style ss:ID="Default" ss:Name="Normal">
|
||||
<Alignment ss:Vertical="Bottom"/>
|
||||
<Borders/>
|
||||
<Font ss:FontName="Calibri" x:CharSet="238" x:Family="Swiss" ss:Size="11"
|
||||
ss:Color="#000000"/>
|
||||
<Interior/>
|
||||
<NumberFormat/>
|
||||
<Protection/>
|
||||
</Style>
|
||||
<Style ss:ID="s62">
|
||||
<NumberFormat ss:Format="General Date"/>
|
||||
</Style>
|
||||
<Style ss:ID="s63">
|
||||
<NumberFormat ss:Format="Short Date"/>
|
||||
</Style>
|
||||
<Style ss:ID="s65">
|
||||
<NumberFormat ss:Format="[h]:mm:ss"/>
|
||||
</Style>
|
||||
</Styles>
|
||||
<Worksheet ss:Name="Sheet1">
|
||||
<Table>
|
||||
';
|
||||
|
||||
// Define column width
|
||||
$tmp .= '
|
||||
<Column ss:AutoFitWidth="0" ss:Width="50"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="84" ss:Span="1"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="110"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="110"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="90"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="90"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="87"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="57.75"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="57.75"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="100"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="100"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="80"/>
|
||||
<Column ss:AutoFitWidth="0" ss:Width="80"/>
|
||||
';
|
||||
|
||||
foreach ($hesk_settings['custom_fields'] as $k=>$v)
|
||||
{
|
||||
if ($v['use'])
|
||||
{
|
||||
$tmp .= '<Column ss:AutoFitWidth="0" ss:Width="80"/>' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Define first row (header)
|
||||
$tmp .= '
|
||||
<Row>
|
||||
<Cell><Data ss:Type="String">#</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['trackID'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['date'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['last_update'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['name'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['email'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['category'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['priority'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['status'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['subject'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['message'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['owner'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String">'.$hesklang['ts'].'</Data></Cell>
|
||||
';
|
||||
|
||||
foreach ($hesk_settings['custom_fields'] as $k=>$v)
|
||||
{
|
||||
if ($v['use'])
|
||||
{
|
||||
$tmp .= '<Cell><Data ss:Type="String">'.$v['name'].'</Data></Cell>' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$tmp .= "</Row>\n";
|
||||
|
||||
// Write what we have by now into the XML file
|
||||
file_put_contents($save_to, $tmp, FILE_APPEND);
|
||||
$flush_me .= hesk_date() . " | {$hesklang['gXML']}<br />\n";
|
||||
|
||||
// OK, now start dumping data and writing it into the file
|
||||
$tickets_exported = 0;
|
||||
$save_after = 100;
|
||||
$this_round = 0;
|
||||
$tmp = '';
|
||||
|
||||
$result = hesk_dbQuery($sql);
|
||||
while ($ticket=hesk_dbFetchAssoc($result))
|
||||
{
|
||||
$ticket['status'] = hesk_get_status_name($ticket['status']);
|
||||
|
||||
switch ($ticket['priority'])
|
||||
{
|
||||
case 0:
|
||||
$ticket['priority']=$hesklang['critical'];
|
||||
break;
|
||||
case 1:
|
||||
$ticket['priority']=$hesklang['high'];
|
||||
break;
|
||||
case 2:
|
||||
$ticket['priority']=$hesklang['medium'];
|
||||
break;
|
||||
default:
|
||||
$ticket['priority']=$hesklang['low'];
|
||||
}
|
||||
|
||||
$ticket['archive'] = !($ticket['archive']) ? $hesklang['no'] : $hesklang['yes'];
|
||||
$ticket['message'] = hesk_msgToPlain($ticket['message'], 1, 0);
|
||||
$ticket['subject'] = hesk_msgToPlain($ticket['subject'], 1, 0);
|
||||
$ticket['owner'] = isset($admins[$ticket['owner']]) ? $admins[$ticket['owner']] : '';
|
||||
$ticket['category'] = isset($my_cat[$ticket['category']]) ? $my_cat[$ticket['category']] : '';
|
||||
|
||||
// Format for export dates
|
||||
$hesk_settings['timeformat'] = "Y-m-d\TH:i:s\.000";
|
||||
|
||||
// Create row for the XML file
|
||||
$tmp .= '
|
||||
<Row>
|
||||
<Cell><Data ss:Type="Number">'.$ticket['id'].'</Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['trackid'].']]></Data></Cell>
|
||||
<Cell ss:StyleID="s62"><Data ss:Type="DateTime">'.hesk_date($ticket['dt'], true).'</Data></Cell>
|
||||
<Cell ss:StyleID="s62"><Data ss:Type="DateTime">'.hesk_date($ticket['lastchange'], true).'</Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.hesk_msgToPlain($ticket['name'], 1).']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['email'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['category'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['priority'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['status'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['subject'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['message'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['owner'].']]></Data></Cell>
|
||||
<Cell><Data ss:Type="String"><![CDATA['.$ticket['time_worked'].']]></Data></Cell>
|
||||
';
|
||||
|
||||
// Add custom fields
|
||||
foreach ($hesk_settings['custom_fields'] as $k=>$v)
|
||||
{
|
||||
if ($v['use'])
|
||||
{
|
||||
switch ($v['type'])
|
||||
{
|
||||
case 'date':
|
||||
$tmp_dt = hesk_custom_date_display_format($ticket[$k], 'Y-m-d\T00:00:00.000');
|
||||
$tmp .= strlen($tmp_dt) ? '<Cell ss:StyleID="s63"><Data ss:Type="DateTime">'.$tmp_dt : '<Cell><Data ss:Type="String">';
|
||||
$tmp .= "</Data></Cell> \n";
|
||||
break;
|
||||
default:
|
||||
$tmp .= '<Cell><Data ss:Type="String"><![CDATA['.hesk_msgToPlain($ticket[$k], 1, 0).']]></Data></Cell> ' . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tmp .= "</Row>\n";
|
||||
|
||||
// Write every 100 rows into the file
|
||||
if ($this_round >= $save_after)
|
||||
{
|
||||
file_put_contents($save_to, $tmp, FILE_APPEND);
|
||||
$this_round = 0;
|
||||
$tmp = '';
|
||||
usleep(1);
|
||||
}
|
||||
|
||||
$tickets_exported++;
|
||||
$this_round++;
|
||||
} // End of while loop
|
||||
|
||||
// Go back to the HH:MM:SS format for hesk_date()
|
||||
$hesk_settings['timeformat'] = 'H:i:s';
|
||||
|
||||
// Append any remaining rows into the file
|
||||
if ($this_round > 0)
|
||||
{
|
||||
file_put_contents($save_to, $tmp, FILE_APPEND);
|
||||
}
|
||||
|
||||
// If any tickets were exported, continue, otherwise cleanup
|
||||
if ($tickets_exported > 0)
|
||||
{
|
||||
// Finish the XML file
|
||||
$tmp = '
|
||||
</Table>
|
||||
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
|
||||
<PageSetup>
|
||||
<Header x:Margin="0.3"/>
|
||||
<Footer x:Margin="0.3"/>
|
||||
<PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>
|
||||
</PageSetup>
|
||||
<Selected/>
|
||||
<Panes>
|
||||
<Pane>
|
||||
<Number>3</Number>
|
||||
<ActiveRow>4</ActiveRow>
|
||||
</Pane>
|
||||
</Panes>
|
||||
<ProtectObjects>False</ProtectObjects>
|
||||
<ProtectScenarios>False</ProtectScenarios>
|
||||
</WorksheetOptions>
|
||||
</Worksheet>
|
||||
<Worksheet ss:Name="Sheet2">
|
||||
<Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"
|
||||
x:FullRows="1" ss:DefaultRowHeight="15">
|
||||
</Table>
|
||||
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
|
||||
<PageSetup>
|
||||
<Header x:Margin="0.3"/>
|
||||
<Footer x:Margin="0.3"/>
|
||||
<PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>
|
||||
</PageSetup>
|
||||
<ProtectObjects>False</ProtectObjects>
|
||||
<ProtectScenarios>False</ProtectScenarios>
|
||||
</WorksheetOptions>
|
||||
</Worksheet>
|
||||
<Worksheet ss:Name="Sheet3">
|
||||
<Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"
|
||||
x:FullRows="1" ss:DefaultRowHeight="15">
|
||||
</Table>
|
||||
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
|
||||
<PageSetup>
|
||||
<Header x:Margin="0.3"/>
|
||||
<Footer x:Margin="0.3"/>
|
||||
<PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>
|
||||
</PageSetup>
|
||||
<ProtectObjects>False</ProtectObjects>
|
||||
<ProtectScenarios>False</ProtectScenarios>
|
||||
</WorksheetOptions>
|
||||
</Worksheet>
|
||||
</Workbook>
|
||||
';
|
||||
file_put_contents($save_to, $tmp, FILE_APPEND);
|
||||
|
||||
// Log how many rows we exported
|
||||
$flush_me .= hesk_date() . " | " . sprintf($hesklang['nrow'], $tickets_exported) . "<br />\n";
|
||||
|
||||
// We will convert XML to Zip to save a lot of space
|
||||
$save_to_zip = $export_dir.$export_name.'.zip';
|
||||
|
||||
// Log start of Zip creation
|
||||
$flush_me .= hesk_date() . " | {$hesklang['cZIP']}<br />\n";
|
||||
|
||||
// Preferrably use the zip extension
|
||||
if (extension_loaded('zip'))
|
||||
{
|
||||
$save_to_zip = $export_dir.$export_name.'.zip';
|
||||
|
||||
$zip = new ZipArchive;
|
||||
$res = $zip->open($save_to_zip, ZipArchive::CREATE);
|
||||
if ($res === TRUE)
|
||||
{
|
||||
$zip->addFile($save_to, "{$export_name}.xml");
|
||||
$zip->close();
|
||||
}
|
||||
else
|
||||
{
|
||||
die("{$hesklang['eZIP']} <$save_to_zip>\n");
|
||||
}
|
||||
|
||||
}
|
||||
// Some servers have ZipArchive class enabled anyway - can we use it?
|
||||
elseif ( class_exists('ZipArchive') )
|
||||
{
|
||||
require(HESK_PATH . 'inc/zip/Zip.php');
|
||||
$zip = new Zip();
|
||||
$zip->addLargeFile($save_to, "{$export_name}.xml");
|
||||
$zip->finalize();
|
||||
$zip->setZipFile($save_to_zip);
|
||||
}
|
||||
// If not available, use a 3rd party Zip class included with HESK
|
||||
else
|
||||
{
|
||||
require(HESK_PATH . 'inc/zip/pclzip.lib.php');
|
||||
$zip = new PclZip($save_to_zip);
|
||||
$zip->add($save_to, PCLZIP_OPT_REMOVE_ALL_PATH);
|
||||
}
|
||||
|
||||
// Delete XML, just leave the Zip archive
|
||||
hesk_unlink($save_to);
|
||||
|
||||
// Echo memory peak usage
|
||||
$flush_me .= hesk_date() . " | " . sprintf($hesklang['pmem'], (@memory_get_peak_usage(true) / 1048576)) . "<br />\r\n";
|
||||
|
||||
// We're done!
|
||||
$flush_me .= hesk_date() . " | {$hesklang['fZIP']}<br /><br />";
|
||||
|
||||
// Success message
|
||||
$referer = isset($_SERVER['HTTP_REFERER']) ? hesk_input($_SERVER['HTTP_REFERER']) : 'export.php';
|
||||
$referer = str_replace('&','&',$referer);
|
||||
if (strpos($referer, 'export.php'))
|
||||
{
|
||||
$referer = 'export.php';
|
||||
}
|
||||
|
||||
$success_msg .= $hesk_settings['debug_mode'] ? $flush_me : '<br /><br />';
|
||||
$success_msg .= $hesklang['step1'] . ': <a href="' . $save_to_zip . '">' . $hesklang['ch2d'] . '</a><br /><br />' . $hesklang['step2'] . ': <a href="export.php?delete='.urlencode($export_name).'&goto='.urlencode($referer).'">' . $hesklang['dffs'] . '</a>';
|
||||
}
|
||||
// No tickets exported, cleanup
|
||||
else
|
||||
{
|
||||
hesk_unlink($save_to);
|
||||
}
|
||||
|
||||
return array($success_msg, $tickets_exported);
|
||||
|
||||
} // END hesk_export_to_XML()
|
||||
94
hesk/inc/footer.inc.php
Normal file
94
hesk/inc/footer.inc.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
// Check if this is a valid include
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
// Users online
|
||||
if (defined('SHOW_ONLINE'))
|
||||
{
|
||||
hesk_printOnline();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
The code below handles HESK licensing and must be included in the template.
|
||||
|
||||
Removing this code is a direct violation of the HESK End User License Agreement,
|
||||
will void all support and may result in unexpected behavior.
|
||||
|
||||
To purchase a HESK license and support future HESK development please visit:
|
||||
https://www.hesk.com/buy.php
|
||||
*******************************************************************************/
|
||||
$hesk_settings['hesk_license']('Qo8Zm9vdGVyIGNsYXNzPSJmb290ZXIiPg0KICAgIDxwIGNsY
|
||||
XNzPSJ0ZXh0LWNlbnRlciI+UG93ZXJlZCBieSA8YSBocmVmPSJodHRwczovL3d3dy5oZXNrLmNvbSIgY
|
||||
2xhc3M9ImxpbmsiPkhlbHAgRGVzayBTb2Z0d2FyZTwvYT4gPHNwYW4gY2xhc3M9ImZvbnQtd2VpZ2h0L
|
||||
WJvbGQiPkhFU0s8L3NwYW4+LCBpbiBwYXJ0bmVyc2hpcCB3aXRoIDxhIGhyZWY9Imh0dHBzOi8vd3d3L
|
||||
nN5c2FpZC5jb20vP3V0bV9zb3VyY2U9SGVzayZhbXA7dXRtX21lZGl1bT1jcGMmYW1wO3V0bV9jYW1wY
|
||||
Wlnbj1IZXNrUHJvZHVjdF9Ub19IUCIgY2xhc3M9ImxpbmsiPlN5c0FpZCBUZWNobm9sb2dpZXM8L2E+P
|
||||
C9wPg0KPC9mb290ZXI+DQo=',"\104", "347db01e129edd4b3877f70ea6fed019462ae827");
|
||||
/*******************************************************************************
|
||||
END LICENSE CODE
|
||||
*******************************************************************************/
|
||||
?>
|
||||
</main> <!-- End main -->
|
||||
<?php
|
||||
if (isset($login_wrapper)) {
|
||||
echo '</div> <!-- End wrapper login -->';
|
||||
}
|
||||
?>
|
||||
</div> <!-- End wrapper -->
|
||||
<input type="hidden" name="HESK_PATH" value="<?php echo HESK_PATH; ?>">
|
||||
<script src="<?php echo HESK_PATH; ?>js/svg4everybody.min.js"></script>
|
||||
<script src="<?php echo HESK_PATH; ?>js/jquery.scrollbar.min.js"></script>
|
||||
<script src="<?php echo HESK_PATH; ?>js/selectize.min.js"></script>
|
||||
<script src="<?php echo HESK_PATH; ?>js/datepicker.min.js"></script>
|
||||
<script src="<?php echo HESK_PATH; ?>js/datepicker.en.js"></script>
|
||||
<script src="<?php echo HESK_PATH; ?>js/jquery.autocomplete.js"></script>
|
||||
<script type="text/javascript" src="<?php echo HESK_PATH; ?>js/app<?php echo $hesk_settings['debug_mode'] ? '' : '.min'; ?>.js"></script>
|
||||
<?php
|
||||
|
||||
// Auto-select first empty or error field on non-staff pages?
|
||||
if (defined('AUTOFOCUS'))
|
||||
{
|
||||
?>
|
||||
<script language="javascript">
|
||||
(function(){
|
||||
var forms = document.forms || [];
|
||||
for(var i = 0; i < forms.length; i++)
|
||||
{
|
||||
for(var j = 0; j < forms[i].length; j++)
|
||||
{
|
||||
if(
|
||||
!forms[i][j].readonly != undefined &&
|
||||
forms[i][j].type != "hidden" &&
|
||||
forms[i][j].disabled != true &&
|
||||
forms[i][j].style.display != 'none' &&
|
||||
(forms[i][j].className == 'isError' || forms[i][j].className == 'isNotice' || forms[i][j].value == '')
|
||||
)
|
||||
{
|
||||
forms[i][j].focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '
|
||||
</body>
|
||||
</html>
|
||||
';
|
||||
|
||||
$hesk_settings['security_cleanup']('exit');
|
||||
143
hesk/inc/header.inc.php
Normal file
143
hesk/inc/header.inc.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/* Check if this is a valid include */
|
||||
if (!defined('IN_SCRIPT')) {die('Invalid attempt');}
|
||||
|
||||
// We'll use this later
|
||||
$onload='';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $hesk_settings['languages'][$hesk_settings['language']]['folder'] ?>">
|
||||
<head>
|
||||
<title><?php echo (isset($hesk_settings['tmp_title']) ? $hesk_settings['tmp_title'] : $hesk_settings['hesk_title']); ?></title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="stylesheet" media="all" href="<?php echo HESK_PATH; ?>css/app<?php echo $hesk_settings['debug_mode'] ? '' : '.min'; ?>.css?<?php echo $hesk_settings['hesk_version']; ?>">
|
||||
<script src="<?php echo HESK_PATH; ?>js/jquery-3.4.1.min.js"></script>
|
||||
<script type="text/javascript" src="<?php echo HESK_PATH; ?>js/hesk_javascript<?php echo $hesk_settings['debug_mode'] ? '' : '.min'; ?>.js?<?php echo $hesk_settings['hesk_version']; ?>"></script>
|
||||
|
||||
<?php
|
||||
/* Tickets shouldn't be indexed by search engines */
|
||||
if (defined('HESK_NO_ROBOTS'))
|
||||
{
|
||||
?>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<?php
|
||||
}
|
||||
|
||||
/* If page requires WYSIWYG editor include TinyMCE Javascript */
|
||||
if (defined('WYSIWYG') && $hesk_settings['kb_wysiwyg'])
|
||||
{
|
||||
?>
|
||||
<script type="text/javascript" src="<?php echo HESK_PATH; ?>inc/tiny_mce/5.2.0/tinymce.min.js"></script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* If page requires timer load Javascript */
|
||||
if (defined('TIMER'))
|
||||
{
|
||||
?>
|
||||
<script type="text/javascript" src="<?php echo HESK_PATH; ?>inc/timer/hesk_timer.js"></script>
|
||||
<?php
|
||||
|
||||
/* Need to load default time or a custom one? */
|
||||
if ( isset($_SESSION['time_worked']) )
|
||||
{
|
||||
$t = hesk_getHHMMSS($_SESSION['time_worked']);
|
||||
$onload .= "load_timer('time_worked', " . $t[0] . ", " . $t[1] . ", " . $t[2] . ");";
|
||||
unset($t);
|
||||
}
|
||||
else
|
||||
{
|
||||
$onload .= "load_timer('time_worked', 0, 0, 0);";
|
||||
}
|
||||
|
||||
/* Autostart timer? */
|
||||
if ( ! empty($_SESSION['autostart']) )
|
||||
{
|
||||
$onload .= "ss();";
|
||||
}
|
||||
}
|
||||
|
||||
// Use ReCaptcha
|
||||
if (defined('RECAPTCHA'))
|
||||
{
|
||||
echo '<script src="https://www.google.com/recaptcha/api.js?hl='.$hesklang['RECAPTCHA'].'" async defer></script>';
|
||||
echo '<script language="Javascript" type="text/javascript">
|
||||
function recaptcha_submitForm() {
|
||||
document.getElementById("form1").submit();
|
||||
}
|
||||
</script>';
|
||||
}
|
||||
|
||||
// Auto reload
|
||||
if (defined('AUTO_RELOAD') && hesk_checkPermission('can_view_tickets',0) && ! isset($_SESSION['hide']['ticket_list']) )
|
||||
{
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
var count = <?php echo empty($_SESSION['autoreload']) ? 30 : intval($_SESSION['autoreload']); ?>;
|
||||
var reloadcounter;
|
||||
var countstart = count;
|
||||
|
||||
function heskReloadTimer()
|
||||
{
|
||||
count=count-1;
|
||||
if (count <= 0)
|
||||
{
|
||||
clearInterval(reloadcounter);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("timer").innerHTML = "(" + count + ")";
|
||||
}
|
||||
|
||||
function heskCheckReloading()
|
||||
{
|
||||
if (<?php if ($_SESSION['autoreload']) echo "getCookie('autorefresh') == null || "; ?>getCookie('autorefresh') == '1')
|
||||
{
|
||||
document.getElementById("reloadCB").checked=true;
|
||||
document.getElementById("timer").innerHTML = "(" + count + ")";
|
||||
reloadcounter = setInterval(heskReloadTimer, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRefresh(cb)
|
||||
{
|
||||
if (cb.checked)
|
||||
{
|
||||
setCookie('autorefresh', '1');
|
||||
document.getElementById("timer").innerHTML = "(" + count + ")";
|
||||
reloadcounter = setInterval(heskReloadTimer, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setCookie('autorefresh', '0');
|
||||
count = countstart;
|
||||
clearInterval(reloadcounter);
|
||||
document.getElementById("timer").innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</head>
|
||||
<body onload="<?php echo $onload; unset($onload); ?>">
|
||||
|
||||
<div class="wrapper">
|
||||
22584
hesk/inc/htmlpurifier/HTMLPurifier.standalone.php
Normal file
22584
hesk/inc/htmlpurifier/HTMLPurifier.standalone.php
Normal file
File diff suppressed because it is too large
Load Diff
66
hesk/inc/htmlpurifier/HeskHTMLPurifier.php
Normal file
66
hesk/inc/htmlpurifier/HeskHTMLPurifier.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
require(HESK_PATH . 'inc/htmlpurifier/HTMLPurifier.standalone.php');
|
||||
|
||||
class HeskHTMLPurifier extends HTMLPurifier
|
||||
{
|
||||
private $allowIframes;
|
||||
private $cacheDir;
|
||||
|
||||
public function __construct($cacheDir = 'cache', $allowIframes = 1)
|
||||
{
|
||||
$this->allowIframes = $allowIframes;
|
||||
$this->cacheDir = $this->setupCacheDir($cacheDir);
|
||||
}
|
||||
|
||||
public function heskPurify($content)
|
||||
{
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Attr.AllowedRel', array('follow', 'referrer', 'nofollow', 'noreferrer') );
|
||||
$config->set('Attr.AllowedFrameTargets', array('_blank', '_self', '_parent', '_top') );
|
||||
$config->set('Cache.SerializerPath', $this->cacheDir);
|
||||
$config->set('URI.AllowedSchemes', array(
|
||||
'http' => true,
|
||||
'https' => true,
|
||||
'mailto' => true,
|
||||
'ftp' => true,
|
||||
'nntp' => true,
|
||||
'news' => true,
|
||||
'tel' => true,
|
||||
'data' => true,
|
||||
)
|
||||
);
|
||||
|
||||
if ($this->allowIframes)
|
||||
{
|
||||
require(HESK_PATH . 'inc/htmlpurifier/custom/heskIframe.php');
|
||||
$config->set('Filter.Custom', array(new HTMLPurifier_Filter_HeskIframe()));
|
||||
}
|
||||
|
||||
$purifier = new HTMLPurifier($config);
|
||||
return $purifier->purify($content);
|
||||
}
|
||||
|
||||
private function setupCacheDir($cache_dir)
|
||||
{
|
||||
$cache_dir = dirname(dirname(dirname(__FILE__))).'/'.$cache_dir.'/hp';
|
||||
|
||||
if (is_dir($cache_dir) || ( @mkdir($cache_dir, 0777) && is_writable($cache_dir) ) )
|
||||
{
|
||||
return $cache_dir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
71
hesk/inc/htmlpurifier/custom/heskIframe.php
Normal file
71
hesk/inc/htmlpurifier/custom/heskIframe.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of HESK - PHP Help Desk Software.
|
||||
*
|
||||
* (c) Copyright Klemen Stirn. All rights reserved.
|
||||
* https://www.hesk.com
|
||||
*
|
||||
* For the full copyright and license agreement information visit
|
||||
* https://www.hesk.com/eula.php
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Based on: http://sachachua.com/blog/2011/08/drupal-html-purifier-embedding-iframes-youtube/
|
||||
* Iframe filter that does some primitive whitelisting in a somewhat recognizable and tweakable way
|
||||
*/
|
||||
class HTMLPurifier_Filter_HeskIframe extends HTMLPurifier_Filter
|
||||
{
|
||||
public $name = 'HeskIframe';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $html
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
*/
|
||||
public function preFilter($html, $config, $context)
|
||||
{
|
||||
$html = preg_replace('#<iframe#i', '<img class="HeskIframe"', $html);
|
||||
$html = preg_replace('#</iframe>#i', '</img>', $html);
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $html
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
*/
|
||||
public function postFilter($html, $config, $context)
|
||||
{
|
||||
$post_regex = '#<img class="HeskIframe"([^>]+?)/?>#';
|
||||
return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $matches
|
||||
* @return string
|
||||
*/
|
||||
protected function postFilterCallback($matches)
|
||||
{
|
||||
// Domain Whitelist
|
||||
$youTubeMatch = preg_match('#src="https?://www.youtube(-nocookie)?.com/#i', $matches[1]);
|
||||
$vimeoMatch = preg_match('#src="https?://player.vimeo.com/#i', $matches[1]);
|
||||
if ($youTubeMatch || $vimeoMatch) {
|
||||
$extra = ' frameborder="0"';
|
||||
if ($youTubeMatch) {
|
||||
$extra .= ' allowfullscreen';
|
||||
} elseif ($vimeoMatch) {
|
||||
$extra .= ' webkitAllowFullScreen mozallowfullscreen allowFullScreen';
|
||||
}
|
||||
return '<iframe ' . $matches[1] . $extra . '></iframe>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
8
hesk/inc/htmlpurifier/custom/index.htm
Normal file
8
hesk/inc/htmlpurifier/custom/index.htm
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<HTML><HEAD>
|
||||
<TITLE>403 Forbidden</TITLE>
|
||||
</HEAD><BODY>
|
||||
<H1>Forbidden</H1>
|
||||
You don't have permission to access this folder.<P>
|
||||
<hr />
|
||||
</BODY></HTML>
|
||||
8
hesk/inc/htmlpurifier/index.htm
Normal file
8
hesk/inc/htmlpurifier/index.htm
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<HTML><HEAD>
|
||||
<TITLE>403 Forbidden</TITLE>
|
||||
</HEAD><BODY>
|
||||
<H1>Forbidden</H1>
|
||||
You don't have permission to access this folder.<P>
|
||||
<hr />
|
||||
</BODY></HTML>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Converts HTMLPurifier_ConfigSchema_Interchange to our runtime
|
||||
* representation used to perform checks on user configuration.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Builder_ConfigSchema
|
||||
{
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
* @return HTMLPurifier_ConfigSchema
|
||||
*/
|
||||
public function build($interchange)
|
||||
{
|
||||
$schema = new HTMLPurifier_ConfigSchema();
|
||||
foreach ($interchange->directives as $d) {
|
||||
$schema->add(
|
||||
$d->id->key,
|
||||
$d->default,
|
||||
$d->type,
|
||||
$d->typeAllowsNull
|
||||
);
|
||||
if ($d->allowed !== null) {
|
||||
$schema->addAllowedValues(
|
||||
$d->id->key,
|
||||
$d->allowed
|
||||
);
|
||||
}
|
||||
foreach ($d->aliases as $alias) {
|
||||
$schema->addAlias(
|
||||
$alias->key,
|
||||
$d->id->key
|
||||
);
|
||||
}
|
||||
if ($d->valueAliases !== null) {
|
||||
$schema->addValueAliases(
|
||||
$d->id->key,
|
||||
$d->valueAliases
|
||||
);
|
||||
}
|
||||
}
|
||||
$schema->postProcess();
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Converts HTMLPurifier_ConfigSchema_Interchange to an XML format,
|
||||
* which can be further processed to generate documentation.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Builder_Xml extends XMLWriter
|
||||
{
|
||||
|
||||
/**
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange
|
||||
*/
|
||||
protected $interchange;
|
||||
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*/
|
||||
protected function writeHTMLDiv($html)
|
||||
{
|
||||
$this->startElement('div');
|
||||
|
||||
$purifier = HTMLPurifier::getInstance();
|
||||
$html = $purifier->purify($html);
|
||||
$this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||
$this->writeRaw($html);
|
||||
|
||||
$this->endElement(); // div
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $var
|
||||
* @return string
|
||||
*/
|
||||
protected function export($var)
|
||||
{
|
||||
if ($var === array()) {
|
||||
return 'array()';
|
||||
}
|
||||
return var_export($var, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
*/
|
||||
public function build($interchange)
|
||||
{
|
||||
// global access, only use as last resort
|
||||
$this->interchange = $interchange;
|
||||
|
||||
$this->setIndent(true);
|
||||
$this->startDocument('1.0', 'UTF-8');
|
||||
$this->startElement('configdoc');
|
||||
$this->writeElement('title', $interchange->name);
|
||||
|
||||
foreach ($interchange->directives as $directive) {
|
||||
$this->buildDirective($directive);
|
||||
}
|
||||
|
||||
if ($this->namespace) {
|
||||
$this->endElement();
|
||||
} // namespace
|
||||
|
||||
$this->endElement(); // configdoc
|
||||
$this->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
|
||||
*/
|
||||
public function buildDirective($directive)
|
||||
{
|
||||
// Kludge, although I suppose having a notion of a "root namespace"
|
||||
// certainly makes things look nicer when documentation is built.
|
||||
// Depends on things being sorted.
|
||||
if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) {
|
||||
if ($this->namespace) {
|
||||
$this->endElement();
|
||||
} // namespace
|
||||
$this->namespace = $directive->id->getRootNamespace();
|
||||
$this->startElement('namespace');
|
||||
$this->writeAttribute('id', $this->namespace);
|
||||
$this->writeElement('name', $this->namespace);
|
||||
}
|
||||
|
||||
$this->startElement('directive');
|
||||
$this->writeAttribute('id', $directive->id->toString());
|
||||
|
||||
$this->writeElement('name', $directive->id->getDirective());
|
||||
|
||||
$this->startElement('aliases');
|
||||
foreach ($directive->aliases as $alias) {
|
||||
$this->writeElement('alias', $alias->toString());
|
||||
}
|
||||
$this->endElement(); // aliases
|
||||
|
||||
$this->startElement('constraints');
|
||||
if ($directive->version) {
|
||||
$this->writeElement('version', $directive->version);
|
||||
}
|
||||
$this->startElement('type');
|
||||
if ($directive->typeAllowsNull) {
|
||||
$this->writeAttribute('allow-null', 'yes');
|
||||
}
|
||||
$this->text($directive->type);
|
||||
$this->endElement(); // type
|
||||
if ($directive->allowed) {
|
||||
$this->startElement('allowed');
|
||||
foreach ($directive->allowed as $value => $x) {
|
||||
$this->writeElement('value', $value);
|
||||
}
|
||||
$this->endElement(); // allowed
|
||||
}
|
||||
$this->writeElement('default', $this->export($directive->default));
|
||||
$this->writeAttribute('xml:space', 'preserve');
|
||||
if ($directive->external) {
|
||||
$this->startElement('external');
|
||||
foreach ($directive->external as $project) {
|
||||
$this->writeElement('project', $project);
|
||||
}
|
||||
$this->endElement();
|
||||
}
|
||||
$this->endElement(); // constraints
|
||||
|
||||
if ($directive->deprecatedVersion) {
|
||||
$this->startElement('deprecated');
|
||||
$this->writeElement('version', $directive->deprecatedVersion);
|
||||
$this->writeElement('use', $directive->deprecatedUse->toString());
|
||||
$this->endElement(); // deprecated
|
||||
}
|
||||
|
||||
$this->startElement('description');
|
||||
$this->writeHTMLDiv($directive->description);
|
||||
$this->endElement(); // description
|
||||
|
||||
$this->endElement(); // directive
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Exceptions related to configuration schema
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Generic schema interchange format that can be converted to a runtime
|
||||
* representation (HTMLPurifier_ConfigSchema) or HTML documentation. Members
|
||||
* are completely validated.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Interchange
|
||||
{
|
||||
|
||||
/**
|
||||
* Name of the application this schema is describing.
|
||||
* @type string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Array of Directive ID => array(directive info)
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange_Directive[]
|
||||
*/
|
||||
public $directives = array();
|
||||
|
||||
/**
|
||||
* Adds a directive array to $directives
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
|
||||
* @throws HTMLPurifier_ConfigSchema_Exception
|
||||
*/
|
||||
public function addDirective($directive)
|
||||
{
|
||||
if (isset($this->directives[$i = $directive->id->toString()])) {
|
||||
throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'");
|
||||
}
|
||||
$this->directives[$i] = $directive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to perform standard validation. Throws exception
|
||||
* on failed validation.
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
$validator = new HTMLPurifier_ConfigSchema_Validator();
|
||||
return $validator->validate($this);
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Interchange component class describing configuration directives.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Interchange_Directive
|
||||
{
|
||||
|
||||
/**
|
||||
* ID of directive.
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Type, e.g. 'integer' or 'istring'.
|
||||
* @type string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Default value, e.g. 3 or 'DefaultVal'.
|
||||
* @type mixed
|
||||
*/
|
||||
public $default;
|
||||
|
||||
/**
|
||||
* HTML description.
|
||||
* @type string
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* Whether or not null is allowed as a value.
|
||||
* @type bool
|
||||
*/
|
||||
public $typeAllowsNull = false;
|
||||
|
||||
/**
|
||||
* Lookup table of allowed scalar values.
|
||||
* e.g. array('allowed' => true).
|
||||
* Null if all values are allowed.
|
||||
* @type array
|
||||
*/
|
||||
public $allowed;
|
||||
|
||||
/**
|
||||
* List of aliases for the directive.
|
||||
* e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))).
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange_Id[]
|
||||
*/
|
||||
public $aliases = array();
|
||||
|
||||
/**
|
||||
* Hash of value aliases, e.g. array('alt' => 'real'). Null if value
|
||||
* aliasing is disabled (necessary for non-scalar types).
|
||||
* @type array
|
||||
*/
|
||||
public $valueAliases;
|
||||
|
||||
/**
|
||||
* Version of HTML Purifier the directive was introduced, e.g. '1.3.1'.
|
||||
* Null if the directive has always existed.
|
||||
* @type string
|
||||
*/
|
||||
public $version;
|
||||
|
||||
/**
|
||||
* ID of directive that supercedes this old directive.
|
||||
* Null if not deprecated.
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
*/
|
||||
public $deprecatedUse;
|
||||
|
||||
/**
|
||||
* Version of HTML Purifier this directive was deprecated. Null if not
|
||||
* deprecated.
|
||||
* @type string
|
||||
*/
|
||||
public $deprecatedVersion;
|
||||
|
||||
/**
|
||||
* List of external projects this directive depends on, e.g. array('CSSTidy').
|
||||
* @type array
|
||||
*/
|
||||
public $external = array();
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a directive ID in the interchange format.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
{
|
||||
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
public $key;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function __construct($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @warning This is NOT magic, to ensure that people don't abuse SPL and
|
||||
* cause problems for PHP 5.0 support.
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRootNamespace()
|
||||
{
|
||||
return substr($this->key, 0, strpos($this->key, "."));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDirective()
|
||||
{
|
||||
return substr($this->key, strpos($this->key, ".") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
*/
|
||||
public static function make($id)
|
||||
{
|
||||
return new HTMLPurifier_ConfigSchema_Interchange_Id($id);
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
class HTMLPurifier_ConfigSchema_InterchangeBuilder
|
||||
{
|
||||
|
||||
/**
|
||||
* Used for processing DEFAULT, nothing else.
|
||||
* @type HTMLPurifier_VarParser
|
||||
*/
|
||||
protected $varParser;
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_VarParser $varParser
|
||||
*/
|
||||
public function __construct($varParser = null)
|
||||
{
|
||||
$this->varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
* @return HTMLPurifier_ConfigSchema_Interchange
|
||||
*/
|
||||
public static function buildFromDirectory($dir = null)
|
||||
{
|
||||
$builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder();
|
||||
$interchange = new HTMLPurifier_ConfigSchema_Interchange();
|
||||
return $builder->buildDir($interchange, $dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
* @param string $dir
|
||||
* @return HTMLPurifier_ConfigSchema_Interchange
|
||||
*/
|
||||
public function buildDir($interchange, $dir = null)
|
||||
{
|
||||
if (!$dir) {
|
||||
$dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema';
|
||||
}
|
||||
if (file_exists($dir . '/info.ini')) {
|
||||
$info = parse_ini_file($dir . '/info.ini');
|
||||
$interchange->name = $info['name'];
|
||||
}
|
||||
|
||||
$files = array();
|
||||
$dh = opendir($dir);
|
||||
while (false !== ($file = readdir($dh))) {
|
||||
if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') {
|
||||
continue;
|
||||
}
|
||||
$files[] = $file;
|
||||
}
|
||||
closedir($dh);
|
||||
|
||||
sort($files);
|
||||
foreach ($files as $file) {
|
||||
$this->buildFile($interchange, $dir . '/' . $file);
|
||||
}
|
||||
return $interchange;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
* @param string $file
|
||||
*/
|
||||
public function buildFile($interchange, $file)
|
||||
{
|
||||
$parser = new HTMLPurifier_StringHashParser();
|
||||
$this->build(
|
||||
$interchange,
|
||||
new HTMLPurifier_StringHash($parser->parseFile($file))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an interchange object based on a hash.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build
|
||||
* @param HTMLPurifier_StringHash $hash source data
|
||||
* @throws HTMLPurifier_ConfigSchema_Exception
|
||||
*/
|
||||
public function build($interchange, $hash)
|
||||
{
|
||||
if (!$hash instanceof HTMLPurifier_StringHash) {
|
||||
$hash = new HTMLPurifier_StringHash($hash);
|
||||
}
|
||||
if (!isset($hash['ID'])) {
|
||||
throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID');
|
||||
}
|
||||
if (strpos($hash['ID'], '.') === false) {
|
||||
if (count($hash) == 2 && isset($hash['DESCRIPTION'])) {
|
||||
$hash->offsetGet('DESCRIPTION'); // prevent complaining
|
||||
} else {
|
||||
throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace');
|
||||
}
|
||||
} else {
|
||||
$this->buildDirective($interchange, $hash);
|
||||
}
|
||||
$this->_findUnused($hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
* @param HTMLPurifier_StringHash $hash
|
||||
* @throws HTMLPurifier_ConfigSchema_Exception
|
||||
*/
|
||||
public function buildDirective($interchange, $hash)
|
||||
{
|
||||
$directive = new HTMLPurifier_ConfigSchema_Interchange_Directive();
|
||||
|
||||
// These are required elements:
|
||||
$directive->id = $this->id($hash->offsetGet('ID'));
|
||||
$id = $directive->id->toString(); // convenience
|
||||
|
||||
if (isset($hash['TYPE'])) {
|
||||
$type = explode('/', $hash->offsetGet('TYPE'));
|
||||
if (isset($type[1])) {
|
||||
$directive->typeAllowsNull = true;
|
||||
}
|
||||
$directive->type = $type[0];
|
||||
} else {
|
||||
throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined");
|
||||
}
|
||||
|
||||
if (isset($hash['DEFAULT'])) {
|
||||
try {
|
||||
$directive->default = $this->varParser->parse(
|
||||
$hash->offsetGet('DEFAULT'),
|
||||
$directive->type,
|
||||
$directive->typeAllowsNull
|
||||
);
|
||||
} catch (HTMLPurifier_VarParserException $e) {
|
||||
throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'");
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($hash['DESCRIPTION'])) {
|
||||
$directive->description = $hash->offsetGet('DESCRIPTION');
|
||||
}
|
||||
|
||||
if (isset($hash['ALLOWED'])) {
|
||||
$directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED')));
|
||||
}
|
||||
|
||||
if (isset($hash['VALUE-ALIASES'])) {
|
||||
$directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES'));
|
||||
}
|
||||
|
||||
if (isset($hash['ALIASES'])) {
|
||||
$raw_aliases = trim($hash->offsetGet('ALIASES'));
|
||||
$aliases = preg_split('/\s*,\s*/', $raw_aliases);
|
||||
foreach ($aliases as $alias) {
|
||||
$directive->aliases[] = $this->id($alias);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($hash['VERSION'])) {
|
||||
$directive->version = $hash->offsetGet('VERSION');
|
||||
}
|
||||
|
||||
if (isset($hash['DEPRECATED-USE'])) {
|
||||
$directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE'));
|
||||
}
|
||||
|
||||
if (isset($hash['DEPRECATED-VERSION'])) {
|
||||
$directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION');
|
||||
}
|
||||
|
||||
if (isset($hash['EXTERNAL'])) {
|
||||
$directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL')));
|
||||
}
|
||||
|
||||
$interchange->addDirective($directive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates an array PHP code string without array() wrapper
|
||||
* @param string $contents
|
||||
*/
|
||||
protected function evalArray($contents)
|
||||
{
|
||||
return eval('return array(' . $contents . ');');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array list into a lookup array.
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
protected function lookup($array)
|
||||
{
|
||||
$ret = array();
|
||||
foreach ($array as $val) {
|
||||
$ret[$val] = true;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
* object based on a string Id.
|
||||
* @param string $id
|
||||
* @return HTMLPurifier_ConfigSchema_Interchange_Id
|
||||
*/
|
||||
protected function id($id)
|
||||
{
|
||||
return HTMLPurifier_ConfigSchema_Interchange_Id::make($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers errors for any unused keys passed in the hash; such keys
|
||||
* may indicate typos, missing values, etc.
|
||||
* @param HTMLPurifier_StringHash $hash Hash to check.
|
||||
*/
|
||||
protected function _findUnused($hash)
|
||||
{
|
||||
$accessed = $hash->getAccessed();
|
||||
foreach ($hash as $k => $v) {
|
||||
if (!isset($accessed[$k])) {
|
||||
trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Performs validations on HTMLPurifier_ConfigSchema_Interchange
|
||||
*
|
||||
* @note If you see '// handled by InterchangeBuilder', that means a
|
||||
* design decision in that class would prevent this validation from
|
||||
* ever being necessary. We have them anyway, however, for
|
||||
* redundancy.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Validator
|
||||
{
|
||||
|
||||
/**
|
||||
* @type HTMLPurifier_ConfigSchema_Interchange
|
||||
*/
|
||||
protected $interchange;
|
||||
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
protected $aliases;
|
||||
|
||||
/**
|
||||
* Context-stack to provide easy to read error messages.
|
||||
* @type array
|
||||
*/
|
||||
protected $context = array();
|
||||
|
||||
/**
|
||||
* to test default's type.
|
||||
* @type HTMLPurifier_VarParser
|
||||
*/
|
||||
protected $parser;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->parser = new HTMLPurifier_VarParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a fully-formed interchange object.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
|
||||
* @return bool
|
||||
*/
|
||||
public function validate($interchange)
|
||||
{
|
||||
$this->interchange = $interchange;
|
||||
$this->aliases = array();
|
||||
// PHP is a bit lax with integer <=> string conversions in
|
||||
// arrays, so we don't use the identical !== comparison
|
||||
foreach ($interchange->directives as $i => $directive) {
|
||||
$id = $directive->id->toString();
|
||||
if ($i != $id) {
|
||||
$this->error(false, "Integrity violation: key '$i' does not match internal id '$id'");
|
||||
}
|
||||
$this->validateDirective($directive);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a HTMLPurifier_ConfigSchema_Interchange_Id object.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Id $id
|
||||
*/
|
||||
public function validateId($id)
|
||||
{
|
||||
$id_string = $id->toString();
|
||||
$this->context[] = "id '$id_string'";
|
||||
if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) {
|
||||
// handled by InterchangeBuilder
|
||||
$this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id');
|
||||
}
|
||||
// keys are now unconstrained (we might want to narrow down to A-Za-z0-9.)
|
||||
// we probably should check that it has at least one namespace
|
||||
$this->with($id, 'key')
|
||||
->assertNotEmpty()
|
||||
->assertIsString(); // implicit assertIsString handled by InterchangeBuilder
|
||||
array_pop($this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
|
||||
*/
|
||||
public function validateDirective($d)
|
||||
{
|
||||
$id = $d->id->toString();
|
||||
$this->context[] = "directive '$id'";
|
||||
$this->validateId($d->id);
|
||||
|
||||
$this->with($d, 'description')
|
||||
->assertNotEmpty();
|
||||
|
||||
// BEGIN - handled by InterchangeBuilder
|
||||
$this->with($d, 'type')
|
||||
->assertNotEmpty();
|
||||
$this->with($d, 'typeAllowsNull')
|
||||
->assertIsBool();
|
||||
try {
|
||||
// This also tests validity of $d->type
|
||||
$this->parser->parse($d->default, $d->type, $d->typeAllowsNull);
|
||||
} catch (HTMLPurifier_VarParserException $e) {
|
||||
$this->error('default', 'had error: ' . $e->getMessage());
|
||||
}
|
||||
// END - handled by InterchangeBuilder
|
||||
|
||||
if (!is_null($d->allowed) || !empty($d->valueAliases)) {
|
||||
// allowed and valueAliases require that we be dealing with
|
||||
// strings, so check for that early.
|
||||
$d_int = HTMLPurifier_VarParser::$types[$d->type];
|
||||
if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) {
|
||||
$this->error('type', 'must be a string type when used with allowed or value aliases');
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateDirectiveAllowed($d);
|
||||
$this->validateDirectiveValueAliases($d);
|
||||
$this->validateDirectiveAliases($d);
|
||||
|
||||
array_pop($this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra validation if $allowed member variable of
|
||||
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
|
||||
*/
|
||||
public function validateDirectiveAllowed($d)
|
||||
{
|
||||
if (is_null($d->allowed)) {
|
||||
return;
|
||||
}
|
||||
$this->with($d, 'allowed')
|
||||
->assertNotEmpty()
|
||||
->assertIsLookup(); // handled by InterchangeBuilder
|
||||
if (is_string($d->default) && !isset($d->allowed[$d->default])) {
|
||||
$this->error('default', 'must be an allowed value');
|
||||
}
|
||||
$this->context[] = 'allowed';
|
||||
foreach ($d->allowed as $val => $x) {
|
||||
if (!is_string($val)) {
|
||||
$this->error("value $val", 'must be a string');
|
||||
}
|
||||
}
|
||||
array_pop($this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra validation if $valueAliases member variable of
|
||||
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
|
||||
*/
|
||||
public function validateDirectiveValueAliases($d)
|
||||
{
|
||||
if (is_null($d->valueAliases)) {
|
||||
return;
|
||||
}
|
||||
$this->with($d, 'valueAliases')
|
||||
->assertIsArray(); // handled by InterchangeBuilder
|
||||
$this->context[] = 'valueAliases';
|
||||
foreach ($d->valueAliases as $alias => $real) {
|
||||
if (!is_string($alias)) {
|
||||
$this->error("alias $alias", 'must be a string');
|
||||
}
|
||||
if (!is_string($real)) {
|
||||
$this->error("alias target $real from alias '$alias'", 'must be a string');
|
||||
}
|
||||
if ($alias === $real) {
|
||||
$this->error("alias '$alias'", "must not be an alias to itself");
|
||||
}
|
||||
}
|
||||
if (!is_null($d->allowed)) {
|
||||
foreach ($d->valueAliases as $alias => $real) {
|
||||
if (isset($d->allowed[$alias])) {
|
||||
$this->error("alias '$alias'", 'must not be an allowed value');
|
||||
} elseif (!isset($d->allowed[$real])) {
|
||||
$this->error("alias '$alias'", 'must be an alias to an allowed value');
|
||||
}
|
||||
}
|
||||
}
|
||||
array_pop($this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra validation if $aliases member variable of
|
||||
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
|
||||
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
|
||||
*/
|
||||
public function validateDirectiveAliases($d)
|
||||
{
|
||||
$this->with($d, 'aliases')
|
||||
->assertIsArray(); // handled by InterchangeBuilder
|
||||
$this->context[] = 'aliases';
|
||||
foreach ($d->aliases as $alias) {
|
||||
$this->validateId($alias);
|
||||
$s = $alias->toString();
|
||||
if (isset($this->interchange->directives[$s])) {
|
||||
$this->error("alias '$s'", 'collides with another directive');
|
||||
}
|
||||
if (isset($this->aliases[$s])) {
|
||||
$other_directive = $this->aliases[$s];
|
||||
$this->error("alias '$s'", "collides with alias for directive '$other_directive'");
|
||||
}
|
||||
$this->aliases[$s] = $d->id->toString();
|
||||
}
|
||||
array_pop($this->context);
|
||||
}
|
||||
|
||||
// protected helper functions
|
||||
|
||||
/**
|
||||
* Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
* for validating simple member variables of objects.
|
||||
* @param $obj
|
||||
* @param $member
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
protected function with($obj, $member)
|
||||
{
|
||||
return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an error, providing helpful context.
|
||||
* @throws HTMLPurifier_ConfigSchema_Exception
|
||||
*/
|
||||
protected function error($target, $msg)
|
||||
{
|
||||
if ($target !== false) {
|
||||
$prefix = ucfirst($target) . ' in ' . $this->getFormattedContext();
|
||||
} else {
|
||||
$prefix = ucfirst($this->getFormattedContext());
|
||||
}
|
||||
throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted context string.
|
||||
* @return string
|
||||
*/
|
||||
protected function getFormattedContext()
|
||||
{
|
||||
return implode(' in ', array_reverse($this->context));
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Fluent interface for validating the contents of member variables.
|
||||
* This should be immutable. See HTMLPurifier_ConfigSchema_Validator for
|
||||
* use-cases. We name this an 'atom' because it's ONLY for validations that
|
||||
* are independent and usually scalar.
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
{
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
protected $obj;
|
||||
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* @type mixed
|
||||
*/
|
||||
protected $contents;
|
||||
|
||||
public function __construct($context, $obj, $member)
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->obj = $obj;
|
||||
$this->member = $member;
|
||||
$this->contents =& $obj->$member;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertIsString()
|
||||
{
|
||||
if (!is_string($this->contents)) {
|
||||
$this->error('must be a string');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertIsBool()
|
||||
{
|
||||
if (!is_bool($this->contents)) {
|
||||
$this->error('must be a boolean');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertIsArray()
|
||||
{
|
||||
if (!is_array($this->contents)) {
|
||||
$this->error('must be an array');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertNotNull()
|
||||
{
|
||||
if ($this->contents === null) {
|
||||
$this->error('must not be null');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertAlnum()
|
||||
{
|
||||
$this->assertIsString();
|
||||
if (!ctype_alnum($this->contents)) {
|
||||
$this->error('must be alphanumeric');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertNotEmpty()
|
||||
{
|
||||
if (empty($this->contents)) {
|
||||
$this->error('must not be empty');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
|
||||
*/
|
||||
public function assertIsLookup()
|
||||
{
|
||||
$this->assertIsArray();
|
||||
foreach ($this->contents as $v) {
|
||||
if ($v !== true) {
|
||||
$this->error('must be a lookup array');
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $msg
|
||||
* @throws HTMLPurifier_ConfigSchema_Exception
|
||||
*/
|
||||
protected function error($msg)
|
||||
{
|
||||
throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg);
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
Attr.AllowedClasses
|
||||
TYPE: lookup/null
|
||||
VERSION: 4.0.0
|
||||
DEFAULT: null
|
||||
--DESCRIPTION--
|
||||
List of allowed class values in the class attribute. By default, this is null,
|
||||
which means all classes are allowed.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Attr.AllowedFrameTargets
|
||||
TYPE: lookup
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
Lookup table of all allowed link frame targets. Some commonly used link
|
||||
targets include _blank, _self, _parent and _top. Values should be
|
||||
lowercase, as validation will be done in a case-sensitive manner despite
|
||||
W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute
|
||||
so this directive will have no effect in that doctype. XHTML 1.1 does not
|
||||
enable the Target module by default, you will have to manually enable it
|
||||
(see the module documentation for more details.)
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
Attr.AllowedRel
|
||||
TYPE: lookup
|
||||
VERSION: 1.6.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
List of allowed forward document relationships in the rel attribute. Common
|
||||
values may be nofollow or print. By default, this is empty, meaning that no
|
||||
document relationships are allowed.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
Attr.AllowedRev
|
||||
TYPE: lookup
|
||||
VERSION: 1.6.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
List of allowed reverse document relationships in the rev attribute. This
|
||||
attribute is a bit of an edge-case; if you don't know what it is for, stay
|
||||
away.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,19 @@
|
||||
Attr.ClassUseCDATA
|
||||
TYPE: bool/null
|
||||
DEFAULT: null
|
||||
VERSION: 4.0.0
|
||||
--DESCRIPTION--
|
||||
If null, class will auto-detect the doctype and, if matching XHTML 1.1 or
|
||||
XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise,
|
||||
it will use a relaxed CDATA definition. If true, the relaxed CDATA definition
|
||||
is forced; if false, the NMTOKENS definition is forced. To get behavior
|
||||
of HTML Purifier prior to 4.0.0, set this directive to false.
|
||||
|
||||
Some rational behind the auto-detection:
|
||||
in previous versions of HTML Purifier, it was assumed that the form of
|
||||
class was NMTOKENS, as specified by the XHTML Modularization (representing
|
||||
XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however
|
||||
specify class as CDATA. HTML 5 effectively defines it as CDATA, but
|
||||
with the additional constraint that each name should be unique (this is not
|
||||
explicitly outlined in previous specifications).
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
Attr.DefaultImageAlt
|
||||
TYPE: string/null
|
||||
DEFAULT: null
|
||||
VERSION: 3.2.0
|
||||
--DESCRIPTION--
|
||||
This is the content of the alt tag of an image if the user had not
|
||||
previously specified an alt attribute. This applies to all images without
|
||||
a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which
|
||||
only applies to invalid images, and overrides in the case of an invalid image.
|
||||
Default behavior with null is to use the basename of the src tag for the alt.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
Attr.DefaultInvalidImage
|
||||
TYPE: string
|
||||
DEFAULT: ''
|
||||
--DESCRIPTION--
|
||||
This is the default image an img tag will be pointed to if it does not have
|
||||
a valid src attribute. In future versions, we may allow the image tag to
|
||||
be removed completely, but due to design issues, this is not possible right
|
||||
now.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,8 @@
|
||||
Attr.DefaultInvalidImageAlt
|
||||
TYPE: string
|
||||
DEFAULT: 'Invalid image'
|
||||
--DESCRIPTION--
|
||||
This is the content of the alt tag of an invalid image if the user had not
|
||||
previously specified an alt attribute. It has no effect when the image is
|
||||
valid but there was no alt attribute present.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,10 @@
|
||||
Attr.DefaultTextDir
|
||||
TYPE: string
|
||||
DEFAULT: 'ltr'
|
||||
--DESCRIPTION--
|
||||
Defines the default text direction (ltr or rtl) of the document being
|
||||
parsed. This generally is the same as the value of the dir attribute in
|
||||
HTML, or ltr if that is not specified.
|
||||
--ALLOWED--
|
||||
'ltr', 'rtl'
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Attr.EnableID
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 1.2.0
|
||||
--DESCRIPTION--
|
||||
Allows the ID attribute in HTML. This is disabled by default due to the
|
||||
fact that without proper configuration user input can easily break the
|
||||
validation of a webpage by specifying an ID that is already on the
|
||||
surrounding HTML. If you don't mind throwing caution to the wind, enable
|
||||
this directive, but I strongly recommend you also consider blacklisting IDs
|
||||
you use (%Attr.IDBlacklist) or prefixing all user supplied IDs
|
||||
(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of
|
||||
pre-1.2.0 versions.
|
||||
--ALIASES--
|
||||
HTML.EnableAttrID
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,8 @@
|
||||
Attr.ForbiddenClasses
|
||||
TYPE: lookup
|
||||
VERSION: 4.0.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
List of forbidden class values in the class attribute. By default, this is
|
||||
empty, which means that no classes are forbidden. See also %Attr.AllowedClasses.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,10 @@
|
||||
Attr.ID.HTML5
|
||||
TYPE: bool/null
|
||||
DEFAULT: null
|
||||
VERSION: 4.8.0
|
||||
--DESCRIPTION--
|
||||
In HTML5, restrictions on the format of the id attribute have been significantly
|
||||
relaxed, such that any string is valid so long as it contains no spaces and
|
||||
is at least one character. In lieu of a general HTML5 compatibility flag,
|
||||
set this configuration directive to true to use the relaxed rules.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,5 @@
|
||||
Attr.IDBlacklist
|
||||
TYPE: list
|
||||
DEFAULT: array()
|
||||
DESCRIPTION: Array of IDs not allowed in the document.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
Attr.IDBlacklistRegexp
|
||||
TYPE: string/null
|
||||
VERSION: 1.6.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
PCRE regular expression to be matched against all IDs. If the expression is
|
||||
matches, the ID is rejected. Use this with care: may cause significant
|
||||
degradation. ID matching is done after all other validation.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Attr.IDPrefix
|
||||
TYPE: string
|
||||
VERSION: 1.2.0
|
||||
DEFAULT: ''
|
||||
--DESCRIPTION--
|
||||
String to prefix to IDs. If you have no idea what IDs your pages may use,
|
||||
you may opt to simply add a prefix to all user-submitted ID attributes so
|
||||
that they are still usable, but will not conflict with core page IDs.
|
||||
Example: setting the directive to 'user_' will result in a user submitted
|
||||
'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true
|
||||
before using this.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
Attr.IDPrefixLocal
|
||||
TYPE: string
|
||||
VERSION: 1.2.0
|
||||
DEFAULT: ''
|
||||
--DESCRIPTION--
|
||||
Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you
|
||||
need to allow multiple sets of user content on web page, you may need to
|
||||
have a seperate prefix that changes with each iteration. This way,
|
||||
seperately submitted user content displayed on the same page doesn't
|
||||
clobber each other. Ideal values are unique identifiers for the content it
|
||||
represents (i.e. the id of the row in the database). Be sure to add a
|
||||
seperator (like an underscore) at the end. Warning: this directive will
|
||||
not work unless %Attr.IDPrefix is set to a non-empty value!
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,31 @@
|
||||
AutoFormat.AutoParagraph
|
||||
TYPE: bool
|
||||
VERSION: 2.0.1
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This directive turns on auto-paragraphing, where double newlines are
|
||||
converted in to paragraphs whenever possible. Auto-paragraphing:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Always applies to inline elements or text in the root node,</li>
|
||||
<li>Applies to inline elements or text with double newlines in nodes
|
||||
that allow paragraph tags,</li>
|
||||
<li>Applies to double newlines in paragraph tags</li>
|
||||
</ul>
|
||||
<p>
|
||||
<code>p</code> tags must be allowed for this directive to take effect.
|
||||
We do not use <code>br</code> tags for paragraphing, as that is
|
||||
semantically incorrect.
|
||||
</p>
|
||||
<p>
|
||||
To prevent auto-paragraphing as a content-producer, refrain from using
|
||||
double-newlines except to specify a new paragraph or in contexts where
|
||||
it has special meaning (whitespace usually has no meaning except in
|
||||
tags like <code>pre</code>, so this should not be difficult.) To prevent
|
||||
the paragraphing of inline text adjacent to block elements, wrap them
|
||||
in <code>div</code> tags (the behavior is slightly different outside of
|
||||
the root node.)
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
AutoFormat.Custom
|
||||
TYPE: list
|
||||
VERSION: 2.0.1
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This directive can be used to add custom auto-format injectors.
|
||||
Specify an array of injector names (class name minus the prefix)
|
||||
or concrete implementations. Injector class must exist.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
AutoFormat.DisplayLinkURI
|
||||
TYPE: bool
|
||||
VERSION: 3.2.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive turns on the in-text display of URIs in <a> tags, and disables
|
||||
those links. For example, <a href="http://example.com">example</a> becomes
|
||||
example (<a>http://example.com</a>).
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
AutoFormat.Linkify
|
||||
TYPE: bool
|
||||
VERSION: 2.0.1
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This directive turns on linkification, auto-linking http, ftp and
|
||||
https URLs. <code>a</code> tags with the <code>href</code> attribute
|
||||
must be allowed.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
AutoFormat.PurifierLinkify.DocURL
|
||||
TYPE: string
|
||||
VERSION: 2.0.1
|
||||
DEFAULT: '#%s'
|
||||
ALIASES: AutoFormatParam.PurifierLinkifyDocURL
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Location of configuration documentation to link to, let %s substitute
|
||||
into the configuration's namespace and directive names sans the percent
|
||||
sign.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
AutoFormat.PurifierLinkify
|
||||
TYPE: bool
|
||||
VERSION: 2.0.1
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Internal auto-formatter that converts configuration directives in
|
||||
syntax <a>%Namespace.Directive</a> to links. <code>a</code> tags
|
||||
with the <code>href</code> attribute must be allowed.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
AutoFormat.RemoveEmpty.Predicate
|
||||
TYPE: hash
|
||||
VERSION: 4.7.0
|
||||
DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src'))
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Given that an element has no contents, it will be removed by default, unless
|
||||
this predicate dictates otherwise. The predicate can either be an associative
|
||||
map from tag name to list of attributes that must be present for the element
|
||||
to be considered preserved: thus, the default always preserves <code>colgroup</code>,
|
||||
<code>th</code> and <code>td</code>, and also <code>iframe</code> if it
|
||||
has a <code>src</code>.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions
|
||||
TYPE: lookup
|
||||
VERSION: 4.0.0
|
||||
DEFAULT: array('td' => true, 'th' => true)
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp
|
||||
are enabled, this directive defines what HTML elements should not be
|
||||
removede if they have only a non-breaking space in them.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,15 @@
|
||||
AutoFormat.RemoveEmpty.RemoveNbsp
|
||||
TYPE: bool
|
||||
VERSION: 4.0.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
When enabled, HTML Purifier will treat any elements that contain only
|
||||
non-breaking spaces as well as regular whitespace as empty, and remove
|
||||
them when %AutoFormat.RemoveEmpty is enabled.
|
||||
</p>
|
||||
<p>
|
||||
See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements
|
||||
that don't have this behavior applied to them.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,46 @@
|
||||
AutoFormat.RemoveEmpty
|
||||
TYPE: bool
|
||||
VERSION: 3.2.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
When enabled, HTML Purifier will attempt to remove empty elements that
|
||||
contribute no semantic information to the document. The following types
|
||||
of nodes will be removed:
|
||||
</p>
|
||||
<ul><li>
|
||||
Tags with no attributes and no content, and that are not empty
|
||||
elements (remove <code><a></a></code> but not
|
||||
<code><br /></code>), and
|
||||
</li>
|
||||
<li>
|
||||
Tags with no content, except for:<ul>
|
||||
<li>The <code>colgroup</code> element, or</li>
|
||||
<li>
|
||||
Elements with the <code>id</code> or <code>name</code> attribute,
|
||||
when those attributes are permitted on those elements.
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
<p>
|
||||
Please be very careful when using this functionality; while it may not
|
||||
seem that empty elements contain useful information, they can alter the
|
||||
layout of a document given appropriate styling. This directive is most
|
||||
useful when you are processing machine-generated HTML, please avoid using
|
||||
it on regular user HTML.
|
||||
</p>
|
||||
<p>
|
||||
Elements that contain only whitespace will be treated as empty. Non-breaking
|
||||
spaces, however, do not count as whitespace. See
|
||||
%AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior.
|
||||
</p>
|
||||
<p>
|
||||
This algorithm is not perfect; you may still notice some empty tags,
|
||||
particularly if a node had elements, but those elements were later removed
|
||||
because they were not permitted in that context, or tags that, after
|
||||
being auto-closed by another tag, where empty. This is for safety reasons
|
||||
to prevent clever code from breaking validation. The general rule of thumb:
|
||||
if a tag looked empty on the way in, it will get removed; if HTML Purifier
|
||||
made it empty, it will stay.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
AutoFormat.RemoveSpansWithoutAttributes
|
||||
TYPE: bool
|
||||
VERSION: 4.0.1
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive causes <code>span</code> tags without any attributes
|
||||
to be removed. It will also remove spans that had all attributes
|
||||
removed during processing.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
CSS.AllowDuplicates
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 4.8.0
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
By default, HTML Purifier removes duplicate CSS properties,
|
||||
like <code>color:red; color:blue</code>. If this is set to
|
||||
true, duplicate properties are allowed.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,8 @@
|
||||
CSS.AllowImportant
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 3.1.0
|
||||
--DESCRIPTION--
|
||||
This parameter determines whether or not !important cascade modifiers should
|
||||
be allowed in user CSS. If false, !important will stripped.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
CSS.AllowTricky
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 3.1.0
|
||||
--DESCRIPTION--
|
||||
This parameter determines whether or not to allow "tricky" CSS properties and
|
||||
values. Tricky CSS properties/values can drastically modify page layout or
|
||||
be used for deceptive practices but do not directly constitute a security risk.
|
||||
For example, <code>display:none;</code> is considered a tricky property that
|
||||
will only be allowed if this directive is set to true.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
CSS.AllowedFonts
|
||||
TYPE: lookup/null
|
||||
VERSION: 4.3.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Allows you to manually specify a set of allowed fonts. If
|
||||
<code>NULL</code>, all fonts are allowed. This directive
|
||||
affects generic names (serif, sans-serif, monospace, cursive,
|
||||
fantasy) as well as specific font families.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,18 @@
|
||||
CSS.AllowedProperties
|
||||
TYPE: lookup/null
|
||||
VERSION: 3.1.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
If HTML Purifier's style attributes set is unsatisfactory for your needs,
|
||||
you can overload it with your own list of tags to allow. Note that this
|
||||
method is subtractive: it does its job by taking away from HTML Purifier
|
||||
usual feature set, so you cannot add an attribute that HTML Purifier never
|
||||
supported in the first place.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warning:</strong> If another directive conflicts with the
|
||||
elements here, <em>that</em> directive will win and override.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
CSS.DefinitionRev
|
||||
TYPE: int
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: 1
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Revision identifier for your custom definition. See
|
||||
%HTML.DefinitionRev for details.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,13 @@
|
||||
CSS.ForbiddenProperties
|
||||
TYPE: lookup
|
||||
VERSION: 4.2.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This is the logical inverse of %CSS.AllowedProperties, and it will
|
||||
override that directive or any other directive. If possible,
|
||||
%CSS.AllowedProperties is recommended over this directive,
|
||||
because it can sometimes be difficult to tell whether or not you've
|
||||
forbidden all of the CSS properties you truly would like to disallow.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
CSS.MaxImgLength
|
||||
TYPE: string/null
|
||||
DEFAULT: '1200px'
|
||||
VERSION: 3.1.1
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This parameter sets the maximum allowed length on <code>img</code> tags,
|
||||
effectively the <code>width</code> and <code>height</code> properties.
|
||||
Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is
|
||||
in place to prevent imagecrash attacks, disable with null at your own risk.
|
||||
This directive is similar to %HTML.MaxImgLength, and both should be
|
||||
concurrently edited, although there are
|
||||
subtle differences in the input format (the CSS max is a number with
|
||||
a unit).
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,10 @@
|
||||
CSS.Proprietary
|
||||
TYPE: bool
|
||||
VERSION: 3.0.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Whether or not to allow safe, proprietary CSS values.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
CSS.Trusted
|
||||
TYPE: bool
|
||||
VERSION: 4.2.1
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
Indicates whether or not the user's CSS input is trusted or not. If the
|
||||
input is trusted, a more expansive set of allowed properties. See
|
||||
also %HTML.Trusted.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
Cache.DefinitionImpl
|
||||
TYPE: string/null
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: 'Serializer'
|
||||
--DESCRIPTION--
|
||||
|
||||
This directive defines which method to use when caching definitions,
|
||||
the complex data-type that makes HTML Purifier tick. Set to null
|
||||
to disable caching (not recommended, as you will see a definite
|
||||
performance degradation).
|
||||
|
||||
--ALIASES--
|
||||
Core.DefinitionCache
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,13 @@
|
||||
Cache.SerializerPath
|
||||
TYPE: string/null
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Absolute path with no trailing slash to store serialized definitions in.
|
||||
Default is within the
|
||||
HTML Purifier library inside DefinitionCache/Serializer. This
|
||||
path must be writable by the webserver.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Cache.SerializerPermissions
|
||||
TYPE: int/null
|
||||
VERSION: 4.3.0
|
||||
DEFAULT: 0755
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Directory permissions of the files and directories created inside
|
||||
the DefinitionCache/Serializer or other custom serializer path.
|
||||
</p>
|
||||
<p>
|
||||
In HTML Purifier 4.8.0, this also supports <code>NULL</code>,
|
||||
which means that no chmod'ing or directory creation shall
|
||||
occur.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,18 @@
|
||||
Core.AggressivelyFixLt
|
||||
TYPE: bool
|
||||
VERSION: 2.1.0
|
||||
DEFAULT: true
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive enables aggressive pre-filter fixes HTML Purifier can
|
||||
perform in order to ensure that open angled-brackets do not get killed
|
||||
during parsing stage. Enabling this will result in two preg_replace_callback
|
||||
calls and at least two preg_replace calls for every HTML document parsed;
|
||||
if your users make very well-formed HTML, you can set this directive false.
|
||||
This has no effect when DirectLex is used.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Notice:</strong> This directive's default turned from false to true
|
||||
in HTML Purifier 3.2.0.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Core.AggressivelyRemoveScript
|
||||
TYPE: bool
|
||||
VERSION: 4.9.0
|
||||
DEFAULT: true
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive enables aggressive pre-filter removal of
|
||||
script tags. This is not necessary for security,
|
||||
but it can help work around a bug in libxml where embedded
|
||||
HTML elements inside script sections cause the parser to
|
||||
choke. To revert to pre-4.9.0 behavior, set this to false.
|
||||
This directive has no effect if %Core.Trusted is true,
|
||||
%Core.RemoveScriptContents is false, or %Core.HiddenElements
|
||||
does not contain script.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Core.AllowHostnameUnderscore
|
||||
TYPE: bool
|
||||
VERSION: 4.6.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
By RFC 1123, underscores are not permitted in host names.
|
||||
(This is in contrast to the specification for DNS, RFC
|
||||
2181, which allows underscores.)
|
||||
However, most browsers do the right thing when faced with
|
||||
an underscore in the host name, and so some poorly written
|
||||
websites are written with the expectation this should work.
|
||||
Setting this parameter to true relaxes our allowed character
|
||||
check so that underscores are permitted.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Core.AllowParseManyTags
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 4.10.1
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive allows parsing of many nested tags.
|
||||
If you set true, relaxes any hardcoded limit from the parser.
|
||||
However, in that case it may cause a Dos attack.
|
||||
Be careful when enabling it.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Core.CollectErrors
|
||||
TYPE: bool
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
|
||||
Whether or not to collect errors found while filtering the document. This
|
||||
is a useful way to give feedback to your users. <strong>Warning:</strong>
|
||||
Currently this feature is very patchy and experimental, with lots of
|
||||
possible error messages not yet implemented. It will not cause any
|
||||
problems, but it may not help your users either.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,160 @@
|
||||
Core.ColorKeywords
|
||||
TYPE: hash
|
||||
VERSION: 2.0.0
|
||||
--DEFAULT--
|
||||
array (
|
||||
'aliceblue' => '#F0F8FF',
|
||||
'antiquewhite' => '#FAEBD7',
|
||||
'aqua' => '#00FFFF',
|
||||
'aquamarine' => '#7FFFD4',
|
||||
'azure' => '#F0FFFF',
|
||||
'beige' => '#F5F5DC',
|
||||
'bisque' => '#FFE4C4',
|
||||
'black' => '#000000',
|
||||
'blanchedalmond' => '#FFEBCD',
|
||||
'blue' => '#0000FF',
|
||||
'blueviolet' => '#8A2BE2',
|
||||
'brown' => '#A52A2A',
|
||||
'burlywood' => '#DEB887',
|
||||
'cadetblue' => '#5F9EA0',
|
||||
'chartreuse' => '#7FFF00',
|
||||
'chocolate' => '#D2691E',
|
||||
'coral' => '#FF7F50',
|
||||
'cornflowerblue' => '#6495ED',
|
||||
'cornsilk' => '#FFF8DC',
|
||||
'crimson' => '#DC143C',
|
||||
'cyan' => '#00FFFF',
|
||||
'darkblue' => '#00008B',
|
||||
'darkcyan' => '#008B8B',
|
||||
'darkgoldenrod' => '#B8860B',
|
||||
'darkgray' => '#A9A9A9',
|
||||
'darkgrey' => '#A9A9A9',
|
||||
'darkgreen' => '#006400',
|
||||
'darkkhaki' => '#BDB76B',
|
||||
'darkmagenta' => '#8B008B',
|
||||
'darkolivegreen' => '#556B2F',
|
||||
'darkorange' => '#FF8C00',
|
||||
'darkorchid' => '#9932CC',
|
||||
'darkred' => '#8B0000',
|
||||
'darksalmon' => '#E9967A',
|
||||
'darkseagreen' => '#8FBC8F',
|
||||
'darkslateblue' => '#483D8B',
|
||||
'darkslategray' => '#2F4F4F',
|
||||
'darkslategrey' => '#2F4F4F',
|
||||
'darkturquoise' => '#00CED1',
|
||||
'darkviolet' => '#9400D3',
|
||||
'deeppink' => '#FF1493',
|
||||
'deepskyblue' => '#00BFFF',
|
||||
'dimgray' => '#696969',
|
||||
'dimgrey' => '#696969',
|
||||
'dodgerblue' => '#1E90FF',
|
||||
'firebrick' => '#B22222',
|
||||
'floralwhite' => '#FFFAF0',
|
||||
'forestgreen' => '#228B22',
|
||||
'fuchsia' => '#FF00FF',
|
||||
'gainsboro' => '#DCDCDC',
|
||||
'ghostwhite' => '#F8F8FF',
|
||||
'gold' => '#FFD700',
|
||||
'goldenrod' => '#DAA520',
|
||||
'gray' => '#808080',
|
||||
'grey' => '#808080',
|
||||
'green' => '#008000',
|
||||
'greenyellow' => '#ADFF2F',
|
||||
'honeydew' => '#F0FFF0',
|
||||
'hotpink' => '#FF69B4',
|
||||
'indianred' => '#CD5C5C',
|
||||
'indigo' => '#4B0082',
|
||||
'ivory' => '#FFFFF0',
|
||||
'khaki' => '#F0E68C',
|
||||
'lavender' => '#E6E6FA',
|
||||
'lavenderblush' => '#FFF0F5',
|
||||
'lawngreen' => '#7CFC00',
|
||||
'lemonchiffon' => '#FFFACD',
|
||||
'lightblue' => '#ADD8E6',
|
||||
'lightcoral' => '#F08080',
|
||||
'lightcyan' => '#E0FFFF',
|
||||
'lightgoldenrodyellow' => '#FAFAD2',
|
||||
'lightgray' => '#D3D3D3',
|
||||
'lightgrey' => '#D3D3D3',
|
||||
'lightgreen' => '#90EE90',
|
||||
'lightpink' => '#FFB6C1',
|
||||
'lightsalmon' => '#FFA07A',
|
||||
'lightseagreen' => '#20B2AA',
|
||||
'lightskyblue' => '#87CEFA',
|
||||
'lightslategray' => '#778899',
|
||||
'lightslategrey' => '#778899',
|
||||
'lightsteelblue' => '#B0C4DE',
|
||||
'lightyellow' => '#FFFFE0',
|
||||
'lime' => '#00FF00',
|
||||
'limegreen' => '#32CD32',
|
||||
'linen' => '#FAF0E6',
|
||||
'magenta' => '#FF00FF',
|
||||
'maroon' => '#800000',
|
||||
'mediumaquamarine' => '#66CDAA',
|
||||
'mediumblue' => '#0000CD',
|
||||
'mediumorchid' => '#BA55D3',
|
||||
'mediumpurple' => '#9370DB',
|
||||
'mediumseagreen' => '#3CB371',
|
||||
'mediumslateblue' => '#7B68EE',
|
||||
'mediumspringgreen' => '#00FA9A',
|
||||
'mediumturquoise' => '#48D1CC',
|
||||
'mediumvioletred' => '#C71585',
|
||||
'midnightblue' => '#191970',
|
||||
'mintcream' => '#F5FFFA',
|
||||
'mistyrose' => '#FFE4E1',
|
||||
'moccasin' => '#FFE4B5',
|
||||
'navajowhite' => '#FFDEAD',
|
||||
'navy' => '#000080',
|
||||
'oldlace' => '#FDF5E6',
|
||||
'olive' => '#808000',
|
||||
'olivedrab' => '#6B8E23',
|
||||
'orange' => '#FFA500',
|
||||
'orangered' => '#FF4500',
|
||||
'orchid' => '#DA70D6',
|
||||
'palegoldenrod' => '#EEE8AA',
|
||||
'palegreen' => '#98FB98',
|
||||
'paleturquoise' => '#AFEEEE',
|
||||
'palevioletred' => '#DB7093',
|
||||
'papayawhip' => '#FFEFD5',
|
||||
'peachpuff' => '#FFDAB9',
|
||||
'peru' => '#CD853F',
|
||||
'pink' => '#FFC0CB',
|
||||
'plum' => '#DDA0DD',
|
||||
'powderblue' => '#B0E0E6',
|
||||
'purple' => '#800080',
|
||||
'rebeccapurple' => '#663399',
|
||||
'red' => '#FF0000',
|
||||
'rosybrown' => '#BC8F8F',
|
||||
'royalblue' => '#4169E1',
|
||||
'saddlebrown' => '#8B4513',
|
||||
'salmon' => '#FA8072',
|
||||
'sandybrown' => '#F4A460',
|
||||
'seagreen' => '#2E8B57',
|
||||
'seashell' => '#FFF5EE',
|
||||
'sienna' => '#A0522D',
|
||||
'silver' => '#C0C0C0',
|
||||
'skyblue' => '#87CEEB',
|
||||
'slateblue' => '#6A5ACD',
|
||||
'slategray' => '#708090',
|
||||
'slategrey' => '#708090',
|
||||
'snow' => '#FFFAFA',
|
||||
'springgreen' => '#00FF7F',
|
||||
'steelblue' => '#4682B4',
|
||||
'tan' => '#D2B48C',
|
||||
'teal' => '#008080',
|
||||
'thistle' => '#D8BFD8',
|
||||
'tomato' => '#FF6347',
|
||||
'turquoise' => '#40E0D0',
|
||||
'violet' => '#EE82EE',
|
||||
'wheat' => '#F5DEB3',
|
||||
'white' => '#FFFFFF',
|
||||
'whitesmoke' => '#F5F5F5',
|
||||
'yellow' => '#FFFF00',
|
||||
'yellowgreen' => '#9ACD32'
|
||||
)
|
||||
--DESCRIPTION--
|
||||
|
||||
Lookup array of color names to six digit hexadecimal number corresponding
|
||||
to color, with preceding hash mark. Used when parsing colors. The lookup
|
||||
is done in a case-insensitive manner.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
Core.ConvertDocumentToFragment
|
||||
TYPE: bool
|
||||
DEFAULT: true
|
||||
--DESCRIPTION--
|
||||
|
||||
This parameter determines whether or not the filter should convert
|
||||
input that is a full document with html and body tags to a fragment
|
||||
of just the contents of a body tag. This parameter is simply something
|
||||
HTML Purifier can do during an edge-case: for most inputs, this
|
||||
processing is not necessary.
|
||||
|
||||
--ALIASES--
|
||||
Core.AcceptFullDocuments
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,17 @@
|
||||
Core.DirectLexLineNumberSyncInterval
|
||||
TYPE: int
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: 0
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Specifies the number of tokens the DirectLex line number tracking
|
||||
implementations should process before attempting to resyncronize the
|
||||
current line count by manually counting all previous new-lines. When
|
||||
at 0, this functionality is disabled. Lower values will decrease
|
||||
performance, and this is only strictly necessary if the counting
|
||||
algorithm is buggy (in which case you should report it as a bug).
|
||||
This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is
|
||||
not being used.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
Core.DisableExcludes
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 4.5.0
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive disables SGML-style exclusions, e.g. the exclusion of
|
||||
<code><object></code> in any descendant of a
|
||||
<code><pre></code> tag. Disabling excludes will allow some
|
||||
invalid documents to pass through HTML Purifier, but HTML Purifier
|
||||
will also be less likely to accidentally remove large documents during
|
||||
processing.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,9 @@
|
||||
Core.EnableIDNA
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
VERSION: 4.4.0
|
||||
--DESCRIPTION--
|
||||
Allows international domain names in URLs. This configuration option
|
||||
requires the PEAR Net_IDNA2 module to be installed. It operates by
|
||||
punycoding any internationalized host names for maximum portability.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,15 @@
|
||||
Core.Encoding
|
||||
TYPE: istring
|
||||
DEFAULT: 'utf-8'
|
||||
--DESCRIPTION--
|
||||
If for some reason you are unable to convert all webpages to UTF-8, you can
|
||||
use this directive as a stop-gap compatibility change to let HTML Purifier
|
||||
deal with non UTF-8 input. This technique has notable deficiencies:
|
||||
absolutely no characters outside of the selected character encoding will be
|
||||
preserved, not even the ones that have been ampersand escaped (this is due
|
||||
to a UTF-8 specific <em>feature</em> that automatically resolves all
|
||||
entities), making it pretty useless for anything except the most I18N-blind
|
||||
applications, although %Core.EscapeNonASCIICharacters offers fixes this
|
||||
trouble with another tradeoff. This directive only accepts ISO-8859-1 if
|
||||
iconv is not enabled.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Core.EscapeInvalidChildren
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p><strong>Warning:</strong> this configuration option is no longer does anything as of 4.6.0.</p>
|
||||
|
||||
<p>When true, a child is found that is not allowed in the context of the
|
||||
parent element will be transformed into text as if it were ASCII. When
|
||||
false, that element and all internal tags will be dropped, though text will
|
||||
be preserved. There is no option for dropping the element but preserving
|
||||
child nodes.</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,7 @@
|
||||
Core.EscapeInvalidTags
|
||||
TYPE: bool
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
When true, invalid tags will be written back to the document as plain text.
|
||||
Otherwise, they are silently dropped.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,13 @@
|
||||
Core.EscapeNonASCIICharacters
|
||||
TYPE: bool
|
||||
VERSION: 1.4.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
This directive overcomes a deficiency in %Core.Encoding by blindly
|
||||
converting all non-ASCII characters into decimal numeric entities before
|
||||
converting it to its native encoding. This means that even characters that
|
||||
can be expressed in the non-UTF-8 encoding will be entity-ized, which can
|
||||
be a real downer for encodings like Big5. It also assumes that the ASCII
|
||||
repetoire is available, although this is the case for almost all encodings.
|
||||
Anyway, use UTF-8!
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,19 @@
|
||||
Core.HiddenElements
|
||||
TYPE: lookup
|
||||
--DEFAULT--
|
||||
array (
|
||||
'script' => true,
|
||||
'style' => true,
|
||||
)
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This directive is a lookup array of elements which should have their
|
||||
contents removed when they are not allowed by the HTML definition.
|
||||
For example, the contents of a <code>script</code> tag are not
|
||||
normally shown in a document, so if script tags are to be removed,
|
||||
their contents should be removed to. This is opposed to a <code>b</code>
|
||||
tag, which defines some presentational changes but does not hide its
|
||||
contents.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,10 @@
|
||||
Core.Language
|
||||
TYPE: string
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: 'en'
|
||||
--DESCRIPTION--
|
||||
|
||||
ISO 639 language code for localizable things in HTML Purifier to use,
|
||||
which is mainly error reporting. There is currently only an English (en)
|
||||
translation, so this directive is currently useless.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,36 @@
|
||||
Core.LegacyEntityDecoder
|
||||
TYPE: bool
|
||||
VERSION: 4.9.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Prior to HTML Purifier 4.9.0, entities were decoded by performing
|
||||
a global search replace for all entities whose decoded versions
|
||||
did not have special meanings under HTML, and replaced them with
|
||||
their decoded versions. We would match all entities, even if they did
|
||||
not have a trailing semicolon, but only if there weren't any trailing
|
||||
alphanumeric characters.
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>Original</th><th>Text</th><th>Attribute</th></tr>
|
||||
<tr><td>&yen;</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yen</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yena</td><td>&yena</td><td>&yena</td></tr>
|
||||
<tr><td>&yen=</td><td>¥=</td><td>¥=</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
In HTML Purifier 4.9.0, we changed the behavior of entity parsing
|
||||
to match entities that had missing trailing semicolons in less
|
||||
cases, to more closely match HTML5 parsing behavior:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>Original</th><th>Text</th><th>Attribute</th></tr>
|
||||
<tr><td>&yen;</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yen</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yena</td><td>¥a</td><td>&yena</td></tr>
|
||||
<tr><td>&yen=</td><td>¥=</td><td>&yen=</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
This flag reverts back to pre-HTML Purifier 4.9.0 behavior.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,34 @@
|
||||
Core.LexerImpl
|
||||
TYPE: mixed/null
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This parameter determines what lexer implementation can be used. The
|
||||
valid values are:
|
||||
</p>
|
||||
<dl>
|
||||
<dt><em>null</em></dt>
|
||||
<dd>
|
||||
Recommended, the lexer implementation will be auto-detected based on
|
||||
your PHP-version and configuration.
|
||||
</dd>
|
||||
<dt><em>string</em> lexer identifier</dt>
|
||||
<dd>
|
||||
This is a slim way of manually overridding the implementation.
|
||||
Currently recognized values are: DOMLex (the default PHP5
|
||||
implementation)
|
||||
and DirectLex (the default PHP4 implementation). Only use this if
|
||||
you know what you are doing: usually, the auto-detection will
|
||||
manage things for cases you aren't even aware of.
|
||||
</dd>
|
||||
<dt><em>object</em> lexer instance</dt>
|
||||
<dd>
|
||||
Super-advanced: you can specify your own, custom, implementation that
|
||||
implements the interface defined by <code>HTMLPurifier_Lexer</code>.
|
||||
I may remove this option simply because I don't expect anyone
|
||||
to use it.
|
||||
</dd>
|
||||
</dl>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Core.MaintainLineNumbers
|
||||
TYPE: bool/null
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
If true, HTML Purifier will add line number information to all tokens.
|
||||
This is useful when error reporting is turned on, but can result in
|
||||
significant performance degradation and should not be used when
|
||||
unnecessary. This directive must be used with the DirectLex lexer,
|
||||
as the DOMLex lexer does not (yet) support this functionality.
|
||||
If the value is null, an appropriate value will be selected based
|
||||
on other configuration.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
Core.NormalizeNewlines
|
||||
TYPE: bool
|
||||
VERSION: 4.2.0
|
||||
DEFAULT: true
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Whether or not to normalize newlines to the operating
|
||||
system default. When <code>false</code>, HTML Purifier
|
||||
will attempt to preserve mixed newline files.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Core.RemoveInvalidImg
|
||||
TYPE: bool
|
||||
DEFAULT: true
|
||||
VERSION: 1.3.0
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This directive enables pre-emptive URI checking in <code>img</code>
|
||||
tags, as the attribute validation strategy is not authorized to
|
||||
remove elements from the document. Revert to pre-1.3.0 behavior by setting to false.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
Core.RemoveProcessingInstructions
|
||||
TYPE: bool
|
||||
VERSION: 4.2.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
Instead of escaping processing instructions in the form <code><? ...
|
||||
?></code>, remove it out-right. This may be useful if the HTML
|
||||
you are validating contains XML processing instruction gunk, however,
|
||||
it can also be user-unfriendly for people attempting to post PHP
|
||||
snippets.
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,12 @@
|
||||
Core.RemoveScriptContents
|
||||
TYPE: bool/null
|
||||
DEFAULT: NULL
|
||||
VERSION: 2.0.0
|
||||
DEPRECATED-VERSION: 2.1.0
|
||||
DEPRECATED-USE: Core.HiddenElements
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive enables HTML Purifier to remove not only script tags
|
||||
but all of their contents.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,11 @@
|
||||
Filter.Custom
|
||||
TYPE: list
|
||||
VERSION: 3.1.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive can be used to add custom filters; it is nearly the
|
||||
equivalent of the now deprecated <code>HTMLPurifier->addFilter()</code>
|
||||
method. Specify an array of concrete implementations.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,14 @@
|
||||
Filter.ExtractStyleBlocks.Escaping
|
||||
TYPE: bool
|
||||
VERSION: 3.0.0
|
||||
DEFAULT: true
|
||||
ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
Whether or not to escape the dangerous characters <, > and &
|
||||
as \3C, \3E and \26, respectively. This is can be safely set to false
|
||||
if the contents of StyleBlocks will be placed in an external stylesheet,
|
||||
where there is no risk of it being interpreted as HTML.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,29 @@
|
||||
Filter.ExtractStyleBlocks.Scope
|
||||
TYPE: string/null
|
||||
VERSION: 3.0.0
|
||||
DEFAULT: NULL
|
||||
ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
If you would like users to be able to define external stylesheets, but
|
||||
only allow them to specify CSS declarations for a specific node and
|
||||
prevent them from fiddling with other elements, use this directive.
|
||||
It accepts any valid CSS selector, and will prepend this to any
|
||||
CSS declaration extracted from the document. For example, if this
|
||||
directive is set to <code>#user-content</code> and a user uses the
|
||||
selector <code>a:hover</code>, the final selector will be
|
||||
<code>#user-content a:hover</code>.
|
||||
</p>
|
||||
<p>
|
||||
The comma shorthand may be used; consider the above example, with
|
||||
<code>#user-content, #user-content2</code>, the final selector will
|
||||
be <code>#user-content a:hover, #user-content2 a:hover</code>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warning:</strong> It is possible for users to bypass this measure
|
||||
using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML
|
||||
Purifier, and I am working to get it fixed. Until then, HTML Purifier
|
||||
performs a basic check to prevent this.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Filter.ExtractStyleBlocks.TidyImpl
|
||||
TYPE: mixed/null
|
||||
VERSION: 3.1.0
|
||||
DEFAULT: NULL
|
||||
ALIASES: FilterParam.ExtractStyleBlocksTidyImpl
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
If left NULL, HTML Purifier will attempt to instantiate a <code>csstidy</code>
|
||||
class to use for internal cleaning. This will usually be good enough.
|
||||
</p>
|
||||
<p>
|
||||
However, for trusted user input, you can set this to <code>false</code> to
|
||||
disable cleaning. In addition, you can supply your own concrete implementation
|
||||
of Tidy's interface to use, although I don't know why you'd want to do that.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,74 @@
|
||||
Filter.ExtractStyleBlocks
|
||||
TYPE: bool
|
||||
VERSION: 3.1.0
|
||||
DEFAULT: false
|
||||
EXTERNAL: CSSTidy
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive turns on the style block extraction filter, which removes
|
||||
<code>style</code> blocks from input HTML, cleans them up with CSSTidy,
|
||||
and places them in the <code>StyleBlocks</code> context variable, for further
|
||||
use by you, usually to be placed in an external stylesheet, or a
|
||||
<code>style</code> block in the <code>head</code> of your document.
|
||||
</p>
|
||||
<p>
|
||||
Sample usage:
|
||||
</p>
|
||||
<pre><![CDATA[
|
||||
<?php
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
||||
<head>
|
||||
<title>Filter.ExtractStyleBlocks</title>
|
||||
<?php
|
||||
require_once '/path/to/library/HTMLPurifier.auto.php';
|
||||
require_once '/path/to/csstidy.class.php';
|
||||
|
||||
$dirty = '<style>body {color:#F00;}</style> Some text';
|
||||
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Filter', 'ExtractStyleBlocks', true);
|
||||
$purifier = new HTMLPurifier($config);
|
||||
|
||||
$html = $purifier->purify($dirty);
|
||||
|
||||
// This implementation writes the stylesheets to the styles/ directory.
|
||||
// You can also echo the styles inside the document, but it's a bit
|
||||
// more difficult to make sure they get interpreted properly by
|
||||
// browsers; try the usual CSS armoring techniques.
|
||||
$styles = $purifier->context->get('StyleBlocks');
|
||||
$dir = 'styles/';
|
||||
if (!is_dir($dir)) mkdir($dir);
|
||||
$hash = sha1($_GET['html']);
|
||||
foreach ($styles as $i => $style) {
|
||||
file_put_contents($name = $dir . $hash . "_$i");
|
||||
echo '<link rel="stylesheet" type="text/css" href="'.$name.'" />';
|
||||
}
|
||||
?>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<?php echo $html; ?>
|
||||
</div>
|
||||
</b]]><![CDATA[ody>
|
||||
</html>
|
||||
]]></pre>
|
||||
<p>
|
||||
<strong>Warning:</strong> It is possible for a user to mount an
|
||||
imagecrash attack using this CSS. Counter-measures are difficult;
|
||||
it is not simply enough to limit the range of CSS lengths (using
|
||||
relative lengths with many nesting levels allows for large values
|
||||
to be attained without actually specifying them in the stylesheet),
|
||||
and the flexible nature of selectors makes it difficult to selectively
|
||||
disable lengths on image tags (HTML Purifier, however, does disable
|
||||
CSS width and height in inline styling). There are probably two effective
|
||||
counter measures: an explicit width and height set to auto in all
|
||||
images in your document (unlikely) or the disabling of width and
|
||||
height (somewhat reasonable). Whether or not these measures should be
|
||||
used is left to the reader.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,16 @@
|
||||
Filter.YouTube
|
||||
TYPE: bool
|
||||
VERSION: 3.1.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
<strong>Warning:</strong> Deprecated in favor of %HTML.SafeObject and
|
||||
%Output.FlashCompat (turn both on to allow YouTube videos and other
|
||||
Flash content).
|
||||
</p>
|
||||
<p>
|
||||
This directive enables YouTube video embedding in HTML Purifier. Check
|
||||
<a href="http://htmlpurifier.org/docs/enduser-youtube.html">this document
|
||||
on embedding videos</a> for more information on what this filter does.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,25 @@
|
||||
HTML.Allowed
|
||||
TYPE: itext/null
|
||||
VERSION: 2.0.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
This is a preferred convenience directive that combines
|
||||
%HTML.AllowedElements and %HTML.AllowedAttributes.
|
||||
Specify elements and attributes that are allowed using:
|
||||
<code>element1[attr1|attr2],element2...</code>. For example,
|
||||
if you would like to only allow paragraphs and links, specify
|
||||
<code>a[href],p</code>. You can specify attributes that apply
|
||||
to all elements using an asterisk, e.g. <code>*[lang]</code>.
|
||||
You can also use newlines instead of commas to separate elements.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warning</strong>:
|
||||
All of the constraints on the component directives are still enforced.
|
||||
The syntax is a <em>subset</em> of TinyMCE's <code>valid_elements</code>
|
||||
whitelist: directly copy-pasting it here will probably result in
|
||||
broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes
|
||||
are set, this directive has no effect.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,19 @@
|
||||
HTML.AllowedAttributes
|
||||
TYPE: lookup/null
|
||||
VERSION: 1.3.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
|
||||
<p>
|
||||
If HTML Purifier's attribute set is unsatisfactory, overload it!
|
||||
The syntax is "tag.attr" or "*.attr" for the global attributes
|
||||
(style, id, class, dir, lang, xml:lang).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warning:</strong> If another directive conflicts with the
|
||||
elements here, <em>that</em> directive will win and override. For
|
||||
example, %HTML.EnableAttrID will take precedence over *.id in this
|
||||
directive. You must set that directive to true before you can use
|
||||
IDs at all.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,10 @@
|
||||
HTML.AllowedComments
|
||||
TYPE: lookup
|
||||
VERSION: 4.4.0
|
||||
DEFAULT: array()
|
||||
--DESCRIPTION--
|
||||
A whitelist which indicates what explicit comment bodies should be
|
||||
allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp
|
||||
(these directives are union'ed together, so a comment is considered
|
||||
valid if any directive deems it valid.)
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,15 @@
|
||||
HTML.AllowedCommentsRegexp
|
||||
TYPE: string/null
|
||||
VERSION: 4.4.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
A regexp, which if it matches the body of a comment, indicates that
|
||||
it should be allowed. Trailing and leading spaces are removed prior
|
||||
to running this regular expression.
|
||||
<strong>Warning:</strong> Make sure you specify
|
||||
correct anchor metacharacters <code>^regex$</code>, otherwise you may accept
|
||||
comments that you did not mean to! In particular, the regex <code>/foo|bar/</code>
|
||||
is probably not sufficiently strict, since it also allows <code>foobar</code>.
|
||||
See also %HTML.AllowedComments (these directives are union'ed together,
|
||||
so a comment is considered valid if any directive deems it valid.)
|
||||
--# vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,23 @@
|
||||
HTML.AllowedElements
|
||||
TYPE: lookup/null
|
||||
VERSION: 1.3.0
|
||||
DEFAULT: NULL
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
If HTML Purifier's tag set is unsatisfactory for your needs, you can
|
||||
overload it with your own list of tags to allow. If you change
|
||||
this, you probably also want to change %HTML.AllowedAttributes; see
|
||||
also %HTML.Allowed which lets you set allowed elements and
|
||||
attributes at the same time.
|
||||
</p>
|
||||
<p>
|
||||
If you attempt to allow an element that HTML Purifier does not know
|
||||
about, HTML Purifier will raise an error. You will need to manually
|
||||
tell HTML Purifier about this element by using the
|
||||
<a href="http://htmlpurifier.org/docs/enduser-customize.html">advanced customization features.</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warning:</strong> If another directive conflicts with the
|
||||
elements here, <em>that</em> directive will win and override.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user