diff --git a/app/admin/getUsers.php b/app/admin/getUsers.php index 0633e72..35e4f40 100644 --- a/app/admin/getUsers.php +++ b/app/admin/getUsers.php @@ -1,5 +1,5 @@ $w) $h=$thbw; - if($thbw>$h) $w=$twbh; + if($twbh > $w) $h = $thbw; + if($thbw > $h) $w = $twbh; } elseif($w) { - $h=$thbw; + $h = $thbw; } elseif($h) { - $w=$twbh; + $w = $twbh; } else { - return FALSE; + return false; } // dir not writeable? - if(!is_writable($path)) return FALSE; + if(!is_writable($path)) return false; // GD lib not loaded? - if(!function_exists('gd_info')) return FALSE; - $gd=gd_info(); + if(!function_exists('gd_info')) return false; + $gd = gd_info(); // GD lib older than 2.0? preg_match('/\d/', $gd['GD Version'], $gdm); - if($gdm[0]<2) return FALSE; + if($gdm[0] < 2) return false; // get file extension preg_match('/\.[a-zA-Z]{3,4}$/U', $img, $matches); - $ext=strtolower($matches[0]); + $ext = strtolower($matches[0]); // check if supplied image is supported and specify actions based on file type - if($ext=='.gif') { - if(!$gd['GIF Create Support']) return FALSE; - $thumbFunc='imagegif'; - } elseif($ext=='.png') { - if(!$gd['PNG Support']) return FALSE; - $thumbFunc='imagepng'; - } elseif($ext=='.jpg' || $ext=='.jpe' || $ext=='.jpeg') { - if(!$gd['JPG Support'] && !$gd['JPEG Support']) return FALSE; - $thumbFunc='imagejpeg'; + if($ext == '.gif') { + if(!$gd['GIF Create Support']) return false; + $thumbFunc = 'imagegif'; + } elseif($ext == '.png') { + if(!$gd['PNG Support']) return false; + $thumbFunc = 'imagepng'; + } elseif($ext == '.jpg' || $ext == '.jpe' || $ext == '.jpeg') { + if(!$gd['JPG Support'] && !$gd['JPEG Support']) return false; + $thumbFunc = 'imagejpeg'; } else { - return FALSE; + return false; } // determine thumbnail file name - $ext=$matches[0]; - $thumb=substr($img, 0, -5).str_replace($ext, $id.$ext, substr($img, -5)); + $ext = $matches[0]; + $thumb = substr($img, 0, -5) . str_replace($ext, $id . $ext, substr($img, -5)); // if the original image smaller than thumb, then just copy it to thumb - if($h>$oh && $w>$ow) { - return (@copy($img, $thumb) ? TRUE : FALSE); + if($h > $oh && $w > $ow) { + return (@copy($img, $thumb) ? true : false); } // get image data - if(!$imgData=imagecreatefromstring(implode('', file($img)))) return FALSE; + if(!$imgData = imagecreatefromstring(implode('', file($img)))) return false; // finally, create thumbnail - $thumbData=imagecreatetruecolor($w, $h); + $thumbData = imagecreatetruecolor($w, $h); //preserve transparency of png and gif images - if($thumbFunc=='imagepng') { - if(($clr=@imagecolorallocate($thumbData, 0, 0, 0))!=-1) { + if($thumbFunc == 'imagepng') { + if(($clr = @imagecolorallocate($thumbData, 0, 0, 0)) != -1) { @imagecolortransparent($thumbData, $clr); @imagealphablending($thumbData, false); @imagesavealpha($thumbData, true); } - } elseif($thumbFunc=='imagegif') { + } elseif($thumbFunc == 'imagegif') { @imagealphablending($thumbData, false); - $transIndex=imagecolortransparent($imgData); - if($transIndex>=0) { - $transClr=imagecolorsforindex($imgData, $transIndex); - $transIndex=imagecolorallocatealpha($thumbData, $transClr['red'], $transClr['green'], $transClr['blue'], 127); + $transIndex = imagecolortransparent($imgData); + if($transIndex >= 0) { + $transClr = imagecolorsforindex($imgData, $transIndex); + $transIndex = imagecolorallocatealpha($thumbData, $transClr['red'], $transClr['green'], $transClr['blue'], 127); imagefill($thumbData, 0, 0, $transIndex); } } // resize original image into thumbnail - if(!imagecopyresampled($thumbData, $imgData, 0, 0 , 0, 0, $w, $h, $ow, $oh)) return FALSE; + if(!imagecopyresampled($thumbData, $imgData, 0, 0 , 0, 0, $w, $h, $ow, $oh)) return false; unset($imgData); // gif transparency - if($thumbFunc=='imagegif' && $transIndex>=0) { + if($thumbFunc == 'imagegif' && $transIndex >= 0) { imagecolortransparent($thumbData, $transIndex); - for($y=0; $y<$h; ++$y) - for($x=0; $x<$w; ++$x) - if(((imagecolorat($thumbData, $x, $y)>>24) & 0x7F) >= 100) imagesetpixel($thumbData, $x, $y, $transIndex); + for($y = 0; $y < $h; ++$y) + for($x = 0; $x < $w; ++$x) + if(((imagecolorat($thumbData, $x, $y) >> 24) & 0x7F) >= 100) imagesetpixel($thumbData, $x, $y, $transIndex); imagetruecolortopalette($thumbData, true, 255); imagesavealpha($thumbData, false); } - if(!$thumbFunc($thumbData, $thumb)) return FALSE; + if(!$thumbFunc($thumbData, $thumb)) return false; unset($thumbData); - return TRUE; + return true; } ######################################################################## function makeSafe($string, $is_gpc = true) { @@ -377,7 +377,7 @@ function sql($statment, &$o) { if(!$connected) { /****** Connect to MySQL ******/ if(!extension_loaded('mysql') && !extension_loaded('mysqli')) { - $o['error'] = 'PHP is not configured to connect to MySQL on this machine. Please see this page for help on how to configure MySQL.'; + $o['error'] = 'PHP is not configured to connect to MySQL on this machine. Please see this page for help on how to configure MySQL.'; if($o['silentErrors']) return false; @include_once($header); @@ -803,11 +803,7 @@ function bootstrapSQLSelect($name, $sql, $selectedValue, $class = '', $selectedC } ######################################################################## function isEmail($email) { - if(preg_match('/^([*+!.&#$¦\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,45})$/i', $email)) { - return $email; - } - - return false; + return filter_var(trim($email), FILTER_VALIDATE_EMAIL); } ######################################################################## function notifyMemberApproval($memberID) { @@ -1294,7 +1290,7 @@ function getUploadedFile($FieldName, $MaxSize=0, $FileTypes='csv|txt', $NoRename ######################################################################## function toBytes($val) { $val = trim($val); - $last = strtolower($val{strlen($val)-1}); + $last = strtolower($val[strlen($val)-1]); switch($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': @@ -1311,7 +1307,7 @@ function toBytes($val) { function convertLegacyOptions($CSVList) { $CSVList=str_replace(';;;', ';||', $CSVList); $CSVList=str_replace(';;', '||', $CSVList); - return $CSVList; + return trim($CSVList, '|'); } ######################################################################## function getValueGivenCaption($query, $caption) { @@ -1576,7 +1572,7 @@ public static function placeholder() { /* dismiss after x seconds if requested */ if(options.dismiss_seconds > 0) { - setTimeout(function() { /* */ this_notif.addClass('invisible'); }, options.dismiss_seconds * 1000); + setTimeout(function() { this_notif.addClass('invisible'); }, options.dismiss_seconds * 1000); } /* dismiss for x days if requested and user dismisses it */ @@ -1692,8 +1688,7 @@ function safe_html($str) { /* if $str has no HTML tags, apply nl2br */ if($str == strip_tags($str)) return nl2br($str); - $hc = new CI_Input(); - $hc->charset = datalist_db_encoding; + $hc = new CI_Input(datalist_db_encoding); return $hc->xss_clean($str); } @@ -2309,7 +2304,6 @@ function guessMySQLDateTime($dt) { return trim("$date $time"); } - ######################################################### function lookupQuery($tn, $lookupField) { /* @@ -2423,3 +2417,22 @@ function userCanImport() { return false; } ######################################################### + function parseTemplate($template) { + if(trim($template) == '') return $template; + + global $Translation; + foreach($Translation as $symbol => $trans) + $template = str_replace("<%%TRANSLATION($symbol)%%>", $trans, $template); + + // Correct and to prevent invalid HTML + $template = str_replace(['', ''], ['{MaxSize}', '{FileTypes}'], $template); + $template = str_replace('<%%BASE_UPLOAD_PATH%%>', getUploadDir(''), $template); + + return $template; + } + ######################################################### + function getUploadDir($dir) { + if($dir == '') $dir = config('adminConfig')['baseUploadPath']; + + return rtrim($dir, '\\/') . '/'; + } diff --git a/app/admin/incHeader.php b/app/admin/incHeader.php index 129d0cc..97d9fe7 100644 --- a/app/admin/incHeader.php +++ b/app/admin/incHeader.php @@ -17,7 +17,7 @@ - + + + + +
+
+
+ + + + + +
+ +
+
+ $Translation['no email notifications'], + 1 => $Translation['member waiting approval'], + 2 => $Translation['new sign-ups'] + ) + ); + ?> + + $Translation['no sign-up allowed'], + 1 => $Translation['admin approve members'], + 2 => $Translation['automatically approve members'] + ) + ); + ?> + +
+ + + + + + +
+ + + +
+ +
+
+ + + 'PHP mail()', + 'smtp' => 'SMTP' + ) + ); + ?> + + $Translation['none'], + 'ssl' => 'SSL', + 'tls' => 'TLS' + ) + ); + ?> + + + +
+ +
+
+ + + +
+ + +
+ +
+
+ + + + {$Translation['google API key instructions']}"); ?> + + ' . + $Translation['base upload path change warning'] . + '
' + ); ?> +
+ -
+
- +
diff --git a/app/admin/pageUploadCSV.php b/app/admin/pageUploadCSV.php index ac41f83..815f754 100644 --- a/app/admin/pageUploadCSV.php +++ b/app/admin/pageUploadCSV.php @@ -1020,13 +1020,13 @@ protected function get_csv_settings() { if(!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $fn) && $fn != 'ignore-field') return false; /* make sure field is not already mapped to another column */ - if(isset($mappgins[$fn])) return false; + if(isset($mappings[$fn])) return false; $settings['mappings'][$i] = $fn; if($fn == 'ignore-field') { unset($settings['mappings'][$i]); } else { - $mappgins[$fn] = true; + $mappings[$fn] = true; } } @@ -1389,7 +1389,7 @@ public function show_import_progress() { if(progress.retries < 10) { progress.retries++; update_progress((progress.failed + progress.imported) / progress.total * 100, '', '10', $this->lang['connection failed retrying'])); ?>', 'warning'); - setTimeout(function() { /* */ import_batch(progress, callbacks); }, 3000); + setTimeout(function() { import_batch(progress, callbacks); }, 3000); return; } else { /* fail and abort importing process */ diff --git a/app/admin/pageViewGroups.php b/app/admin/pageViewGroups.php index c66ee24..2ace70c 100644 --- a/app/admin/pageViewGroups.php +++ b/app/admin/pageViewGroups.php @@ -82,7 +82,7 @@ + onClick="return confirm('');"> diff --git a/app/admin/pageViewMembers.php b/app/admin/pageViewMembers.php index 39d7a5b..cc829ce 100644 --- a/app/admin/pageViewMembers.php +++ b/app/admin/pageViewMembers.php @@ -180,7 +180,7 @@ - + " title="">sql != '') { - $where .= ($where ? ' and ' : '') . "r.memberID like '{$memberID->sql}%'"; - } + if($memberID->sql == '{none}') + $where[] = "NOT LENGTH(r.memberID)"; + elseif($memberID->sql != '') + $where[] = "r.memberID LIKE '{$memberID->sql}%'"; - if($groupID) { - $where .= ($where ? ' and ' : '') . "g.groupID='{$groupID}'"; - } + if($groupID) $where[] = "g.groupID='{$groupID}'"; - if($tableName->sql != '') { - $where .= ($where ? ' and ' : '') . "r.tableName='{$tableName->sql}'"; - } + if($tableName->sql != '') $where[] = "r.tableName='{$tableName->sql}'"; - if($where) { - $where = "where {$where}"; - } + $whereStr = count($where) ? 'WHERE ' . implode(' AND ', $where) : ''; + + $numRecords = sqlValue("SELECT COUNT(1) FROM membership_userrecords r LEFT JOIN membership_groups g ON r.groupID=g.groupID {$whereStr}"); - $numRecords = sqlValue("select count(1) from membership_userrecords r left join membership_groups g on r.groupID=g.groupID {$where}"); $noResults = false; if(!$numRecords) { echo "
{$Translation['no matching results found']}
"; @@ -113,13 +108,19 @@ - + @@ -171,7 +172,7 @@ diff --git a/app/ajax_combo.php b/app/ajax_combo.php index 3d987a9..4ea4e5e 100644 --- a/app/ajax_combo.php +++ b/app/ajax_combo.php @@ -1,5 +1,5 @@ charset = datalist_db_encoding; + $xss = new CI_Input(datalist_db_encoding); // receive and verify user input $table_name = $_REQUEST['t']; @@ -332,7 +331,7 @@ } else { ?> MatchText; ?> - + true]; - $uploads_dir = realpath(dirname(__FILE__) . '/../' . $Translation['ImageFolder']); $safe_sid = makeSafe($source_id); // launch requests, asynchronously @@ -362,7 +361,7 @@ function applicants_and_tenants_form($selected_id = '', $AllowUpdate = 1, $Allow $combo_driver_license_state->SelectedData = $row['driver_license_state']; $combo_status->SelectedData = $row['status']; $urow = $row; /* unsanitized data */ - $hc = new CI_Input(); + $hc = new CI_Input(datalist_db_encoding); $row = $hc->xss_clean($row); /* sanitize data */ } else { $combo_driver_license_state->SelectedText = ( $_REQUEST['FilterField'][1] == '8' && $_REQUEST['FilterOperator'][1] == '<=>' ? $_REQUEST['FilterValue'][1] : ''); @@ -560,9 +559,7 @@ function applicants_and_tenants_form($selected_id = '', $AllowUpdate = 1, $Allow } // process translations - foreach($Translation as $symbol=>$trans) { - $templateCode = str_replace("<%%TRANSLATION($symbol)%%>", $trans, $templateCode); - } + $templateCode = parseTemplate($templateCode); // clear scrap $templateCode = str_replace('<%%', '' => '-->', - ' '<![CDATA[' - ); - /* never allowed, regex replacement */ - var $never_allowed_regex = array( - "javascript\s*:" => '[removed]', - "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE - "vbscript\s*:" => '[removed]', // IE, surprise! - "Redirect\s+302" => '[removed]' - ); - + /** + * List of never allowed strings + * + * @var array + */ + protected $_never_allowed_str = array( + 'document.cookie' => '[removed]', + '(document).cookie' => '[removed]', + 'document.write' => '[removed]', + '(document).write' => '[removed]', + '.parentNode' => '[removed]', + '.innerHTML' => '[removed]', + '-moz-binding' => '[removed]', + '' => '-->', + ' '<![CDATA[', + '' => '<comment>', + '<%' => '<%' + ); - function __construct(){ - $this->CI_Input(); - } + /** + * List of never allowed regex replacements + * + * @var array + */ + protected $_never_allowed_regex = array( + 'javascript\s*:', + '(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)', + 'expression\s*(\(|&\#40;)', // CSS and IE + 'vbscript\s*:', // IE, surprise! + 'wscript\s*:', // IE + 'jscript\s*:', // IE + 'vbs\s*:', // IE + 'Redirect\s+30\d', + "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?" + ); /** * Constructor * * Sets whether to globally enable the XSS processing * and whether to allow the $_GET array - * - * @access public */ - function CI_Input() - { - $this->use_xss_clean = FALSE; - $this->allow_get_array = TRUE; - // $this->_sanitize_globals(); - } - // -------------------------------------------------------------------- - - /** - * Sanitize Globals - * - * This function does the following: - * - * Unsets $_GET data (if query strings are not enabled) - * - * Unsets all globals if register_globals is enabled - * - * Standardizes newline characters to \n - * - * @access private - * @return void - */ - function _sanitize_globals() - { - // Would kind of be "wrong" to unset any of these GLOBALS - $protected = array('_SERVER', '_GET', '_POST', '_FILES', '_REQUEST', '_SESSION', '_ENV', 'GLOBALS', 'HTTP_RAW_POST_DATA', - 'system_folder', 'application_folder', 'BM', 'EXT', 'CFG', 'URI', 'RTR', 'OUT', 'IN'); - - // Unset globals for security. - // This is effectively the same as register_globals = off - foreach (array($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, $_ENV, (isset($_SESSION) && is_array($_SESSION)) ? $_SESSION : array()) as $global) - { - if ( ! is_array($global)) - { - if ( ! in_array($global, $protected)) - { - unset($GLOBALS[$global]); - } - } - else - { - foreach ($global as $key => $val) - { - if ( ! in_array($key, $protected)) - { - unset($GLOBALS[$key]); - } - - if (is_array($val)) - { - foreach($val as $k => $v) - { - if ( ! in_array($k, $protected)) - { - unset($GLOBALS[$k]); - } - } - } - } - } - } - - // Is $_GET data allowed? If not we'll set the $_GET to an empty array - if ($this->allow_get_array == FALSE) - { - $_GET = array(); - } - else - { - $_GET = $this->_clean_input_data($_GET); - } - - // Clean $_POST Data - $_POST = $this->_clean_input_data($_POST); - - // Clean $_COOKIE Data - // Also get rid of specially treated cookies that might be set by a server - // or silly application, that are of no use to a CI application anyway - // but that when present will trip our 'Disallowed Key Characters' alarm - // http://www.ietf.org/rfc/rfc2109.txt - // note that the key names below are single quoted strings, and are not PHP variables - unset($_COOKIE['$Version']); - unset($_COOKIE['$Path']); - unset($_COOKIE['$Domain']); - $_COOKIE = $this->_clean_input_data($_COOKIE); - - } - - // -------------------------------------------------------------------- - - /** - * Clean Input Data - * - * This is a helper function. It escapes data and - * standardizes newline characters to \n - * - * @access private - * @param string - * @return string - */ - function _clean_input_data($str) - { - if (is_array($str)) - { - $new_array = array(); - foreach ($str as $key => $val) - { - $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($val); - } - return $new_array; - } - - // We strip slashes if magic quotes is on to keep things consistent - if (get_magic_quotes_gpc()) - { - $str = stripslashes($str); - } - - // Should we filter the input data? - if ($this->use_xss_clean === TRUE) - { - $str = $this->xss_clean($str); - } - - // Standardize newlines - if (strpos($str, "\r") !== FALSE) - { - $str = str_replace(array("\r\n", "\r"), "\n", $str); - } - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Clean Keys - * - * This is a helper function. To prevent malicious users - * from trying to exploit keys we make sure that keys are - * only named with alpha-numeric text and a few other items. - * - * @access private - * @param string - * @return string - */ - function _clean_input_keys($str) - { - if ( ! preg_match("/^[a-z0-9:_\/-]+$/i", $str)) - { - exit('Disallowed Key Characters.'); - } - - return $str; + public function __construct($charset = 'iso-8859-1') { + $this->charset = $charset; + $this->use_xss_clean = false; + $this->allow_get_array = true; } // -------------------------------------------------------------------- @@ -231,23 +91,15 @@ function _clean_input_keys($str) * * This is a helper function to retrieve values from global arrays * - * @access private * @param array * @param string * @param bool * @return string */ - function _fetch_from_array(&$array, $index = '', $xss_clean = FALSE) - { - if ( ! isset($array[$index])) - { - return FALSE; - } + protected function _fetch_from_array(&$array, $index = '', $xss_clean = false) { + if(!isset($array[$index])) return false; - if ($xss_clean === TRUE) - { - return $this->xss_clean($array[$index]); - } + if($xss_clean === true) return $this->xss_clean($array[$index]); return $array[$index]; } @@ -257,13 +109,11 @@ function _fetch_from_array(&$array, $index = '', $xss_clean = FALSE) /** * Fetch an item from the GET array * - * @access public * @param string * @param bool * @return string */ - function get($index = '', $xss_clean = FALSE) - { + public function get($index = '', $xss_clean = false) { return $this->_fetch_from_array($_GET, $index, $xss_clean); } @@ -272,13 +122,11 @@ function get($index = '', $xss_clean = FALSE) /** * Fetch an item from the POST array * - * @access public * @param string * @param bool * @return string */ - function post($index = '', $xss_clean = FALSE) - { + public function post($index = '', $xss_clean = false) { return $this->_fetch_from_array($_POST, $index, $xss_clean); } @@ -287,21 +135,15 @@ function post($index = '', $xss_clean = FALSE) /** * Fetch an item from either the GET array or the POST * - * @access public * @param string The index key * @param bool XSS cleaning * @return string */ - function get_post($index = '', $xss_clean = FALSE) - { - if ( ! isset($_POST[$index]) ) - { + public function get_post($index = '', $xss_clean = false) { + if(!isset($_POST[$index])) return $this->get($index, $xss_clean); - } else - { return $this->post($index, $xss_clean); - } } // -------------------------------------------------------------------- @@ -309,13 +151,11 @@ function get_post($index = '', $xss_clean = FALSE) /** * Fetch an item from the COOKIE array * - * @access public * @param string * @param bool * @return string */ - function cookie($index = '', $xss_clean = FALSE) - { + public function cookie($index = '', $xss_clean = false) { return $this->_fetch_from_array($_COOKIE, $index, $xss_clean); } @@ -324,13 +164,11 @@ function cookie($index = '', $xss_clean = FALSE) /** * Fetch an item from the SERVER array * - * @access public * @param string * @param bool * @return string */ - function server($index = '', $xss_clean = FALSE) - { + public function server($index = '', $xss_clean = false) { return $this->_fetch_from_array($_SERVER, $index, $xss_clean); } @@ -339,54 +177,37 @@ function server($index = '', $xss_clean = FALSE) /** * Fetch the IP Address * - * @access public * @return string */ - function ip_address() - { - if ($this->ip_address !== FALSE) - { - return $this->ip_address; - } + public function ip_address() { + if($this->ip_address !== false) return $this->ip_address; - if (config_item('proxy_ips') != '' && $this->server('HTTP_X_FORWARDED_FOR') && $this->server('REMOTE_ADDR')) - { + if(config_item('proxy_ips') != '' && $this->server('HTTP_X_FORWARDED_FOR') && $this->server('REMOTE_ADDR')) { $proxies = preg_split('/[\s,]/', config_item('proxy_ips'), -1, PREG_SPLIT_NO_EMPTY); $proxies = is_array($proxies) ? $proxies : array($proxies); $this->ip_address = in_array($_SERVER['REMOTE_ADDR'], $proxies) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR']; - } - elseif ($this->server('REMOTE_ADDR') AND $this->server('HTTP_CLIENT_IP')) - { + } elseif($this->server('REMOTE_ADDR') && $this->server('HTTP_CLIENT_IP')) { $this->ip_address = $_SERVER['HTTP_CLIENT_IP']; - } - elseif ($this->server('REMOTE_ADDR')) - { + } elseif ($this->server('REMOTE_ADDR')) { $this->ip_address = $_SERVER['REMOTE_ADDR']; - } - elseif ($this->server('HTTP_CLIENT_IP')) - { + } elseif ($this->server('HTTP_CLIENT_IP')) { $this->ip_address = $_SERVER['HTTP_CLIENT_IP']; - } - elseif ($this->server('HTTP_X_FORWARDED_FOR')) - { + } elseif ($this->server('HTTP_X_FORWARDED_FOR')) { $this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR']; } - if ($this->ip_address === FALSE) - { + if($this->ip_address === false) { $this->ip_address = '0.0.0.0'; return $this->ip_address; } - if (strstr($this->ip_address, ',')) - { + if(strstr($this->ip_address, ',')) { $x = explode(',', $this->ip_address); $this->ip_address = trim(end($x)); } - if ( ! $this->valid_ip($this->ip_address)) - { + if(!$this->valid_ip($this->ip_address)) { $this->ip_address = '0.0.0.0'; } @@ -400,36 +221,30 @@ function ip_address() * * Updated version suggested by Geert De Deckere * - * @access public * @param string * @return string */ - function valid_ip($ip) - { + public function valid_ip($ip) { $ip_segments = explode('.', $ip); // Always 4 segments needed if (count($ip_segments) != 4) - { - return FALSE; - } + return false; + // IP can not start with 0 if ($ip_segments[0][0] == '0') - { - return FALSE; - } + return false; + // Check each segment - foreach ($ip_segments as $segment) - { + foreach ($ip_segments as $segment) { // IP segments must be digits and can not be // longer than 3 digits or greater then 255 - if ($segment == '' OR preg_match("/[^0-9]/", $segment) OR $segment > 255 OR strlen($segment) > 3) - { - return FALSE; + if($segment == '' || preg_match("/[^0-9]/", $segment) || $segment > 255 || strlen($segment) > 3) { + return false; } } - return TRUE; + return true; } // -------------------------------------------------------------------- @@ -437,17 +252,13 @@ function valid_ip($ip) /** * User Agent * - * @access public * @return string */ - function user_agent() - { - if ($this->user_agent !== FALSE) - { + public function user_agent() { + if($this->user_agent !== false) return $this->user_agent; - } - $this->user_agent = ( ! isset($_SERVER['HTTP_USER_AGENT'])) ? FALSE : $_SERVER['HTTP_USER_AGENT']; + $this->user_agent = (!isset($_SERVER['HTTP_USER_AGENT'])) ? false : $_SERVER['HTTP_USER_AGENT']; return $this->user_agent; } @@ -457,46 +268,44 @@ function user_agent() /** * Filename Security * - * @access public * @param string * @return string */ - function filename_security($str) - { + public function filename_security($str) { $bad = array( - "../", - "./", - "", - "<", - ">", - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - "%20", - "%22", - "%3c", // < - "%253c", // < - "%3e", // > - "%0e", // > - "%28", // ( - "%29", // ) - "%2528", // ( - "%26", // & - "%24", // $ - "%3f", // ? - "%3b", // ; - "%3d" // = - ); + "../", + "./", + "", + "<", + ">", + "'", + '"', + '&', + '$', + '#', + '{', + '}', + '[', + ']', + '=', + ';', + '?', + "%20", + "%22", + "%3c", // < + "%253c", // < + "%3e", // > + "%0e", // > + "%28", // ( + "%29", // ) + "%2528", // ( + "%26", // & + "%24", // $ + "%3f", // ? + "%3b", // ; + "%3d" // = + ); return stripslashes(str_replace($bad, '', $str)); } @@ -525,280 +334,212 @@ function filename_security($str) * harvested from examining vulnerabilities in other programs: * http://ha.ckers.org/xss.html * - * @access public * @param string * @return string */ - function xss_clean($str, $is_image = FALSE) - { - /* - * Is the string an array? - * - */ - if (is_array($str)) - { - while (list($key) = each($str)) - { - $str[$key] = $this->xss_clean($str[$key]); + public function xss_clean($str, $is_image = false) { + // Is the string an array? + if(is_array($str)) { + foreach ($str as $key => &$value) { + $str[$key] = $this->xss_clean($value); } return $str; } - /* - * Remove Invisible Characters - */ - $str = $this->_remove_invisible_characters($str); - - /* - * Protect GET variables in URLs - */ - - // 901119URL5918AMP18930PROTECT8198 - - $str = preg_replace('|\&([a-z\_0-9]+)\=([a-z\_0-9]+)|i', $this->xss_hash()."\\1=\\2", $str); - - /* - * Validate standard character entities - * - * Add a semicolon if missing. We do this to enable - * the conversion of entities to ASCII later. - * - */ - $str = preg_replace('#(&\#[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); - - /* - * Validate UTF16 two byte encoding (x00) - * - * Just as above, adds a semicolon if missing. - * - */ - $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); - - /* - * Un-Protect GET variables in URLs - */ - $str = str_replace($this->xss_hash(), '&', $str); - - /* - * URL Decode - * - * Just in case stuff like this is submitted: - * - * Google - * - * Note: Use rawurldecode() so it does not remove plus signs - * - */ - //$str = rawurldecode($str); - - /* - * Convert character entities to ASCII - * - * This permits our tests below to work reliably. - * We only convert entities that are within tags since - * these are the ones that will pose security problems. - * - */ - - $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); - - $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_html_entity_decode_callback'), $str); - - /* - * Remove Invisible Characters Again! - */ - $str = $this->_remove_invisible_characters($str); + // Remove Invisible Characters + $str = remove_invisible_characters($str); /* - * Convert all tabs to spaces - * - * This prevents strings like this: ja vascript - * NOTE: we deal with spaces between characters later. - * NOTE: preg_replace was found to be amazingly slow here on large blocks of data, - * so we use str_replace. - * - */ - - if (strpos($str, "\t") !== FALSE) - { - $str = str_replace("\t", ' ', $str); + * URL Decode + * + * Just in case stuff like this is submitted: + * + * Google + * + * Note: Use rawurldecode() so it does not remove plus signs + */ + if(stripos($str, '%') !== false) { + do { + $oldstr = $str; + $str = rawurldecode($str); + $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str); + } while ($oldstr !== $str); + + unset($oldstr); } /* - * Capture converted string for later comparison - */ - $converted_string = $str; + * Convert character entities to ASCII + * + * This permits our tests below to work reliably. + * We only convert entities that are within tags since + * these are the ones that will pose security problems. + */ + $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); + $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str); + + // Remove Invisible Characters Again! + $str = remove_invisible_characters($str); /* - * Not Allowed Under Any Conditions - */ + * Convert all tabs to spaces + * + * This prevents strings like this: ja vascript + * NOTE: we deal with spaces between characters later. + * NOTE: preg_replace was found to be amazingly slow here on + * large blocks of data, so we use str_replace. + */ + $str = str_replace("\t", ' ', $str); + + // Capture converted string for later comparison + $converted_string = $str; - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } + // Remove Strings that are never allowed + $str = $this->_do_never_allowed($str); /* - * Makes PHP tags safe - * - * Note: XML tags are inadvertently replaced too: - * - * '), array('<?', '?>'), $str); + * Makes PHP tags safe + * + * Note: XML tags are inadvertently replaced too: + * + * '), array('<?', '?>'), $str); } /* - * Compact any exploded words - * - * This corrects words like: j a v a s c r i p t - * These words are compacted back to their correct state. - * - */ - $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window'); - foreach ($words as $word) - { - $temp = ''; - - for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++) - { - $temp .= substr($word, $i, 1)."\s*"; - } + * Compact any exploded words + * + * This corrects words like: j a v a s c r i p t + * These words are compacted back to their correct state. + */ + $words = array( + 'javascript', 'expression', 'vbscript', 'jscript', 'wscript', + 'vbs', 'script', 'base64', 'applet', 'alert', 'document', + 'write', 'cookie', 'window', 'confirm', 'prompt', 'eval' + ); + + foreach ($words as $word) { + $word = implode('\s*', str_split($word)).'\s*'; // We only want to do this when it is followed by a non-word character // That way valid stuff like "dealer to" does not become "dealerto" - $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); + $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); } /* - * Remove disallowed Javascript in links or img tags - * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared - * to these simplified non-capturing preg_match(), especially if the pattern exists in the string - */ - do - { + * Remove disallowed Javascript in links or img tags + * We used to do some version comparisons and use of stripos(), + * but it is dog slow compared to these simplified non-capturing + * preg_match(), especially if the pattern exists in the string + * + * Note: It was reported that not only space characters, but all in + * the following pattern can be parsed as separators between a tag name + * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C] + * ... however, remove_invisible_characters() above already strips the + * hex-encoded ones, so we'll skip them below. + */ + do { $original = $str; - if (preg_match("/]*?)(>|$)#si", array($this, '_js_link_removal'), $str); + if (preg_match('/]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str); } - if (preg_match("/]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str); + if (preg_match('/]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str); } - if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str)) - { - $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str); + if (preg_match('/script|xss/i', $str)) { + $str = preg_replace('##si', '[removed]', $str); } } - while($original != $str); - + while ($original !== $str); unset($original); /* - * Remove JavaScript Event Handlers - * - * Note: This code is a little blunt. It removes - * the event handler and anything up to the closing >, - * but it's unlikely to be a problem. - * - */ - $event_handlers = array('[^a-z_\-]on\w*','xmlns'); - - if ($is_image === TRUE) - { - /* - * Adobe Photoshop puts XML metadata into JFIF images, including namespacing, - * so we have to allow this for images. -Paul - */ - unset($event_handlers[array_search('xmlns', $event_handlers)]); - } - - $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str); - - /* - * Sanitize naughty HTML elements - * - * If a tag containing any of the words in the list - * below is found, the tag gets converted to entities. - * - * So this: - * Becomes: <blink> - * - */ - $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss'; - $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str); + * Sanitize naughty HTML elements + * + * If a tag containing any of the words in the list + * below is found, the tag gets converted to entities. + * + * So this: + * Becomes: <blink> + */ + $pattern = '#' + .'<((?/*\s*)((?[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character + .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator + // optional attributes + .'(?(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons + .'[^\s\042\047>/=]+' // attribute characters + // optional attribute-value + .'(?:\s*=' // attribute-value separator + .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value + .')?' // end optional attribute-value group + .')*)' // end optional attributes group + .'[^>]*)(?\>)?#isS'; + + // Note: It would be nice to optimize this for speed, BUT + // only matching the naughty elements here results in + // false positives and in turn - vulnerabilities! + do { + $old_str = $str; + $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str); + } while ($old_str !== $str); + + unset($old_str); /* - * Sanitize naughty scripting elements - * - * Similar to above, only instead of looking for - * tags it looks for PHP and JavaScript commands - * that are disallowed. Rather than removing the - * code, it simply converts the parenthesis to entities - * rendering the code un-executable. - * - * For example: eval('some code') - * Becomes: eval('some code') - * - */ - $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str); + * Sanitize naughty scripting elements + * + * Similar to above, only instead of looking for + * tags it looks for PHP and JavaScript commands + * that are disallowed. Rather than removing the + * code, it simply converts the parenthesis to entities + * rendering the code un-executable. + * + * For example: eval('some code') + * Becomes: eval('some code') + */ + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', + '\\1\\2(\\3)', + $str + ); + + // Same thing, but for "tag functions" (e.g. eval`some code`) + // See https://github.com/bcit-ci/CodeIgniter/issues/5420 + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si', + '\\1\\2`\\3`', + $str + ); + + // Final clean up + // This adds a bit of extra precaution in case + // something got through the above filters + $str = $this->_do_never_allowed($str); /* - * Final clean up - * - * This adds a bit of extra precaution in case - * something got through the above filters - * - */ - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } - - /* - * Images are Handled in a Special Way - * - Essentially, we want to know that after all of the character conversion is done whether - * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean. - * However, if the string post-conversion does not matched the string post-removal of XSS, - * then it fails, as there was unwanted XSS code found and removed/changed during processing. - */ - - if ($is_image === TRUE) - { - if ($str == $converted_string) - { - return TRUE; - } - else - { - return FALSE; - } + * Images are Handled in a Special Way + * - Essentially, we want to know that after all of the character + * conversion is done whether any unwanted, likely XSS, code was found. + * If not, we return true, as the image is clean. + * However, if the string post-conversion does not matched the + * string post-removal of XSS, then it fails, as there was unwanted XSS + * code found and removed/changed during processing. + */ + if ($is_image === true) { + return ($str === $converted_string); } return $str; @@ -812,10 +553,8 @@ function xss_clean($str, $is_image = FALSE) * @access public * @return string */ - function xss_hash() - { - if ($this->xss_hash == '') - { + function xss_hash() { + if($this->xss_hash == '') { if (phpversion() >= 4.2) mt_srand(); else @@ -839,12 +578,10 @@ function xss_hash() * @param string * @return string */ - function _remove_invisible_characters($str) - { + function _remove_invisible_characters($str) { static $non_displayables; - if ( ! isset($non_displayables)) - { + if(!isset($non_displayables)) { // every control character except newline (dec 10), carriage return (dec 13), and horizontal tab (dec 09), $non_displayables = array( '/%0[0-8bcef]/', // url encoded 00-08, 11, 12, 14, 15 @@ -855,12 +592,10 @@ function _remove_invisible_characters($str) ); } - do - { + do { $cleaned = $str; $str = preg_replace($non_displayables, '', $str); - } - while ($cleaned != $str); + } while ($cleaned != $str); return $str; } @@ -877,69 +612,148 @@ function _remove_invisible_characters($str) * @param type * @return type */ - function _compact_exploded_words($matches) - { + function _compact_exploded_words($matches) { return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; } // -------------------------------------------------------------------- /** - * Sanitize Naughty HTML - * - * Callback function for xss_clean() to remove naughty HTML elements - * - * @access private - * @param array - * @return string - */ - function _sanitize_naughty_html($matches) - { - // encode opening brace - $str = '<'.$matches[1].$matches[2].$matches[3]; + * Sanitize Naughty HTML + * + * Callback method for xss_clean() to remove naughty HTML elements. + * + * @param array $matches + * @return string + */ + protected function _sanitize_naughty_html($matches) { + static $naughty_tags = array( + 'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound', + 'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer', + 'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object', + 'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss' + ); + + static $evil_attributes = array( + 'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime' + ); + + // First, escape unclosed tags + if (empty($matches['closeTag'])) + { + return '<'.$matches[1]; + } + // Is the element that we caught naughty? If so, escape it + elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE)) + { + return '<'.$matches[1].'>'; + } + // For other tags, see if their attributes are "evil" and strip those + elseif (isset($matches['attributes'])) + { + // We'll store the already filtered attributes here + $attributes = array(); + + // Attribute-catching pattern + $attributes_pattern = '#' + .'(?[^\s\042\047>/=]+)' // attribute characters + // optional attribute-value + .'(?:\s*=(?[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator + .'#i'; + + // Blacklist pattern for evil attribute names + $is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i'; + + // Each iteration filters a single attribute + do + { + // Strip any non-alpha characters that may precede an attribute. + // Browsers often parse these incorrectly and that has been a + // of numerous XSS issues we've had. + $matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']); - // encode captured opening or closing brace to prevent recursive vectors - $str .= str_replace(array('>', '<'), array('>', '<'), $matches[4]); + if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE)) + { + // No (valid) attribute found? Discard everything else inside the tag + break; + } - return $str; + if ( + // Is it indeed an "evil" attribute? + preg_match($is_evil_pattern, $attribute['name'][0]) + // Or does it have an equals sign, but no value and not quoted? Strip that too! + OR (trim($attribute['value'][0]) === '') + ) + { + $attributes[] = 'xss=removed'; + } + else + { + $attributes[] = $attribute[0][0]; + } + + $matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0])); + } + while ($matches['attributes'] !== ''); + + $attributes = empty($attributes) + ? '' + : ' '.implode(' ', $attributes); + return '<'.$matches['slash'].$matches['tagName'].$attributes.'>'; + } + + return $matches[0]; } // -------------------------------------------------------------------- /** - * JS Link Removal - * - * Callback function for xss_clean() to sanitize links - * This limits the PCRE backtracks, making it more performance friendly - * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in - * PHP 5.2+ on link-heavy strings - * - * @access private - * @param array - * @return string - */ - function _js_link_removal($match) - { - $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); } /** - * JS Image Removal - * - * Callback function for xss_clean() to sanitize image tags - * This limits the PCRE backtracks, making it more performance friendly - * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in - * PHP 5.2+ on image tag heavy strings - * - * @access private - * @param array - * @return string - */ - function _js_img_removal($match) - { - $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); } // -------------------------------------------------------------------- @@ -949,130 +763,166 @@ function _js_img_removal($match) * * Used as a callback for XSS Clean * - * @access public * @param array * @return string */ - function _convert_attribute($match) - { + protected function _convert_attribute($match) { return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); } // -------------------------------------------------------------------- /** - * HTML Entity Decode Callback + * Filter Attributes * - * Used as a callback for XSS Clean + * Filters tag attributes for consistency and safety * - * @access public - * @param array + * @param string $str * @return string */ - function _html_entity_decode_callback($match) - { - $charset = $this->charset; + protected function _filter_attributes($str) { + $out = ''; + + if(preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) + foreach ($matches[0] as $match) + $out .= preg_replace("#/\*.*?\*/#s", '', $match); - return $this->_html_entity_decode($match[0], strtoupper($charset)); + return $out; } // -------------------------------------------------------------------- /** - * HTML Entities Decode - * - * This function is a replacement for html_entity_decode() - * - * In some versions of PHP the native function does not work - * when UTF-8 is the specified character set, so this gives us - * a work-around. More info here: - * http://bugs.php.net/bug.php?id=25670 - * - * @access private - * @param string - * @param string - * @return string - */ - /* ------------------------------------------------- - /* Replacement for html_entity_decode() - /* -------------------------------------------------*/ - - /* - NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the - character set, and the PHP developers said they were not back porting the - fix to versions other than PHP 5.x. - */ - function _html_entity_decode($str, $charset='UTF-8') - { - if (stristr($str, '&') === FALSE) return $str; + * URL-decode taking spaces into account + * + * @see https://github.com/bcit-ci/CodeIgniter/issues/4877 + * @param array $matches + * @return string + */ + protected function _urldecodespaces($matches) { + $input = $matches[0]; + $nospaces = preg_replace('#\s+#', '', $input); + return ($nospaces === $input) ? $input : rawurldecode($nospaces); + } + + /** + * HTML Entity Decode Callback + * + * @param array $match + * @return string + */ + protected function _decode_entity($match) { + // Protect GET variables in URLs + // 901119URL5918AMP18930PROTECT8198 + $match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]); + + // Decode, then un-protect URL GET vars + return str_replace( + $this->xss_hash(), + '&', + $this->entity_decode($match, $this->charset) + ); + } - // The reason we are not using html_entity_decode() by itself is because - // while it is not technically correct to leave out the semicolon - // at the end of an entity most browsers will still interpret the entity - // correctly. html_entity_decode() does not convert entities without - // semicolons, so we are left with our own little solution here. Bummer. + /** + * HTML Entities Decode + * + * A replacement for html_entity_decode() + * + * The reason we are not using html_entity_decode() by itself is because + * while it is not technically correct to leave out the semicolon + * at the end of an entity most browsers will still interpret the entity + * correctly. html_entity_decode() does not convert entities without + * semicolons, so we are left with our own little solution here. Bummer. + * + * @link https://secure.php.net/html-entity-decode + * + * @param string $str Input + * @param string $charset Character set + * @return string + */ + public function entity_decode($str, $charset = NULL) { + if (strpos($str, '&') === false) + return $str; - if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR version_compare(phpversion(), '5.0.0', '>='))) - { - $str = @html_entity_decode($str, ENT_COMPAT, $charset); - // $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); - $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5})~i', '_chrhexdec', $str); - // return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); - return preg_replace_callback('~&#([0-9]{2,4})~', '_chr', $str); - } + static $_entities; - // Numeric Entities - // $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str); - $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5});{0,1}~i', '_chrhexdec', $str); - // $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str); - $str = preg_replace_callback('~&#([0-9]{2,4});{0,1}~', '_chr', $str); + isset($charset) OR $charset = $this->charset; + isset($_entities) OR $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, ENT_COMPAT | ENT_HTML5, $charset)); - // Literal Entities - Slightly slow so we do another check - if (stristr($str, '&') === FALSE) + do { - $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES))); - } + $str_compare = $str; + + // Decode standard entities, avoiding false positives + if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches)) + { + $replace = array(); + $matches = array_unique(array_map('strtolower', $matches[0])); + foreach ($matches as &$match) + { + if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE) + { + $replace[$match] = $char; + } + } + $str = str_replace(array_keys($replace), array_values($replace), $str); + } + + // Decode numeric & UTF16 two byte entities + $str = html_entity_decode( + preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str), + ENT_COMPAT | ENT_HTML5, + $charset + ); + } + while ($str_compare !== $str); return $str; } - // -------------------------------------------------------------------- - /** - * Filter Attributes - * - * Filters tag attributes for consistency and safety - * - * @access public - * @param string - * @return string - */ - function _filter_attributes($str) - { - $out = ''; + * Do Never Allowed + * + * @param string $str + * @return string + */ + protected function _do_never_allowed($str) { + $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str); - if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) - { - foreach ($matches[0] as $match) - { - $out .= preg_replace("#/\*.*?\*/#s", '', $match); - } - } + foreach ($this->_never_allowed_regex as $regex) + $str = preg_replace('#'.$regex.'#is', '[removed]', $str); - return $out; + return $str; } - - // -------------------------------------------------------------------- - } +/** + * Remove Invisible Characters + * + * This prevents sandwiching null characters + * between ascii characters, like Java\0script. + * + * @param string + * @param bool + * @return string + */ +function remove_invisible_characters($str, $url_encoded = true) { + $non_displayables = array(); + + // every control character except newline (dec 10), + // carriage return (dec 13) and horizontal tab (dec 09) + if ($url_encoded) { + $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 + $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 + $non_displayables[] = '/%7f/i'; // url encoded 127 + } -/* callback function added for use in _html_entity_decode */ -function _chr($m){ - return chr($m[1]); -} + $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 + + do { + $str = preg_replace($non_displayables, '', $str, -1, $count); + } while ($count); -/* callback function added for use in _html_entity_decode */ -function _chrhexdec($m){ - return chr(hexdec($m[1])); + return $str; } diff --git a/app/combo.class.php b/app/combo.class.php index c9c9f12..509881e 100644 --- a/app/combo.class.php +++ b/app/combo.class.php @@ -47,6 +47,8 @@ function Combo() { // Constructor function function Render() { global $Translation; $this->HTML = ''; + + if(!is_array($this->ListItem)) $this->ListItem = $this->ListData; $ArrayCount = count($this->ListItem); if($ArrayCount > count($this->ListData)) { @@ -58,9 +60,9 @@ function Render() { if($this->ListType != 2) { if($this->ApplySelect2) { - $this->HTML .= "SelectName" . ($this->ListType == 3 ? '[]' : '') . "\" id=\"$this->SelectID\"" . ($this->ListType == 1 ? ' size="' . ($this->ListBoxHeight < $ArrayCount ? $this->ListBoxHeight : ($ArrayCount + ($this->AllowNull ? 1 : 0))) . '"' : '') . ($this->ListType == 3 ? ' multiple' : '') . '>'; } else { - $this->HTML .= "SelectName".($this->ListType == 3 ? '[]' : '') . "\" id=\"$this->SelectID\" class=\"$this->Class\" style=\"$this->Style\"" . ($this->ListType == 1 ? ' size="' . ($this->ListBoxHeight < $ArrayCount ? $this->ListBoxHeight : ($ArrayCount + ($this->AllowNull ? 1 : 0))) . '"' : '') . ($this->ListType == 3 ? ' multiple' : '') . '>'; } if($this->ListType != 3 && $this->AllowNull) @@ -69,12 +71,12 @@ function Render() { if($this->ListType == 3) $arrSelectedData = explode($this->MultipleSeparator, $this->SelectedData); if($this->ListType == 3) $arrSelectedText = explode($this->MultipleSeparator, $this->SelectedText); for($i = 0; $i < $ArrayCount; $i++) { + $sel = ''; + if($this->ListType == 3) { if(in_array($this->ListData[$i], $arrSelectedData)) { $sel = "selected class=\"$this->SelectedClass\""; - $this->MatchText.=$this->ListItem[$i].$this->MultipleSeparator; - } else { - $sel = ''; + $this->MatchText .= $this->ListItem[$i] . $this->MultipleSeparator; } } else { if($this->SelectedData == $this->ListData[$i] || ($this->SelectedText == $this->ListItem[$i] && $this->SelectedText)) { @@ -82,14 +84,13 @@ function Render() { $this->MatchText = $this->ListItem[$i]; $this->SelectedData = $this->ListData[$i]; $this->SelectedText = $this->ListItem[$i]; - } else { - $sel = ''; } } - $this->HTML .= "\n\t"; + $this->HTML .= "\n\t"; } $this->HTML .= ''; + if($this->ApplySelect2) { $this->HTML .= ''; } - if($this->ListType == 3 && strlen($this->MatchText)>0) $this->MatchText=substr($this->MatchText, 0, -1 * strlen($this->MultipleSeparator)); + + if($this->ListType == 3 && strlen($this->MatchText) > 0) $this->MatchText = substr($this->MatchText, 0, -1 * strlen($this->MultipleSeparator)); } else { global $Translation; $separator = '       '; - $j=0; + $j = 0; $this->HTML .= '
'; + + $shift = 1; if($this->AllowNull) { - $this->HTML .= "SelectName$j\" type=\"radio\" name=\"$this->SelectName\" value=\"\" ".($this->SelectedData==''?'checked':'')."> "; - $this->HTML .= ($this->RadiosPerLine==1 ? '
' : $separator); - $shift=2; - } else { - $shift=1; + $this->HTML .= "SelectName}{$j}\" type=\"radio\" name=\"$this->SelectName\" value=\"\" " . ($this->SelectedData == '' ? 'checked' : '') . "> "; + $this->HTML .= ($this->RadiosPerLine == 1 ? '
' : $separator); + $shift = 2; } + for($i = 0; $i < $ArrayCount; $i++) { $j++; + $sel = ''; if($this->SelectedData == $this->ListData[$i] || ($this->SelectedText == $this->ListItem[$i] && $this->SelectedText)) { $sel = "checked class=\"$this->SelectedClass\""; - $this->MatchText=$this->ListItem[$i]; - $this->SelectedData=$this->ListData[$i]; - $this->SelectedText=$this->ListItem[$i]; - } else { - $sel = ''; + $this->MatchText = $this->ListItem[$i]; + $this->SelectedData = $this->ListData[$i]; + $this->SelectedText = $this->ListItem[$i]; } - $this->HTML .= "SelectName$j\" type=\"radio\" name=\"$this->SelectName\" value=\"{$this->ListData[$i]}\" $sel> "; - if(($i+$shift)%$this->RadiosPerLine) { - $this->HTML .= $separator; - } else { - $this->HTML .= '
'; - } + $safeVal = html_attr($this->ListData[$i]); + $this->HTML .= "SelectName}{$j}\" type=\"radio\" name=\"$this->SelectName\" value=\"$safeVal\" $sel> "; + $this->HTML .= ""; + + $this->HTML .= ($i + $shift) % $this->RadiosPerLine ? $separator : '
'; } $this->HTML .= '
'; } diff --git a/app/common.js.php b/app/common.js similarity index 92% rename from app/common.js.php rename to app/common.js index b049fbd..328d6ac 100644 --- a/app/common.js.php +++ b/app/common.js @@ -1,41 +1,5 @@ - var AppGini = AppGini || {}; -/* translation strings */ -AppGini.Translate = { - _map: , - _encoding: '' -} - /* initials and fixes */ jQuery(function() { AppGini.count_ajaxes_blocking_saving = 0; @@ -80,8 +44,6 @@ fix_lookup_width(field); }); - //fix_table_responsive_width(); - var full_img_factor = 0.9; /* xs */ if(window_width >= 992) full_img_factor = 0.6; /* md, lg */ else if(window_width >= 768) full_img_factor = 0.9; /* sm */ @@ -95,17 +57,10 @@ var mlabel = label.replace(/.*(<\/i>).*/, '$1'); $j(this).html(mlabel); }); - - // fix size of nicEditor, if present - var ne = $j('.nicEdit-panelContain'); - if(ne.length) { - ne.parent().width('100%').next().width('99.75%'); - $j('.nicEdit-main').width('99%'); - } }); - setTimeout(function() { /* */ $j(window).resize(); }, 1000); - setTimeout(function() { /* */ $j(window).resize(); }, 3000); + setTimeout(function() { $j(window).resize(); }, 1000); + setTimeout(function() { $j(window).resize(); }, 3000); /* don't allow saving detail view when there's an ajax request to a url that matches the following */ var ajax_blockers = new RegExp(/(ajax_combo\.php|_autofill\.php|ajax_check_unique\.php)/); @@ -119,7 +74,7 @@ if(s.url.match(ajax_blockers)) { AppGini.count_ajaxes_blocking_saving = Math.max(AppGini.count_ajaxes_blocking_saving - 1, 0); if(AppGini.count_ajaxes_blocking_saving <= 0) - $j('#update, #insert').prop('disabled', false); + $j('#update:not(.user-locked), #insert').prop('disabled', false); } }); @@ -166,7 +121,7 @@ }); /* fix behavior of select2 in bootstrap modal. See: https://github.com/ivaynberg/select2/issues/1436 */ - jQuery.fn.modal.Constructor.prototype.enforceFocus = function() { /* */ }; + jQuery.fn.modal.Constructor.prototype.enforceFocus = function() { }; /* remove empty navbar menus */ $j('nav li.dropdown').each(function() { @@ -177,8 +132,7 @@ update_action_buttons(); /* remove empty images and links from TV, TVP */ - var imgFolder = AppGini.Translate._map['ImageFolder']; - $j('.table a[href="' + imgFolder + '"], .table img[src="' + imgFolder + '"]').remove(); + $j('.table a[href="' + AppGini.imgFolder + '"], .table img[src="' + AppGini.imgFolder + '"]').remove(); /* remove empty email links from TV, TVP */ $j('a[href="mailto:"]').remove(); @@ -228,7 +182,7 @@ }) /* select email/web links on uncollapsing in DV */ - $j('.detail_view').on('click', '.btn[data-toggle="collapse"].collapsed', function() { + $j('.detail_view').on('click', '.btn[data-toggle="collapse"]', function() { var target = $j($j(this).data('target')); setTimeout(function() { target.focus(); }, 100); }); @@ -258,6 +212,9 @@ // apply keyboard shortcuts AppGini.handleKeyboardShortcuts(); AppGini.updateKeyboardShortcutsStatus(); + + // handle clearing dates + AppGini.handleClearingDates(); }); /* show/hide TV action buttons based on whether records are selected or not */ @@ -602,7 +559,7 @@ function random_string(string_length) { * @return array of IDs (PK values) of selected records in TV (records that the user checked) */ function get_selected_records_ids() { - return jQuery('.record_selector:checked').map(function() { /* */ return jQuery(this).val() }).get(); + return jQuery('.record_selector:checked').map(function() { return jQuery(this).val() }).get(); } function print_multiple_dv_tvdv(t, ids) { @@ -946,7 +903,7 @@ function enforce_uniqueness(table, field) { $j('#' + field + '-uniqueness-note').show(); $j('#' + field).parents('.form-group').addClass('has-error'); $j('#' + field).focus(); - setTimeout(function() { /* */ $j('#update, #insert').prop('disabled', true); }, 500); + setTimeout(function() { $j('#update, #insert').prop('disabled', true); }, 500); } } }) @@ -1204,7 +1161,9 @@ function select2_max_width_decrement() { var wh = $j(window).height(), mtm = mod.find(ipm + 'dialog').css('margin-top'), - mhfoh = mod.find(ipm + 'header').outerHeight() + mod.find(ipm + 'footer').outerHeight(); + mhfoh = mod.find(ipm + 'header').outerHeight(); + + if(mod.find(ipm + 'footer').length) mhfoh += mod.find(ipm + 'footer').outerHeight(); mod.find(ipm + 'dialog').css({ margin: mtm, @@ -1258,7 +1217,7 @@ function select2_max_width_decrement() { '' : op.message @@ -1335,7 +1294,7 @@ function select2_max_width_decrement() { '' : op.message @@ -1387,7 +1346,7 @@ function select2_max_width_decrement() { var id = op.id, rsz = _resize; rsz(id); - $j(window).resize(function() { /* */ rsz(id); }); + $j(window).resize(function() { rsz(id); }); }) //.agModal('show') .on('hidden.bs.modal', function() { @@ -1682,7 +1641,7 @@ function select2_max_width_decrement() { fileTypeError.html( fileTypeError .html() - .replace(//i, extensions.replace(/\|/g, ', ')) + .replace(/[<{]filetypes[}>]/i, extensions.replace(/\|/g, ', ')) ); return false; @@ -1702,7 +1661,7 @@ function select2_max_width_decrement() { fileSizeError.html( fileSizeError .html() - .replace(//i, Math.round(maxSize / 1024)) + .replace(/[<{]maxsize[}>]/i, Math.round(maxSize / 1024)) ); return false; @@ -1800,7 +1759,7 @@ function select2_max_width_decrement() { AppGini.lockUpdatesOnUserRequest = function() { // if this is not DV of existing record where editing and saving a copy are both enabled, skip - if(!$j('#update:visible').length || !$j('#insert:visible').length || !$j('input[name=SelectedID]').val().length) return; + if(!$j('#update').length || !$j('#insert').length || !$j('input[name=SelectedID]').val().length) return; // if lock behavior already implemented, skip if($j('#update').hasClass('locking-enabled')) return; @@ -1822,32 +1781,51 @@ function select2_max_width_decrement() { }) .prop('title', AppGini.Translate._map['Disable']) .click(function() { - var locker = $j(this); + var locker = $j(this), disable = !$j('#update').prop('disabled'); - $j('#update').prop('disabled', !$j('#update').prop('disabled')); + $j('#update').prop('disabled', disable).toggleClass('user-locked', disable); locker.toggleClass('active'); locker.prop('title', AppGini.Translate._map[locker.hasClass('active') ? 'Enable' : 'Disable']); }) } -/* function to focus on first element of a form, with support for select2 */ -AppGini.focusFirstFormElement = function() { +/* function to focus a specific element of a form, given field name */ +AppGini.focusFormElement = function(tn, fn) { if(AppGini.mobileDevice()) return; - var fieTop = 1000000; // some very large initial value for element tops - - var firstInputElem = $j('select, input[type=text], textarea, .nicEdit-main').not(':disabled').not('.select2-offscreen').filter(':visible').eq(0); - if(firstInputElem.length) fieTop = firstInputElem.offset().top; + var inputElem = $j([ + 'select[id=' + fn + '-mm]', + 'select[id=' + fn + ']', + 'input[type=text][id=' + fn + ']', + 'input[type=file][id=' + fn + ']', + 'input[type=radio][id=' + fn + '0]', + 'input[type=checkbox][id=' + fn + ']', + 'textarea[id=' + fn + ']', + '.' + tn + '-' + fn + ' .nicEdit-main' + ].join(',')).not(':disabled').not('.select2-offscreen').filter(':visible').eq(0); + + if(inputElem.length) { + inputElem.focus(); + AppGini.scrollToDOMElement(inputElem[0], -100); + return; + } - var firstSelect2 = $j('.select2-container').eq(0); - if(firstSelect2.length && firstSelect2.offset().top < fieTop) { - // we have a select2 on the top of the form, so focus it - $j('#' + firstSelect2.attr('id').replace(/^s2id_/, '')).select2('focus'); + // maybe the field is a web/email link? + var linker = $j('[id=' + fn + '-edit-link]'); + if(linker.length) { + linker.click(); + AppGini.scrollToDOMElement(linker[0], -100); return; } - firstInputElem.focus(); + // perhaps field is a select2? + var s2 = $j('[id=' + fn + '-container], [id=s2id_' + fn + ']'); + if(s2.length) { + s2.select2('focus'); + AppGini.scrollToDOMElement(s2[0], -100); + return; + } } AppGini.scrollTo = function(id, useName) { @@ -1861,8 +1839,8 @@ function select2_max_width_decrement() { AppGini.scrollToDOMElement(obj); } -AppGini.scrollToDOMElement = function(obj) { - var curtop = 0; +AppGini.scrollToDOMElement = function(obj, shift) { + var curtop = shift ? shift : 0; if(!obj.offsetParent) return; @@ -2160,3 +2138,20 @@ function select2_max_width_decrement() { return $j(inp).length > 0 && ($j(inp).val().length || $j(inp).text().length); } +AppGini.handleClearingDates = function() { + // run only once + if(AppGini._handleClearingDatesApplied != undefined) return; + AppGini._handleClearingDatesApplied = true; + + $j('.detail_view').on('click', '.fd-date-clearer', function() { + var dateField = $j(this).data('for'); + if(!dateField) return; + + var dropdowns = '#DF-mm, #DF-dd, #DF'.replace(/DF/g, dateField); + var oldDate = [0, 1, 2].reduce(function(prev, i) { return prev + $j(dropdowns).eq(i).val(); }, ''); + + $j(dropdowns).val(''); + if(oldDate != '') $j(this).parents('form').trigger('change'); + }) +} + diff --git a/app/data_combo.class.php b/app/data_combo.class.php index e0f6eb5..b1b1707 100644 --- a/app/data_combo.class.php +++ b/app/data_combo.class.php @@ -31,17 +31,17 @@ function DataCombo() { // Constructor function $this->Class = 'form-control Lookup'; $this->MatchText = ''; $this->ListType = 0; - $this->ListBoxHeight=10; - $this->RadiosPerLine=1; - $this->AllowNull=1; + $this->ListBoxHeight = 10; + $this->RadiosPerLine = 1; + $this->AllowNull = 1; } function Render() { global $Translation; - $eo['silentErrors']=true; + $eo = ['silentErrors' => true]; $result = sql($this->Query . ' limit ' . datalist_auto_complete_size, $eo); - if($eo['error']!='') { + if(!empty($eo['error'])) { $this->HTML = error_message(html_attr($eo['error']) . "\n\n\n\n"); return; } diff --git a/app/datalist.php b/app/datalist.php index 3f7cc60..af0aff1 100644 --- a/app/datalist.php +++ b/app/datalist.php @@ -224,7 +224,7 @@ function Render() { $this->HTML .= ''; $this->HTML .= ''; - $this->ContentType='tableview'; // default content type + $this->ContentType = 'tableview'; // default content type if($PrintTV != '') { $Print_x = 1; @@ -345,7 +345,7 @@ function Render() { } elseif($addNew_x != '') { - $SelectedID=''; + $SelectedID = ''; $this->hideTV(); } @@ -387,13 +387,13 @@ function Render() { $orderBy = []; if($SortField) { $sortFields = explode(',', $SortField); - $i=0; + $i = 0; foreach($sortFields as $sf) { $tob = preg_split('/\s+/', $sf, 2); - $orderBy[] = array(trim($tob[0]) => (strtolower(trim($tob[1]))=='desc' ? 'desc' : 'asc')); + $orderBy[] = [trim($tob[0]) => (strtolower(trim($tob[1])) == 'desc' ? 'desc' : 'asc')]; $i++; } - $orderBy[$i-1][$tob[0]] = (strtolower(trim($SortDirection))=='desc' ? 'desc' : 'asc'); + $orderBy[$i - 1][$tob[0]] = (strtolower(trim($SortDirection)) == 'desc' ? 'desc' : 'asc'); } // check if magic filter files exist @@ -429,9 +429,10 @@ function Render() { } // hidden variables .... - $this->HTML .= ''; - $this->HTML .= ''; - $this->HTML .= ''; + $this->HTML .= ''; + $this->HTML .= ''; + $this->HTML .= ''; + $this->HTML .= ''; $this->ContentType = 'filters'; return; @@ -474,20 +475,18 @@ function Render() { // apply quick search to the query if($SearchString != '') { - if($Search_x!='') { $FirstRecord=1; } + if($Search_x != '') { $FirstRecord = 1; } - if($this->QueryWhere=='') + if($this->QueryWhere == '') $this->QueryWhere = "where "; else $this->QueryWhere .= " and "; - foreach($this->QueryFieldsQS as $fName => $fCaption) { - if(strpos($fName, 'QuerySearchableFields[$fName]=$fCaption; - } - } + foreach($this->QueryFieldsQS as $fName => $fCaption) + if(strpos($fName, 'QuerySearchableFields[$fName] = $fCaption; - $this->QueryWhere.='('.implode(" LIKE '%".makeSafe($SearchString)."%' or ", array_keys($this->QuerySearchableFields))." LIKE '%".makeSafe($SearchString)."%')"; + $this->QueryWhere .= '(' . implode(" LIKE '%" . makeSafe($SearchString) . "%' or ", array_keys($this->QuerySearchableFields)) . " LIKE '%" . makeSafe($SearchString) . "%')"; } @@ -547,13 +546,13 @@ function Render() { $this->QueryWhere .= ' ' . $FilterAnd[$ij] . ' (' . $this->QueryFieldsIndexed[($FilterField[$ij])] . "='' or " . $this->QueryFieldsIndexed[($FilterField[$ij])] . ' is NULL) '; } elseif($FilterOperator[$ij] == 'is-not-empty' && !$isDateTime) { $this->QueryWhere .= ' ' . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . "!='' "; - } elseif($FilterOperator[$ij]=='is-empty' && $isDateTime) { + } elseif($FilterOperator[$ij] == 'is-empty' && $isDateTime) { $this->QueryWhere .= " " . $FilterAnd[$ij] . " (" . $this->QueryFieldsIndexed[($FilterField[$ij])] . "=0 or " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " is NULL) "; - } elseif($FilterOperator[$ij]=='is-not-empty' && $isDateTime) { + } elseif($FilterOperator[$ij] == 'is-not-empty' && $isDateTime) { $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . "!=0 "; - } elseif($FilterOperator[$ij]=='like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { + } elseif($FilterOperator[$ij] == 'like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " like '%" . makeSafe($FilterValue[$ij]) . "%' "; - } elseif($FilterOperator[$ij]=='not-like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { + } elseif($FilterOperator[$ij] == 'not-like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " not like '%" . makeSafe($FilterValue[$ij]) . "%' "; } elseif($isDate) { $dateValue = mysql_datetime($FilterValue[$ij]); @@ -701,7 +700,8 @@ function Render() { $this->HTML .= ''; } - // quick search and TV action buttons + // quick search and TV action buttons + $tvRowNeedsClosing = false; if(!$this->HideTableView && !($dvprint_x && $this->AllowSelection && $SelectedID) && !$PrintDV) { /* if user can print DV, add action to 'More' menu */ $selected_records_more = []; @@ -726,7 +726,7 @@ function Render() { /* if user is admin, add 'Change owner' action to 'More' menu */ /* also, add help link for adding more actions */ - if($mi['admin']) { + if(getLoggedAdmin() !== false) { $selected_records_more[] = array( 'function' => 'mass_change_owner', 'title' => $this->translation['Change owner'], @@ -809,6 +809,7 @@ function Render() { $this->HTML .= ''; $this->HTML .= '
'; + $tvRowNeedsClosing = true; } if($Print_x != '') { @@ -831,6 +832,7 @@ function Render() { $this->HTML .= ''; if(!$Print_x) $this->HTML .= ''; + // Templates $rowTemplate = $selrowTemplate = ''; if($this->Template) { @@ -841,16 +843,8 @@ function Render() { } // process translations - if($rowTemplate) { - foreach($this->translation as $symbol => $trans) { - $rowTemplate=str_replace("<%%TRANSLATION($symbol)%%>", $trans, $rowTemplate); - } - } - if($selrowTemplate) { - foreach($this->translation as $symbol => $trans) { - $selrowTemplate = str_replace("<%%TRANSLATION($symbol)%%>", $trans, $selrowTemplate); - } - } + $rowTemplate = parseTemplate($rowTemplate); + $selrowTemplate = parseTemplate($selrowTemplate); // End of templates // $this->ccffv: map $FilterField values to field captions as stored in ColCaption @@ -903,7 +897,7 @@ function Render() { if($this->AllowSorting == 1) { $sortCombo = new Combo; - for($i=0; $i < count($this->ColCaption); $i++) { + for($i = 0; $i < count($this->ColCaption); $i++) { $sortCombo->ListItem[] = $this->ColCaption[$i]; $sortCombo->ListData[] = $this->ColNumber[$i]; } @@ -917,7 +911,7 @@ function Render() { if($SortField) { $SortDirection = ($SortDirection == 'desc' ? 'asc' : 'desc'); $sort_class = ($SortDirection == 'asc' ? 'sort-by-attributes-alt' : 'sort-by-attributes'); - $sort = ""; + $sort = ""; $SortDirection = ($SortDirection == 'desc' ? 'asc' : 'desc'); } else { $sort = ''; @@ -965,7 +959,7 @@ function Render() { } if($this->AllowSelection == 1 && $SelectedID != $currentID) { - $rowTemp = str_replace('<%%SELECT%%>',"ScriptFileName}?SelectedID=" . html_attr($currentID) . "\" style=\"display: block; padding:0px;\">",$rowTemp); + $rowTemp = str_replace('<%%SELECT%%>',"ScriptFileName}?SelectedID=" . html_attr($currentID) . "\" style=\"display: block; padding:0px;\">",$rowTemp); $rowTemp = str_replace('<%%ENDSELECT%%>','',$rowTemp); } else { $rowTemp = str_replace('<%%SELECT%%>', '', $rowTemp); @@ -987,8 +981,8 @@ function Render() { if(strpos($rowTemp, "<%%YOUTUBETHUMB($fieldTVCaption)%%>") !== false) $rowTemp = str_replace("<%%YOUTUBETHUMB($fieldTVCaption)%%>", thisOr(get_embed('youtube', $fd, '', '', 'thumbnail_url'), 'blank.gif'), $rowTemp); if(strpos($rowTemp, "<%%GOOGLEMAPTHUMB($fieldTVCaption)%%>") !== false) $rowTemp = str_replace("<%%GOOGLEMAPTHUMB($fieldTVCaption)%%>", thisOr(get_embed('googlemap', $fd, '', '', 'thumbnail_url'), 'blank.gif'), $rowTemp); - if(thisOr($fd)==' ' && preg_match('//i', $rowTemp, $m)) { + $rowTemp = str_replace($m[0], '', $rowTemp); } } @@ -999,7 +993,7 @@ function Render() { // default view if no template for($j = 0; $j < $fieldCountTV; $j++) { if($this->AllowSelection == 1) { - $sel1 = "ScriptFileName}?SelectedID=" . html_attr($currentID) . "\" onclick=\"document.myform.SelectedID.value='" . addslashes($currentID) . "'; document.myform.submit(); return false;\" style=\"padding:0px;\">"; + $sel1 = "ScriptFileName}?SelectedID=" . html_attr($currentID) . "\" onclick=\"document.myform.SelectedID.value = '" . addslashes($currentID) . "'; document.myform.submit(); return false;\" style=\"padding:0px;\">"; $sel2 = ""; } else { $sel1 = ''; @@ -1019,14 +1013,14 @@ function Render() { $this->HTML = preg_replace("/]*>( )*<\/a>/", ' ', $this->HTML); $this->HTML = preg_replace("/<%%.*%%>/U", ' ', $this->HTML); // end of data - $this->HTML.=''; + $this->HTML .= ''; $this->HTML .= "\n"; if($Print_x == '') { // TV $pagesMenu = ''; if($RecordCount > $this->RecordsPerPage) { $pagesMenuId = "{$this->TableName}_pagesMenu"; - $pagesMenu = $this->translation['go to page'] . ' '; $pagesMenu .= ''; $pagesMenu .= ''; + if($SearchString != '' && $RecordCount) $this->HTML .= ''; if($Print_x == '' && $i) { // TV $this->HTML .= '
'; @@ -1121,7 +1115,7 @@ function Render() { $this->HTML .= '
'; $this->HTML .= '
'; - if($i < $RecordCount) $this->HTML .= ''; + if($i < $RecordCount) $this->HTML .= ''; $this->HTML .= '
'; $this->HTML .= '
'; } @@ -1134,7 +1128,7 @@ function Render() { // hidden variables .... foreach($this->filterers as $filterer => $caption) { if($_REQUEST['filterer_' . $filterer] != '') { - $this->HTML .= ""; + $this->HTML .= ""; break; // currently, only one filterer can be applied at a time } } @@ -1163,11 +1157,11 @@ function Render() { $FiltersCode .= "\n"; } } - $FiltersCode .= ""; + $FiltersCode .= ""; $this->HTML .= $FiltersCode; // display details form ... - if(($this->AllowSelection || $this->AllowInsert || $this->AllowUpdate || $this->AllowDelete) && $Print_x=='' && !$PrintDV) { + if(($this->AllowSelection || $this->AllowInsert || $this->AllowUpdate || $this->AllowDelete) && $Print_x == '' && !$PrintDV) { if(($this->SeparateDV && $this->HideTableView) || !$this->SeparateDV) { $dvCode = call_user_func_array($this->TableName . '_form', [ $SelectedID, @@ -1234,7 +1228,7 @@ function Render() { // hidden vars foreach($this->filterers as $filterer => $caption) { if($_REQUEST['filterer_' . $filterer] != '') { - $this->HTML .= ""; + $this->HTML .= ""; break; // currently, only one filterer can be applied at a time } } @@ -1251,7 +1245,7 @@ function Render() { $dvCode .= call_user_func_array($this->TableName . '_form', array($id, 0, 0, 0, 1, $this->TemplateDV, $this->TemplateDVP)); } - if($dvCode!='') { + if($dvCode != '') { $dvCode = preg_replace('//', '', $dvCode); $this->HTML .= $dvCode; } @@ -1261,7 +1255,8 @@ function Render() { } } - $this->HTML .= "
"; + if($tvRowNeedsClosing) $this->HTML .= ""; + $this->HTML .= ""; $this->HTML .= '
'; // $this->HTML .= ''.html_attr($tvQuery).''; // uncomment this line for debugging the table view query @@ -1272,9 +1267,9 @@ function Render() { if($PrintDV != '') $this->ContentType = 'print-detailview'; // call detail view javascript hook file if found - $dvJSHooksFile=dirname(__FILE__).'/hooks/'.$this->TableName.'-dv.js'; - if(is_file($dvJSHooksFile) && ($this->ContentType=='detailview' || $this->ContentType=='tableview+detailview')) { - $this->HTML.="\n\n"; + $dvJSHooksFile = dirname(__FILE__) . '/hooks/' . $this->TableName . '-dv.js'; + if(is_file($dvJSHooksFile) && ($this->ContentType == 'detailview' || $this->ContentType == 'tableview+detailview')) { + $this->HTML .= "\n\n"; } return; @@ -1480,15 +1475,6 @@ function tv_tools() { return ob_get_clean(); } - function templateTranslate($template) { - $tr = &$this->translation; - return preg_replace_callback( - '/<%%TRANSLATION\((.*?)\)%%>/', - function($m) use($tr) { return !empty($tr[$m[1]]) ? $tr[$m[1]] : $m[1]; }, - $template - ); - } - function resetSelection() { if($this->SeparateDV) return "document.myform.SelectedID.value = '';"; @@ -1499,7 +1485,7 @@ function resetSelection() { function getTVButtons($print = false) { $buttons = ''; - if($print) return $this->templateTranslate( + if($print) return parseTemplate( '' . '' ); @@ -1510,24 +1496,24 @@ function getTVButtons($print = false) { // display Print icon if($this->AllowPrinting) - $buttons .= ''; + $buttons .= ''; // display CSV icon if($this->AllowCSV) - $buttons .= ''; + $buttons .= ''; // display Filter icon if($this->AllowFilters) - $buttons .= ''; + $buttons .= ''; // display Show All icon if(($this->AllowFilters)) - $buttons .= ''; + $buttons .= ''; return str_replace( '<%%RESET_SELECTION%%>', $this->resetSelection(), - $this->templateTranslate($buttons) + parseTemplate($buttons) ); } diff --git a/app/date_combo.class.php b/app/date_combo.class.php index 0b49258..26d9120 100644 --- a/app/date_combo.class.php +++ b/app/date_combo.class.php @@ -33,12 +33,12 @@ function DateCombo() { $this->CSSCommentClass = ''; } - function GetHTML($readOnly=false) { - list($xy, $xm, $xd)=explode('-', $this->DefaultDate); + function GetHTML($readOnly = false) { + list($xy, $xm, $xd) = explode('-', $this->DefaultDate); //$y : render years combo $years = new Combo; - for($i=$this->MinYear; $i<=$this->MaxYear; $i++) { + for($i = $this->MinYear; $i <= $this->MaxYear; $i++) { $years->ListItem[] = $i; $years->ListData[] = $i; } @@ -53,7 +53,7 @@ function GetHTML($readOnly=false) { //$m : render months combo $months = new Combo; - for($i=1; $i<=12; $i++) { + for($i = 1; $i <= 12; $i++) { $months->ListData[] = $i; } $months->ListItem = explode(",", $this->MonthNames); @@ -68,7 +68,7 @@ function GetHTML($readOnly=false) { //$d : render days combo $days = new Combo; - for($i=1; $i<=31; $i++) { + for($i = 1; $i <= 31; $i++) { $days->ListItem[] = $i; $days->ListData[] = $i; } @@ -100,7 +100,15 @@ function GetHTML($readOnly=false) { $editable_date .= '' . $y . ''; break; } - if($i == 2) $editable_date .= ''; + + if($i == 2) $editable_date .= + '' . + '
' . + '' . + '' . + '
' . + '
' . + ''; } $editable_date .= ''; diff --git a/app/defaultFilters.php b/app/defaultFilters.php index f8149a3..baf3f96 100644 --- a/app/defaultFilters.php +++ b/app/defaultFilters.php @@ -1,7 +1,7 @@
diff --git a/app/defaultLang.php b/app/defaultLang.php index e009a0c..c707c8b 100644 --- a/app/defaultLang.php +++ b/app/defaultLang.php @@ -239,9 +239,9 @@ 'member approval email subject control' => 'When the admin approves a member, the member is notified by
email that he is approved. You can control the subject of the
approval email in this box, and the content in the box below.', 'member approval email message' => 'Member approval
email message', 'MySQL date' => 'MySQL date
formatting string', - 'MySQL reference' => 'Please refer to
the MySQL reference for possible formats.', + 'MySQL reference' => 'Please refer to the MySQL reference for possible formats.', 'PHP short date' => 'PHP short date
formatting string', - 'PHP manual' => 'Please refer to the PHP manual for possible formats.', + 'PHP manual' => 'Please refer to the PHP manual for possible formats.', 'PHP long date' => 'PHP long date
formatting string', 'groups per page' => 'Groups per page', 'members per page' => 'Members per page', @@ -758,4 +758,16 @@ // Added in 5.91 'keyboard shorcuts reference' => 'Keyboard shortcuts reference', + + // Added in 5.93 + 'google API key' => 'Google API key', + 'google API key instructions' => 'How to get and configure your Google API key?', + 'base upload path' => 'Base upload path', + 'base upload path change warning' => 'If you change the base upload path, existing uploaded files/images will not be accessible from the app until moving them to the new upload path.', + 'base upload path instructions' => 'The path were uploaded files/images are to be stored. The path is assumed to be relative to the app path, but you can start it with ../ to specify a path outside the app.', + 'invalid upload path' => 'Specified base upload path does not exists and could not be created.', + 'Appearance' => 'Appearance', + 'Mail' => 'Mail', + 'Preconfigured users and groups' => 'Preconfigured users and groups', + 'Application' => 'Application', ]; diff --git a/app/dynamic.css.php b/app/dynamic.css similarity index 89% rename from app/dynamic.css.php rename to app/dynamic.css index 5890745..dc9cf45 100644 --- a/app/dynamic.css.php +++ b/app/dynamic.css @@ -1,5 +1,3 @@ - - @media (min-width: 768px) { .container{ width: 90% !important; } } @media (max-width: 767px) { .detail_view{ padding: 0; } } @media print{ @@ -31,15 +29,54 @@ /* prevent prototype conflicts */ li.dropdown{ display: block !important; } +/* horizontal spacer */ .hspacer-xs{ margin-left: 0.1em; margin-right: 0.1em; } .hspacer-sm{ margin-left: 0.2em; margin-right: 0.2em; } .hspacer-md{ margin-left: 0.4em; margin-right: 0.4em; } .hspacer-lg{ margin-left: 0.8em; margin-right: 0.8em; } + +/* right spacer in ltr context */ +.container:not(.theme-rtl) .rspacer-xs{ margin-right: 0.1em; } +.container:not(.theme-rtl) .rspacer-sm{ margin-right: 0.2em; } +.container:not(.theme-rtl) .rspacer-md{ margin-right: 0.4em; } +.container:not(.theme-rtl) .rspacer-lg{ margin-right: 0.8em; } + +/* right spacer in rtl context */ +.theme-rtl .rspacer-xs{ margin-left: 0.1em; } +.theme-rtl .rspacer-sm{ margin-left: 0.2em; } +.theme-rtl .rspacer-md{ margin-left: 0.4em; } +.theme-rtl .rspacer-lg{ margin-left: 0.8em; } + +/* left spacer in ltr context */ +.container:not(.theme-rtl) .lspacer-xs{ margin-left: 0.1em; } +.container:not(.theme-rtl) .lspacer-sm{ margin-left: 0.2em; } +.container:not(.theme-rtl) .lspacer-md{ margin-left: 0.4em; } +.container:not(.theme-rtl) .lspacer-lg{ margin-left: 0.8em; } + +/* left spacer in rtl context */ +.theme-rtl .lspacer-xs{ margin-right: 0.1em; } +.theme-rtl .lspacer-sm{ margin-right: 0.2em; } +.theme-rtl .lspacer-md{ margin-right: 0.4em; } +.theme-rtl .lspacer-lg{ margin-right: 0.8em; } + +/* vertical spacer */ .vspacer-xs{ margin-top: 0.1em; margin-bottom: 0.1em; } .vspacer-sm{ margin-top: 0.2em; margin-bottom: 0.2em; } .vspacer-md{ margin-top: 0.4em; margin-bottom: 0.4em; } .vspacer-lg{ margin-top: 0.8em; margin-bottom: 0.8em; } +/* top spacer */ +.tspacer-xs{ margin-top: 0.1em; } +.tspacer-sm{ margin-top: 0.2em; } +.tspacer-md{ margin-top: 0.4em; } +.tspacer-lg{ margin-top: 0.8em; } + +/* bottom spacer */ +.bspacer-xs{ margin-bottom: 0.1em; } +.bspacer-sm{ margin-bottom: 0.2em; } +.bspacer-md{ margin-bottom: 0.4em; } +.bspacer-lg{ margin-bottom: 0.8em; } + div.datePicker{ font-size: 1.3em; } .always_shown{ display: inline !important; } .always-shown-block { display: block !important; } @@ -347,3 +384,21 @@ .theme-compact .help-shortcuts-launcher-container { margin: 10px !important; } + +/* web and email links in DV */ +.detail_view a > .glyphicon-globe { + vertical-align: top; + font-size: x-large; +} +.detail_view a > .glyphicon-envelope { + vertical-align: text-bottom; + font-size: large; +} + +.glyphicon-lg { + font-size: 2em; +} + +/* fix nicedit components' widths */ +.has-nicedit > div { width: 100% !important; } +.has-nicedit .nicEdit-main { width: 99% !important; } diff --git a/app/employment_and_income_history_autofill.php b/app/employment_and_income_history_autofill.php index a15bdfe..5c649ba 100644 --- a/app/employment_and_income_history_autofill.php +++ b/app/employment_and_income_history_autofill.php @@ -1,5 +1,5 @@ true]; - $uploads_dir = realpath(dirname(__FILE__) . '/../' . $Translation['ImageFolder']); $safe_sid = makeSafe($source_id); // launch requests, asynchronously @@ -246,7 +245,7 @@ function employment_and_income_history_form($selected_id = '', $AllowUpdate = 1, $combo_employed_from->DefaultDate = $row['employed_from']; $combo_employed_till->DefaultDate = $row['employed_till']; $urow = $row; /* unsanitized data */ - $hc = new CI_Input(); + $hc = new CI_Input(datalist_db_encoding); $row = $hc->xss_clean($row); /* sanitize data */ } else { $combo_tenant->SelectedData = $filterer_tenant; @@ -264,7 +263,7 @@ function employment_and_income_history_form($selected_id = '', $AllowUpdate = 1, jQuery(function() { setTimeout(function() { if(typeof(tenant_reload__RAND__) == 'function') tenant_reload__RAND__(); - }, 10); /* we need to slightly delay client-side execution of the above code to allow AppGini.ajaxCache to work */ + }, 50); /* we need to slightly delay client-side execution of the above code to allow AppGini.ajaxCache to work */ }); function tenant_reload__RAND__() { @@ -291,17 +290,17 @@ function tenant_reload__RAND__() { }); }, width: '100%', - formatNoMatches: function(term) { /* */ return ''; }, + formatNoMatches: function(term) { return ''; }, minimumResultsForSearch: 5, loadMorePadding: 200, ajax: { url: 'ajax_combo.php', dataType: 'json', cache: true, - data: function(term, page) { /* */ return { s: term, p: page, t: 'employment_and_income_history', f: 'tenant' }; }, - results: function(resp, page) { /* */ return resp; } + data: function(term, page) { return { s: term, p: page, t: 'employment_and_income_history', f: 'tenant' }; }, + results: function(resp, page) { return resp; } }, - escapeMarkup: function(str) { /* */ return str; } + escapeMarkup: function(str) { return str; } }).on('change', function(e) { AppGini.current_tenant__RAND__.value = e.added.id; AppGini.current_tenant__RAND__.text = e.added.text; @@ -507,9 +506,7 @@ function tenant_reload__RAND__() { } // process translations - foreach($Translation as $symbol=>$trans) { - $templateCode = str_replace("<%%TRANSLATION($symbol)%%>", $trans, $templateCode); - } + $templateCode = parseTemplate($templateCode); // clear scrap $templateCode = str_replace('<%%', ' @@ -23,7 +24,7 @@ - + + - +

<%%DETAIL_VIEW_TITLE%%>

-
+
<%%TRANSLATION(Loading ...)%%>
+