0) {
// also trim off extra characters they want gone
$el = trim($el, $trimchars);
}
// Add to the returned array if there is anything left
if (strlen($el) > 0) {
$array_trimmed[$index] = $el;
$index++;
}
}
if (isset($unshiftblank)) {
array_unshift($array_trimmed, "");
}
return $array_trimmed;
}
/**
* Takes a value as returned from a check-list field type and reformats to be more display-friendly.
* Check-list fields have a leading comma.
*
* @param string $list
* @return string
*/
function tidylist($list)
{
$list = trim((string) $list);
if (strpos($list, ",") === false) {
return $list;
}
$list = explode(",", $list);
if (trim($list[0]) == "") {
array_shift($list);
} # remove initial comma used to identify item is a list
return join(", ", trim_array($list));
}
/**
* Trims $text to $length if necessary. Tries to trim at a space if possible. Adds three full stops if trimmed...
*
* @param string $text
* @param integer $length
* @return string
*/
function tidy_trim($text, $length)
{
$text = trim($text);
if (strlen($text) > $length) {
$text = mb_substr($text, 0, $length - 3, 'utf-8');
# Trim back to the last space
$t = strrpos($text, " ");
$c = strrpos($text, ",");
if ($c !== false) {
$t = $c;
}
if ($t > 5) {
$text = substr($text, 0, $t);
}
$text = $text . "...";
}
return $text;
}
/**
* Returns the average length of the strings in an array
*
* @param array $array
* @return float
*/
function average_length($array)
{
if (count($array) == 0) {
return 0;
}
$total = 0;
for ($n = 0; $n < count($array); $n++) {
$total += strlen(i18n_get_translated($array[$n]));
}
return $total / count($array);
}
/**
* Returns a list of activity types for which we have stats data (Search, User Session etc.)
*
* @return array
*/
function get_stats_activity_types()
{
return ps_array("SELECT DISTINCT activity_type `value` FROM daily_stat ORDER BY activity_type", array());
}
/**
* Replace escaped newlines with real newlines.
*
* @param string $text
* @return string
*/
function newlines($text)
{
$text = str_replace("\\n", "\n", $text);
$text = str_replace("\\r", "\r", $text);
return $text;
}
/**
* Returns a list of all available editable site text (content). If $find is specified
* a search is performed across page, name and text fields.
*
* @param string $findpage
* @param string $findname
* @param string $findtext
* @return array
*/
function get_all_site_text($findpage = "", $findname = "", $findtext = "")
{
global $defaultlanguage,$languages,$applicationname,$storagedir,$homeanim_folder;
$findname = trim($findname);
$findpage = trim($findpage);
$findtext = trim($findtext);
$return = array();
// en should always be included as it is the fallback language of the system
$search_languages = array('en');
if ('en' != $defaultlanguage) {
$search_languages[] = $defaultlanguage;
}
// When searching text, search all languages to pick up matches for languages other than the default. Add array so that default is first then we can skip adding duplicates.
if ('' != $findtext) {
$search_languages = $search_languages + array_keys($languages);
}
global $language, $lang; // Need to save these for later so we can revert after search
$languagesaved = $language;
$langsaved = $lang;
foreach ($search_languages as $search_language) {
# Reset $lang and include the appropriate file to search.
$lang = array();
# Include language file
$searchlangfile = __DIR__ . "/../languages/" . safe_file_name($search_language) . ".php";
if (file_exists($searchlangfile)) {
include $searchlangfile;
}
include __DIR__ . "/../languages/" . safe_file_name($search_language) . ".php";
# Include plugin languages in reverse order as per boot.php
global $plugins;
$language = $search_language;
for ($n = count($plugins) - 1; $n >= 0; $n--) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin_language($plugins[$n]);
}
# Find language strings.
ksort($lang);
foreach ($lang as $key => $text) {
$pagename = "";
$s = explode("__", $key);
if (count($s) > 1) {
$pagename = $s[0];
$key = $s[1];
}
if (
!is_array($text) # Do not support overrides for array values (used for months)... complex UI needed and very unlikely to need overrides.
&&
($findname == "" || stripos($key, $findname) !== false)
&&
($findpage == "" || stripos($pagename, $findpage) !== false)
&&
($findtext == "" || stripos($text, $findtext) !== false)
) {
$testrow = array();
$testrow["page"] = $pagename;
$testrow["name"] = $key;
$testrow["text"] = $text;
$testrow["language"] = $defaultlanguage;
$testrow["group"] = "";
// Make sure this isn't already set for default/another language
if (!in_array($testrow, $return)) {
$row["page"] = $pagename;
$row["name"] = $key;
$row["text"] = $text;
$row["language"] = $search_language;
$row["group"] = "";
$return[] = $row;
}
}
}
}
// Need to revert to saved values
$language = $languagesaved;
$lang = $langsaved;
# If searching, also search overridden text in site_text and return that also.
if ($findtext != "" || $findpage != "" || $findname != "") {
if ($findtext != "") {
$search = "text LIKE ? HAVING language = ? OR language = ? ORDER BY (CASE WHEN language = ? THEN 3 WHEN language = ? THEN 2 ELSE 1 END)";
$search_param = array("s", '%' . $findtext . '%', "s", $language, "s", $defaultlanguage, "s", $language, "s", $defaultlanguage);
}
if ($findpage != "") {
$search = "page LIKE ? HAVING language = ? OR language = ? ORDER BY (CASE WHEN language = ? THEN 2 ELSE 1 END)";
$search_param = array("s", '%' . $findpage . '%', "s", $language, "s", $defaultlanguage, "s", $language);
}
if ($findname != "") {
$search = "name LIKE ? HAVING language = ? OR language = ? ORDER BY (CASE WHEN language = ? THEN 2 ELSE 1 END)";
$search_param = array("s", '%' . $findname . '%', "s", $language, "s", $defaultlanguage, "s", $language);
}
$site_text = ps_query("select `page`, `name`, `text`, ref, `language`, specific_to_group, custom from site_text where $search", $search_param);
foreach ($site_text as $text) {
$row["page"] = $text["page"];
$row["name"] = $text["name"];
$row["text"] = $text["text"];
$row["language"] = $text["language"];
$row["group"] = $text["specific_to_group"];
// Make sure we dont'include the default if we have overwritten
$customisedtext = false;
for ($n = 0; $n < count($return); $n++) {
if ($row["page"] == $return[$n]["page"] && $row["name"] == $return[$n]["name"] && $row["language"] == $return[$n]["language"] && $row["group"] == $return[$n]["group"]) {
$customisedtext = true;
$return[$n] = $row;
}
}
if (!$customisedtext) {
$return[] = $row;
}
}
}
// Clean returned array so it contains unique records by name
$unique_returned_records = array();
$existing_lang_names = array();
$i = 0;
foreach (array_reverse($return) as $returned_record) {
if (!in_array($returned_record['name'], $existing_lang_names)) {
$existing_lang_names[$i] = $returned_record['name'];
$unique_returned_records[$i] = $returned_record;
}
$i++;
}
// Reverse again so that the default language appears first in results
return array_values(array_reverse($unique_returned_records));
}
/**
* Returns a specific site text entry.
*
* @param string $page
* @param string $name
* @param string $getlanguage
* @param string $group
* @return string
*/
function get_site_text($page, $name, $getlanguage, $group)
{
global $defaultlanguage, $lang, $language; // Registering plugin text uses $language and $lang
global $applicationname, $storagedir, $homeanim_folder; // These are needed as they are referenced in lang files
$params = array("s", $page, "s", $name, "s", $getlanguage);
if ($group == "") {
$stg_sql_cond = ' is null';
} else {
$stg_sql_cond = ' = ?';
$params = array_merge($params, array("i", $group));
}
$text = ps_query("select `page`, `name`, `text`, ref, `language`, specific_to_group, custom from site_text where page = ? and name = ? and language = ? and specific_to_group $stg_sql_cond", $params);
if (count($text) > 0) {
return $text[0]["text"];
}
# Fall back to default language.
$text = ps_query("select `page`, `name`, `text`, ref, `language`, specific_to_group, custom from site_text where page = ? and name = ? and language = ? and specific_to_group $stg_sql_cond", $params);
if (count($text) > 0) {
return $text[0]["text"];
}
# Fall back to default group.
$text = ps_query("select `page`, `name`, `text`, ref, `language`, specific_to_group, custom from site_text where page = ? and name = ? and language = ? and specific_to_group is null", array("s", $page, "s", $name, "s", $defaultlanguage));
if (count($text) > 0) {
return $text[0]["text"];
}
# Fall back to language strings.
if ($page == "") {
$key = $name;
} else {
$key = $page . "__" . $name;
}
# Include specific language(s)
$defaultlangfile = __DIR__ . "/../languages/" . safe_file_name($defaultlanguage) . ".php";
if (file_exists($defaultlangfile)) {
include $defaultlangfile;
}
$getlangfile = __DIR__ . "/../languages/" . safe_file_name($getlanguage) . ".php";
if (file_exists($getlangfile)) {
include $getlangfile;
}
# Include plugin languages in reverse order as per boot.php
global $plugins;
$language = $defaultlanguage;
for ($n = count($plugins) - 1; $n >= 0; $n--) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin_language($plugins[$n]);
}
$language = $getlanguage;
for ($n = count($plugins) - 1; $n >= 0; $n--) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin_language($plugins[$n]);
}
if (array_key_exists($key, $lang)) {
return $lang[$key];
} elseif (array_key_exists("all_" . $key, $lang)) {
return $lang["all_" . $key];
} else {
return "";
}
}
/**
* Check if site text section is custom, i.e. deletable.
*
* @param mixed $page
* @param mixed $name
*/
function check_site_text_custom($page, $name): bool
{
$check = ps_query("select custom from site_text where page = ? and name = ?", array("s", $page, "s", $name));
return $check[0]["custom"] ?? false;
}
/**
* Saves the submitted site text changes to the database.
*
* @param string $page
* @param string $name
* @param string $language
* @param integer $group
* @return void
*/
function save_site_text($page, $name, $language, $group)
{
global $lang,$custom,$newcustom,$defaultlanguage,$newhelp;
if (!is_int_loose($group)) {
$group = null;
}
$text = getval("text", "");
if ($newcustom) {
$params = ["s",$page,"s",$name];
$test = ps_query("SELECT ref,page,name,text,language,specific_to_group,custom FROM site_text WHERE page=? AND name=?", $params);
if (count($test) > 0) {
return true;
}
}
if (is_null($custom) || trim($custom) == "") {
$custom = 0;
}
if (getval("deletecustom", "") != "") {
$params = ["s",$page,"s",$name];
ps_query("DELETE FROM site_text WHERE page=? AND name=?", $params);
} elseif (getval("deleteme", "") != "") {
$params = ["s",$page,"s",$name,"i",$group];
ps_query("DELETE FROM site_text WHERE page=? AND name=? AND specific_to_group <=> ?", $params);
} elseif (getval("copyme", "") != "") {
$params = ["s",$page,"s",$name,"s",$text,"s",$language,"i",$group,"i",$custom];
ps_query("INSERT INTO site_text(page,name,text,language,specific_to_group,custom) VALUES (?,?,?,?,?,?)", $params);
} elseif (getval("newhelp", "") != "") {
$params = ["s",$newhelp];
$check = ps_query("SELECT ref,page,name,text,language,specific_to_group,custom FROM site_text where page = 'help' and name=?", $params);
if (!isset($check[0])) {
$params = ["s",$page,"s",$newhelp,"s","","s",$language,"i",$group];
ps_query("INSERT INTO site_text(page,name,text,language,specific_to_group) VALUES (?,?,?,?,?)", $params);
}
} else {
$params = ["s",$page,"s",$name,"s",$language,"i",$group];
$curtext = ps_query("SELECT ref,page,name,text,language,specific_to_group,custom FROM site_text WHERE page=? AND name=? AND language=? AND specific_to_group <=> ?", $params);
if (count($curtext) == 0) {
# Insert a new row for this language/group.
$params = ["s",$page,"s",$name,"s",$text,"s",$language,"i",$group,"i",$custom];
ps_query("INSERT INTO site_text(page,name,text,language,specific_to_group,custom) VALUES (?,?,?,?,?,?)", $params);
log_activity($lang["text"], LOG_CODE_CREATED, $text, 'site_text', null, "'{$page}','{$name}','{$language}',{$group}");
} else {
# Update existing row
$params = ["s",$text,"s",$page,"s",$name,"s",$language,"i",$group];
ps_query("UPDATE site_text SET text=? WHERE page=? AND name=? AND language=? AND specific_to_group <=> ?", $params);
log_activity($lang["text"], LOG_CODE_EDITED, $text, 'site_text', null, "'{$page}','{$name}','{$language}',{$group}");
}
# Language clean up - remove all entries that are exactly the same as the default text.
$params = ["s",$page,"s",$name,"s",$defaultlanguage,"i",$group];
$defaulttext = ps_value("SELECT text value FROM site_text WHERE page=? AND name=? AND language=? AND specific_to_group<=>?", $params, "");
$params = ["s",$page,"s",$name,"s",$defaultlanguage,"s",trim($defaulttext)];
ps_query("DELETE FROM site_text WHERE page=? AND name=? AND language != ? AND trim(text)=?", $params);
}
// Clear cache
clear_query_cache("sitetext");
}
/**
* Return a human-readable string representing $bytes in either KB or MB.
*
* @param integer $bytes
* @return string
*/
function formatfilesize($bytes)
{
# Binary mode
$multiple = 1024;
$lang_suffix = "-binary";
# Decimal mode, if configured
global $byte_prefix_mode_decimal;
if ($byte_prefix_mode_decimal) {
$multiple = 1000;
$lang_suffix = "";
}
global $lang;
if ($bytes < $multiple) {
return number_format((double)$bytes) . " " . escape($lang["byte-symbol"]);
} elseif ($bytes < pow($multiple, 2)) {
return number_format((double)ceil($bytes / $multiple)) . " " . escape($lang["kilobyte-symbol" . $lang_suffix]);
} elseif ($bytes < pow($multiple, 3)) {
return number_format((double)$bytes / pow($multiple, 2), 1) . " " . escape($lang["megabyte-symbol" . $lang_suffix]);
} elseif ($bytes < pow($multiple, 4)) {
return number_format((double)$bytes / pow($multiple, 3), 1) . " " . escape($lang["gigabyte-symbol" . $lang_suffix]);
} else {
return number_format((double)$bytes / pow($multiple, 4), 1) . " " . escape($lang["terabyte-symbol" . $lang_suffix]);
}
}
/**
* Converts human readable file size (e.g. 10 MB, 200.20 GB) into bytes.
*
* @param string $str
* @return int the result is in bytes
*/
function filesize2bytes($str)
{
$bytes = 0;
$bytes_array = array(
'b' => 1,
'kb' => 1024,
'mb' => 1024 * 1024,
'gb' => 1024 * 1024 * 1024,
'tb' => 1024 * 1024 * 1024 * 1024,
'pb' => 1024 * 1024 * 1024 * 1024 * 1024,
);
$bytes = floatval($str);
if (preg_match('#([KMGTP]?B)$#si', $str, $matches) && !empty($bytes_array[strtolower($matches[1])])) {
$bytes *= $bytes_array[strtolower($matches[1])];
}
$bytes = intval(round($bytes, 2));
#add leading zeroes (as this can be used to format filesize data in resource_data for sorting)
return sprintf("%010d", $bytes);
}
/**
* Get the mime type for a file on disk
*
* @param string $path
* @param string $ext
* @param ?bool $file_based_detection Determine the MIME type:
* - null: Check by extension or using exiftool
* - true: Only file based (uses exiftool)
* - false: Only based on extension
*/
function get_mime_type($path, $ext = null, ?bool $file_based_detection = null): array
{
if (empty($ext)) {
$ext = parse_filename_extension($path);
}
if (!$file_based_detection && ($found_by_ext = get_mime_types_by_extension($ext)) && $found_by_ext !== []) {
return $found_by_ext;
}
# Get mime type via exiftool if possible
$exiftool_fullpath = get_utility_path("exiftool");
if (($file_based_detection || $file_based_detection === null) && $exiftool_fullpath != false) {
$command = $exiftool_fullpath . " -s -s -s -t -mimetype " . escapeshellarg($path);
$file_mime_type = trim(run_command($command));
if ($file_mime_type !== '') {
return [$file_mime_type];
}
}
return ['application/octet-stream'];
}
/**
* Find matching MIME type(s) for a file extension.
*
* @param string $extension
* @return list
';
} elseif ($placeholder == "embed_thumbnail") {
# [embed_thumbnail] (requires url in templatevars['thumbnail'])
$thumbcid = uniqid('thumb');
$embed_thumbnail = true;
$setvalues[$placeholder] = "";
} else {
// Not recognised, skip this
continue;
}
# Don't overwrite templatevars that have been explicitly passed
if (!isset($templatevars[$placeholder]) && isset($setvalues[$placeholder])) {
$templatevars[$placeholder] = $setvalues[$placeholder];
}
}
foreach ($templatevars as $key => $value) {
$template = str_replace("[" . $key . "]", nl2br($value), $template);
}
$body = $template;
}
}
if (!isset($body)) {
$body = $message;
}
global $use_smtp,$smtp_secure,$smtp_host,$smtp_port,$smtp_auth,$smtp_username,$smtp_password,$debug_log,$smtpautotls, $smtp_debug_lvl;
$mail = new PHPMailer\PHPMailer\PHPMailer();
// use an external SMTP server? (e.g. Gmail)
if ($use_smtp) {
$mail->IsSMTP(); // enable SMTP
$mail->SMTPAuth = $smtp_auth; // authentication enabled/disabled
$mail->SMTPSecure = $smtp_secure; // '', 'tls' or 'ssl'
$mail->SMTPAutoTLS = $smtpautotls;
$mail->SMTPDebug = ($debug_log ? $smtp_debug_lvl : 0);
$mail->Debugoutput = function (string $msg, int $debug_lvl) {
debug("SMTPDebug: {$msg}");
};
$mail->Host = $smtp_host; // hostname
$mail->Port = $smtp_port; // port number
$mail->Username = $smtp_username; // username
$mail->Password = $smtp_password; // password
}
$reply_tos = explode(",", $reply_to);
if (!$from_system) {
// only one from address is possible, so only use the first one:
if (strstr($reply_tos[0], "<")) {
$rtparts = explode("<", $reply_tos[0]);
$mail->From = str_replace(">", "", $rtparts[1]);
$mail->FromName = $rtparts[0];
} else {
$mail->From = $reply_tos[0];
$mail->FromName = $from_name;
}
} else {
$mail->From = $from;
$mail->FromName = $from_name;
}
// if there are multiple addresses, that's what replyto handles.
for ($n = 0; $n < count($reply_tos); $n++) {
if (strstr($reply_tos[$n], "<")) {
$rtparts = explode("<", $reply_tos[$n]);
$mail->AddReplyto(str_replace(">", "", $rtparts[1]), $rtparts[0]);
} else {
$mail->AddReplyto($reply_tos[$n], $from_name);
}
}
# modification to handle multiple comma delimited emails
# such as for a multiple $email_notify
$emails = $email;
$emails = explode(',', $emails);
$emails = array_map('trim', $emails);
foreach ($emails as $email) {
if (strstr($email, "<")) {
$emparts = explode("<", $email);
$mail->AddAddress(str_replace(">", "", $emparts[1]), $emparts[0]);
} else {
$mail->AddAddress($email);
}
}
if ($cc != "") {
# modification for multiple is also necessary here, though a broken cc seems to be simply removed by phpmailer rather than breaking it.
$ccs = $cc;
$ccs = explode(',', $ccs);
$ccs = array_map('trim', $ccs);
global $userfullname;
foreach ($ccs as $cc) {
if (strstr($cc, "<")) {
$ccparts = explode("<", $cc);
$mail->AddCC(str_replace(">", "", $ccparts[1]), $ccparts[0]);
} else {
$mail->AddCC($cc, $userfullname);
}
}
}
if ($bcc != "") {
# modification for multiple is also necessary here, though a broken cc seems to be simply removed by phpmailer rather than breaking it.
$bccs = $bcc;
$bccs = explode(',', $bccs);
$bccs = array_map('trim', $bccs);
global $userfullname;
foreach ($bccs as $bccemail) {
if (strstr($bccemail, "<")) {
$bccparts = explode("<", $bccemail);
$mail->AddBCC(str_replace(">", "", $bccparts[1]), $bccparts[0]);
} else {
$mail->AddBCC($bccemail, $userfullname);
}
}
}
$mail->CharSet = "utf-8";
if (is_html($body)) {
$mail->IsHTML(true);
// Standardise line breaks
$body = str_replace(["\r\n","\r","\n","
","
"], "
", $body);
// Remove any sequences of three or more line breaks with doubles
while (strpos($body, "
") !== false) {
$body = str_replace("
", "
", $body);
}
// Also remove any unnecessary line breaks that were already formatted by HTML paragraphs
$body = str_replace(["
";
debug("PHPMailer Error: email: " . $email . " - " . $e->errorMessage());
exit;
}
unset($GLOBALS["use_error_exception"]);
}
/**
* Log email
*
* Data logged is:
* Time
* To address
* From, User ID or 0 for system emails (cron etc.)
* Subject
*
* @param string $email
* @param string $subject
* @param string $sender The email address of the sender
* @return void
*/
function log_mail($email, $subject, $sender)
{
global $userref;
if (isset($userref)) {
$from = $userref;
} else {
$from = 0;
}
$sub = mb_strcut($subject, 0, 100);
// Record a separate log entry for each email recipient
$email_recipients = explode(', ', $email);
$sql = array();
$params = array();
foreach ($email_recipients as $email_recipient) {
$sql[] = '(NOW(), ?, ?, ?, ?)';
$params = array_merge($params, array("s", $email_recipient, "i", $from, "s", $sub, "s", $sender));
}
// Write log to database
ps_query("INSERT into mail_log (`date`, mail_to, mail_from, `subject`, sender_email) VALUES " . implode(", ", $sql) . ";", $params);
}
/**
* Quoted printable encoding is rather simple.
* Each character in the string $string should be encoded if:
* Character code is <0x20 (space)
* Character is = (as it has a special meaning: 0x3d)
* Character is over ASCII range (>=0x80)
*
* @param string $string
* @param integer $linelen
* @param string $linebreak
* @param integer $breaklen
* @param boolean $encodecrlf
* @return string
*/
function rs_quoted_printable_encode($string, $linelen = 0, $linebreak = "=\r\n", $breaklen = 0, $encodecrlf = false)
{
$len = strlen($string);
$result = '';
for ($i = 0; $i < $len; $i++) {
if ($linelen >= 76) { // break lines over 76 characters, and put special QP linebreak
$linelen = $breaklen;
$result .= $linebreak;
}
$c = ord($string[$i]);
if (($c == 0x3d) || ($c >= 0x80) || ($c < 0x20)) { // in this case, we encode...
if ((($c == 0x0A) || ($c == 0x0D)) && (!$encodecrlf)) { // but not for linebreaks
$result .= chr($c);
$linelen = 0;
continue;
}
$result .= '=' . str_pad(strtoupper(dechex($c)), 2, '0');
$linelen += 3;
continue;
}
$result .= chr($c); // normal characters aren't encoded
$linelen++;
}
return $result;
}
/**
* As rs_quoted_printable_encode() but for e-mail subject
*
* @param string $string
* @param string $encoding
* @return string
*/
function rs_quoted_printable_encode_subject($string, $encoding = 'UTF-8')
{
// use this function with headers, not with the email body as it misses word wrapping
$len = strlen($string);
$result = '';
$enc = false;
for ($i = 0; $i < $len; ++$i) {
$c = $string[$i];
if (ctype_alpha($c)) {
$result .= $c;
} elseif ($c == ' ') {
$result .= '_';
$enc = true;
} else {
$result .= sprintf("=%02X", ord($c));
$enc = true;
}
}
//L: so spam agents won't mark your email with QP_EXCESS
if (!$enc) {
return $string;
}
return '=?' . $encoding . '?q?' . $result . '?=';
}
/**
* A generic pager function used by many display lists in ResourceSpace.
*
* Requires the following globals to be set or passed inb the $options array
* $url - Current page url
* $curpage - Current page
* $totalpages - Total number of pages
*
* @param boolean $break
* @param boolean $scrolltotop
* @param array $options - array of options to use instead of globals
* @return void
*/
function pager($break = true, $scrolltotop = true, $options = array())
{
global $curpage, $url, $url_params, $totalpages, $offset, $per_page, $jumpcount, $pagename, $confirm_page_change, $lang;
$curpage = $options['curpage'] ?? $curpage;
$url = $options['url'] ?? $url;
$url_params = $options['url_params'] ?? $url_params;
$totalpages = $options['totalpages'] ?? $totalpages;
$offset = $options['offset'] ?? $offset;
$per_page = $options['per_page'] ?? $per_page;
$jumpcount = $options['jumpcount'] ?? $jumpcount;
$confirm_page_change = $options['confirm_page_change'] ?? $confirm_page_change;
$modal = ('true' == getval('modal', ''));
$scroll = $scrolltotop ? "true" : "false";
$jumpcount++;
// If pager URL includes query string params, remove them and store in $url_params array
if (!isset($url_params) && strpos($url, "?") !== false) {
$urlparts = explode("?", $url);
parse_str($urlparts[1], $url_params);
$url = $urlparts[0];
}
unset($url_params["offset"]);
if ($totalpages != 0 && $totalpages != 1) { ?>
1) { ?>
"
href=" "prev","offset" => ($offset - $per_page)));?>"
onclick=" return Load(this, );"
>
1) ? "" : ''; ?>
"
onclick="
document.getElementById('jumppanel').style.display='inline';
document.getElementById('jumplink').style.display='none';
document.getElementById('jumpto').focus();
return false;">
"
href=" "next","offset" => ($offset + $per_page)));?>"
onclick=" return Load(this, );">
Page Load | |
Hook cache hits | |
Hook cache entries | |
Query count | |
Query time | |
Dupes | |
details |
" ; $error .= isset($s[$line - 2]) ? trim(escape($s[$line - 2])) . ""; return $error; } else { return true; } } /** * Utility function to generate URLs with query strings easier, with the ability * to override existing query string parameters when needed. * * @param string $url * @param array $parameters Default query string params (e.g "k", which appears on most of ResourceSpace URLs) * @param array $set_params Override existing query string params */ function generateURL(string $url, array $parameters = array(), array $set_params = array()): string { foreach ($set_params as $set_param => $set_value) { if ('' != $set_param) { $parameters[$set_param] = $set_value; } } $query_string_params = array(); foreach ($parameters as $parameter => $parameter_value) { if (!is_array($parameter_value)) { $query_string_params[$parameter] = (string) $parameter_value; } } # Ability to hook in and change the URL. $hookurl = hook("generateurl", "", array($url)); if ($hookurl !== false) { $url = $hookurl; } return $url . '?' . http_build_query($query_string_params); } /** * Utility function used to move the element of one array from a position * to another one in the same array * Note: the manipulation is done on the same array * * @param array $array * @param integer $from_index Array index we are moving from * @param integer $to_index Array index we are moving to * * @return void */ function move_array_element(array &$array, $from_index, $to_index) { $out = array_splice($array, $from_index, 1); array_splice($array, $to_index, 0, $out); } /** * Check if a value that may equate to false in PHP is actually a zero * * @param mixed $value * @return boolean */ function emptyiszero($value) { return $value !== null && $value !== false && trim($value) !== ''; } /** * Get data for each image that should be used on the slideshow. * The format of the returned array should be: * Array * ( * [0] => Array * ( * [ref] => 1 * [resource_ref] => * [homepage_show] => 1 * [featured_collections_show] => 0 * [login_show] => 1 * [file_path] => /var/www/filestore/system/slideshow_1bf4796ac6f051a/1.jpg * [checksum] => 1539875502 * ) * * [1] => Array * ( * [ref] => 4 * [resource_ref] => 19 * [homepage_show] => 1 * [featured_collections_show] => 0 * [login_show] => 0 * [file_path] => /var/www/filestore/system/slideshow_1bf4796ac6f051a/4.jpg * [checksum] => 1542818794 * [link] => http://localhost/?r=19 * ) * * ) * * @return array */ function get_slideshow_files_data() { global $baseurl, $homeanim_folder; $homeanim_folder_path = dirname(__DIR__) . "/{$homeanim_folder}"; $slideshow_records = ps_query("SELECT ref, resource_ref, homepage_show, featured_collections_show, login_show FROM slideshow", array(), "slideshow"); $slideshow_files = array(); foreach ($slideshow_records as $slideshow) { $slideshow_file = $slideshow; $image_file_path = "{$homeanim_folder_path}/{$slideshow['ref']}.jpg"; if (!file_exists($image_file_path) || !is_readable($image_file_path)) { continue; } $slideshow_file['checksum'] = filemtime($image_file_path); $slideshow_file['file_path'] = $image_file_path; $slideshow_file['file_url'] = generateURL( "{$baseurl}/pages/download.php", array( 'slideshow' => $slideshow['ref'], 'nc' => $slideshow_file['checksum'], ) ); $slideshow_files[] = $slideshow_file; } return $slideshow_files; } /** * Returns a sanitised row from the table in a safe form for use in a form value, * suitable overwritten by POSTed data if it has been supplied. * * @param array $row * @param string $name * @param string $default * @return string */ function form_value_display($row, $name, $default = "") { if (!is_array($row)) { return false; } if (array_key_exists($name, $row)) { $default = $row[$name]; } return escape((string) getval($name, $default)); } /** * Change the user's user group * * @param integer $user * @param integer $usergroup * @return void */ function user_set_usergroup($user, $usergroup) { ps_query("update user set usergroup = ? where ref = ?", array("i", $usergroup, "i", $user)); } /** * Generates a random string of requested length. * * Used to generate initial spider and scramble keys. * * @param int $length Length of desired string of bytes * @return string Random character string */ function generateSecureKey(int $length = 64): string { return bin2hex(openssl_random_pseudo_bytes($length / 2)); } /** * Check if current page is a modal and set global $modal variable if not already set * * @return boolean true if modal, false otherwise */ function IsModal() { global $modal; if (isset($modal) && $modal) { return true; } return getval("modal", "") == "true"; } /** * Generates a CSRF token (Encrypted Token Pattern) * * @uses generateSecureKey() * @uses rsEncrypt() * * @param string $session_id The current user session ID * @param string $form_id A unique form ID * * @return string Token */ function generateCSRFToken($session_id, $form_id) { // IMPORTANT: keep nonce at the beginning of the data array $data = json_encode(array( "nonce" => generateSecureKey(128), "session" => $session_id, "timestamp" => time(), "form_id" => $form_id )); return rsEncrypt($data, $session_id); } /** * Checks if CSRF Token is valid * * @uses rs_validate_token() * * @return boolean Returns TRUE if token is valid or CSRF is not enabled, FALSE otherwise */ function isValidCSRFToken($token_data, $session_id) { global $CSRF_enabled; if (!$CSRF_enabled) { return true; } return rs_validate_token($token_data, $session_id); } /** * Render the CSRF Token input tag * * @uses generateCSRFToken() * * @param string $form_id The id/ name attribute of the form * * @return void */ function generateFormToken($form_id) { global $CSRF_enabled, $CSRF_token_identifier, $usersession; if (!$CSRF_enabled) { return; } $token = generateCSRFToken($usersession, $form_id); ?> generateCSRFToken($GLOBALS['usersession'] ?? null, $name)]) : '{}'; } /** * Create an HTML data attribute holding a CSRF token (JS) object * * @param string $fct_name The name of the API function called (e.g create_resource) */ function generate_csrf_data_for_api_native_authmode(string $fct_name): string { return $GLOBALS['CSRF_enabled'] ? sprintf(' data-api-native-csrf="%s"', escape(generate_csrf_js_object($fct_name))) : ''; } /** * Enforce using POST requests * * @param boolean $ajax Set to TRUE if request is done via AJAX * * @return boolean|void Returns true if request method is POST or sends 405 header otherwise */ function enforcePostRequest($ajax) { if ($_SERVER["REQUEST_METHOD"] === "POST") { return true; } header("HTTP/1.1 405 Method Not Allowed"); $ajax = filter_var($ajax, FILTER_VALIDATE_BOOLEAN); if ($ajax) { global $lang; $return["error"] = array( "status" => 405, "title" => $lang["error-method-not_allowed"], "detail" => $lang["error-405-method-not_allowed"] ); echo json_encode($return); exit(); } return false; } /** * Check if ResourceSpace is up to date or an upgrade is available * * @uses get_sysvar() * @uses set_sysvar() * * @return boolean */ function is_resourcespace_upgrade_available() { $cvn_cache = get_sysvar('centralised_version_number'); $last_cvn_update = get_sysvar('last_cvn_update'); $centralised_version_number = $cvn_cache; debug("RS_UPGRADE_AVAILABLE: cvn_cache = {$cvn_cache}"); debug("RS_UPGRADE_AVAILABLE: last_cvn_update = $last_cvn_update"); if ($last_cvn_update !== false) { $cvn_cache_interval = DateTime::createFromFormat('Y-m-d H:i:s', $last_cvn_update)->diff(new DateTime()); if ($cvn_cache_interval->days >= 1) { $centralised_version_number = false; } } if ($centralised_version_number === false) { $default_socket_timeout_cache = ini_get('default_socket_timeout'); ini_set('default_socket_timeout', 5); //Set timeout to 5 seconds incase server cannot access resourcespace.com $use_error_exception_cache = $GLOBALS["use_error_exception"] ?? false; $GLOBALS["use_error_exception"] = true; try { $centralised_version_number = file_get_contents('https://www.resourcespace.com/current_release.txt'); } catch (Exception $e) { $centralised_version_number = false; } $GLOBALS["use_error_exception"] = $use_error_exception_cache; ini_set('default_socket_timeout', $default_socket_timeout_cache); debug("RS_UPGRADE_AVAILABLE: centralised_version_number = $centralised_version_number"); if ($centralised_version_number === false) { debug("RS_UPGRADE_AVAILABLE: unable to get centralised_version_number from https://www.resourcespace.com/current_release.txt"); set_sysvar('last_cvn_update', date('Y-m-d H:i:s')); return false; } set_sysvar('centralised_version_number', $centralised_version_number); set_sysvar('last_cvn_update', date('Y-m-d H:i:s')); } $get_version_details = function ($version) { $version_data = explode('.', $version); if (empty($version_data)) { return array(); } $return = array( 'major' => isset($version_data[0]) ? (int) $version_data[0] : 0, 'minor' => isset($version_data[1]) ? (int) $version_data[1] : 0, 'revision' => isset($version_data[2]) ? (int) $version_data[2] : 0, ); if ($return['major'] == 0) { return array(); } return $return; }; $product_version = trim(str_replace('SVN', '', $GLOBALS['productversion'])); $product_version_data = $get_version_details($product_version); $cvn_data = $get_version_details($centralised_version_number); debug("RS_UPGRADE_AVAILABLE: product_version = $product_version"); debug("RS_UPGRADE_AVAILABLE: centralised_version_number = $centralised_version_number"); if (empty($product_version_data) || empty($cvn_data)) { return false; } if ( ($product_version_data['major'] < $cvn_data['major']) || ($product_version_data['major'] == $cvn_data['major'] && $product_version_data['minor'] < $cvn_data['minor']) ) { return true; } return false; } /** * Fetch a count of recently active users * * @param int $days How many days to look back * * @return integer */ function get_recent_users($days) { return ps_value("SELECT count(*) value FROM user WHERE datediff(now(), last_active) <= ?", array("i", $days), 0); } /** * Return the total number of approved * * @return integer The number of approved users */ function get_total_approved_users(): int { return ps_value("SELECT COUNT(*) value FROM user WHERE approved = 1", [], 0); } /** * Return the number of resources in the system with optional filter by archive state * * @param int|bool $status Archive state to filter by if required * @return int Number of resources in the system, filtered by status if provided */ function get_total_resources($status = false): int { $sql = new PreparedStatementQuery("SELECT COUNT(*) value FROM resource WHERE ref>0", []); if (is_int($status)) { $sql->sql .= " AND archive=?"; $sql->parameters = array_merge($sql->parameters, ["i",$status]); } return ps_value($sql->sql, $sql->parameters, 0); } /** * Check if script last ran more than the failure notification days * Note: Never/ period longer than allowed failure should return false * * @param string $name Name of the sysvar to check the record for * @param integer $fail_notify_allowance How long to allow (in days) before user can consider script has failed * @param string $last_ran_datetime Datetime (string format) when script was last run * * @return boolean */ function check_script_last_ran($name, $fail_notify_allowance, &$last_ran_datetime) { $last_ran_datetime = (trim($last_ran_datetime) === '' ? $GLOBALS['lang']['status-never'] : $last_ran_datetime); if (trim($name) === '') { return false; } $script_last_ran = ps_value("SELECT `value` FROM sysvars WHERE name = ?", array("s", $name), ''); $script_failure_notify_seconds = intval($fail_notify_allowance) * 24 * 60 * 60; if ('' != $script_last_ran) { $last_ran_datetime = date('l F jS Y @ H:m:s', strtotime($script_last_ran)); // It's been less than user allows it to last run, meaning it is all good! if (time() < (strtotime($script_last_ran) + $script_failure_notify_seconds)) { return true; } } return false; } /** * Counting errors found in a collection of items. An error is found when an item has an "error" key. * * @param array $a Collection of items that may contain errors. * * @return integer */ function count_errors(array $a) { return array_reduce( $a, function ($carry, $item) { if (isset($item["error"])) { return ++$carry; } return $carry; }, 0 ); } /** * Function can be used to order a multi-dimensional array using a key and corresponding value * * @param array $array2search multi-dimensional array in which the key/value pair may be present * @param string $search_key key of the key/value pair used for search * @param string $search_value value of the key/value pair to search * @param array $return_array array to which the matching elements in the search array are pushed - also returned by function * * @return array $return_array */ function search_array_by_keyvalue($array2search, $search_key, $search_value, $return_array) { if (!isset($search_key, $search_value, $array2search, $return_array) || !is_array($array2search) || !is_array($return_array)) { exit("Error: invalid input to search_array_by_keyvalue function"); } // loop through array to search foreach ($array2search as $sub_array) { // if the search key exists and its value matches the search value if (array_key_exists($search_key, $sub_array) && ($sub_array[$search_key] == $search_value)) { // push the sub array to the return array array_push($return_array, $sub_array); } } return $return_array; } /** * Temporary bypass access controls for a particular function * * When functions check for permissions internally, in order to keep backwards compatibility it may be better if we * temporarily bypass the permissions instead of adding a parameter to the function for this. It will allow developers to * keep the code clean. * * IMPORTANT: never make this function public to the API. * * Example code: * $log = bypass_permissions(array("v"), "get_resource_log", array($ref)); * * @param array $perms Permission list to be bypassed * @param callable $f Callable that we need to bypas permissions for * @param array $p Parameters to be passed to the callable if required * * @return mixed */ function bypass_permissions(array $perms, callable $f, array $p = array()) { global $userpermissions; if (empty($perms)) { return call_user_func_array($f, $p); } // fake having these permissions temporarily $o_perm = $userpermissions; $userpermissions = array_values(array_merge($userpermissions ?? [], $perms)); $result = call_user_func_array($f, $p); $userpermissions = $o_perm; return $result; } /** * Set a system variable (which is stored in the sysvars table) - set to null to remove * * @param mixed $name Variable name * @param mixed $value String to set a new value; null to remove any existing value. * @return bool */ function set_sysvar($name, $value = null) { global $sysvars; db_begin_transaction("set_sysvar"); ps_query("DELETE FROM `sysvars` WHERE `name` = ?", array("s", $name)); if ($value != null) { ps_query("INSERT INTO `sysvars` (`name`, `value`) values (?, ?)", array("s", $name, "s", $value)); } db_end_transaction("set_sysvar"); //Update the $sysvars array or get_sysvar() won't be aware of this change $sysvars[$name] = $value; // Clear query cache so the change takes effect clear_query_cache("sysvars"); return true; } /** * Get a system variable (which is received from the sysvars table) * * @param string $name * @param string $default Returned if no matching variable was found * @return string */ function get_sysvar($name, $default = false) { // Check the global array. global $sysvars; if (isset($sysvars) && array_key_exists($name, $sysvars)) { return $sysvars[$name]; } // Load from db or return default return ps_value("SELECT `value` FROM `sysvars` WHERE `name` = ?", array("s", $name), $default, "sysvars"); } /** * Plugin architecture. Look for hooks with this name (and corresponding page, if applicable) and run them sequentially. * Utilises a cache for significantly better performance. * Enable $draw_performance_footer in config.php to see stats. * * @param string $name * @param string $pagename * @param array $params * @param boolean $last_hook_value_wins * @return mixed */ function hook($name, $pagename = "", $params = array(), $last_hook_value_wins = false) { global $hook_cache; if ($pagename == '') { global $pagename; } # the index name for the $hook_cache $hook_cache_index = $name . "|" . $pagename; # we have already processed this hook name and page combination before so return from cache if (isset($hook_cache[$hook_cache_index])) { # increment stats global $hook_cache_hits; $hook_cache_hits++; unset($GLOBALS['hook_return_value']); $empty_global_return_value = true; // we use $GLOBALS['hook_return_value'] so that hooks can directly modify the overall return value // $GLOBALS["hook_return_value"] will be unset by new calls to hook() - when using $GLOBALS["hook_return_value"] make sure // the value is used or stored locally before calling hook() or functions using hook(). foreach ($hook_cache[$hook_cache_index] as $function) { $function_return_value = call_user_func_array($function, $params); if ($function_return_value === null) { continue; // the function did not return a value so skip to next hook call } if ( !$last_hook_value_wins && !$empty_global_return_value && isset($GLOBALS['hook_return_value']) && (gettype($GLOBALS['hook_return_value']) == gettype($function_return_value)) && (is_array($function_return_value) || is_string($function_return_value) || is_bool($function_return_value)) ) { if (is_array($function_return_value)) { // We merge the cached result with the new result from the plugin and remove any duplicates // Note: in custom plugins developers should work with the full array (ie. superset) rather than just a sub-set of the array. // If your plugin needs to know if the array has been modified previously by other plugins use the global variable "hook_return_value" $numeric_key = false; foreach ($GLOBALS['hook_return_value'] as $key => $value) { if (is_numeric($key)) { $numeric_key = true; } else { $numeric_key = false; } break; } if ($numeric_key) { $merged_arrays = array_merge($GLOBALS['hook_return_value'], $function_return_value); $GLOBALS['hook_return_value'] = array_intersect_key($merged_arrays, array_unique(array_column($merged_arrays, 'value'), SORT_REGULAR)); } else { $GLOBALS['hook_return_value'] = array_unique(array_merge_recursive($GLOBALS['hook_return_value'], $function_return_value), SORT_REGULAR); } } elseif (is_string($function_return_value)) { $GLOBALS['hook_return_value'] .= $function_return_value; // appends string } elseif (is_bool($function_return_value)) { $GLOBALS['hook_return_value'] = $GLOBALS['hook_return_value'] || $function_return_value; // boolean OR } } else { $GLOBALS['hook_return_value'] = $function_return_value; $empty_global_return_value = false; } } return isset($GLOBALS['hook_return_value']) ? $GLOBALS['hook_return_value'] : false; } # we have not encountered this hook and page combination before so go add it global $plugins; # this will hold all of the functions to call when hitting this hook name and page combination $function_list = array(); for ($n = 0; $n < count($plugins); $n++) { # "All" hooks $function = isset($plugins[$n]) ? "Hook" . ucfirst((string) $plugins[$n]) . "All" . ucfirst((string) $name) : ""; if (function_exists($function)) { $function_list[] = $function; } else { # Specific hook $function = isset($plugins[$n]) ? "Hook" . ucfirst((string) $plugins[$n]) . ucfirst((string) $pagename) . ucfirst((string) $name) : ""; if (function_exists($function)) { $function_list[] = $function; } } } // Support a global, non-plugin format of hook function that can be defined in config overrides. $function = "GlobalHook" . ucfirst((string) $name); if (function_exists($function)) { $function_list[] = $function; } # add the function list to cache $hook_cache[$hook_cache_index] = $function_list; # do a callback to run the function(s) - this will not cause an infinite loop as we have just added to cache for execution. return hook($name, $pagename, $params, $last_hook_value_wins); } /** * Performs a string replace once a text-only node is encountered, * otherwise loops and calls itself to iterate over child nodes * Not intended to be called directly - use html_find_and_replace * * @param string $findstring * @param string $replacestring can be blank if using for removal * @param DOMNode $node * @return void */ function html_find_and_replace_node(string $findstring, string $replacestring, DOMNode $node): void { if($node->nodeName == '#text') { $node->textContent = str_replace($findstring, $replacestring, $node->textContent); } elseif($node->childNodes->count() > 0) { foreach($node->childNodes as $cn) { html_find_and_replace_node($findstring, $replacestring, $cn); } } } /** * Loads HTML fragment into a DOMDocument instance to parse and * perform a recursive find/replace on text-only nodes. * Returns modified HTML if possible, otherwise the original HTML. * * @param string $findstring * @param string $replacestring can be blank if using for removal * @param string $html * @return string */ function html_find_and_replace(string $findstring, string $replacestring, string $html): string { if (!is_string($html) || 0 === strlen($html)) { return $html; } $html = htmlspecialchars_decode($html); // Return character codes for non-ASCII characters (UTF-8 characters more than a single byte - 0x80 / 128 decimal or greater). // This will prevent them being lost when loaded into libxml. // Second parameter represents convert mappings array - in UTF-8 convert characters of 2,3 and 4 bytes, 0x80 to 0x10FFFF, with no offset and add mask to return character code. $html = mb_encode_numericentity($html, array(0x80, 0x10FFFF, 0, 0xFFFFFF), 'UTF-8'); libxml_use_internal_errors(true); // Load the fragment into DOMDocument object $doc = new DOMDocument(); $doc->encoding = 'UTF-8'; $process_html = false; if($html != strip_tags($html)) { $process_html = $doc->loadHTML($html); } // If DOMDocumment can parse the fragment, attempt to search through and replace if ($process_html) { foreach ($doc->getElementsByTagName('*') as $element) { // Call the recursive find/replace function for each element html_find_and_replace_node($findstring, $replacestring, $element); } $output_html = $doc->saveHTML(); // Remove the extraneous tags added when loading fragment into DOMDocument object if (false !== strpos($output_html, '')) { $body_o_tag_pos = strpos($output_html, ''); $body_c_tag_pos = strpos($output_html, ''); $output_html = substr($output_html, $body_o_tag_pos + 6, $body_c_tag_pos - ($body_o_tag_pos + 6)); } $output_html = html_entity_decode($output_html, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); return $output_html; } else { return $html; } } /** * Utility function to remove unwanted HTML tags and attributes. * Note: if $html is a full page, developers should allow html and body tags. * * @param string $html HTML string * @param array $tags Extra tags to be allowed * @param array $attributes Extra attributes to be allowed * * @return string */ function strip_tags_and_attributes($html, array $tags = array(), array $attributes = array()) { global $permitted_html_tags, $permitted_html_attributes; if (!is_string($html) || 0 === strlen($html)) { return $html; } $html = htmlspecialchars_decode($html); // Return character codes for non-ASCII characters (UTF-8 characters more than a single byte - 0x80 / 128 decimal or greater). // This will prevent them being lost when loaded into libxml. // Second parameter represents convert mappings array - in UTF-8 convert characters of 2,3 and 4 bytes, 0x80 to 0x10FFFF, with no offset and add mask to return character code. $html = mb_encode_numericentity($html, array(0x80, 0x10FFFF, 0, 0xFFFFFF), 'UTF-8'); // Basic way of telling whether we had any tags previously // This allows us to know that the returned value should actually be just text rather than HTML // (DOMDocument::saveHTML() returns a text string as a string wrapped in a
" : ""; $error .= isset($s[$line - 1]) ? "" . trim(escape($s[$line - 1])) . "
" : ""; $error .= isset($s[$line]) ? trim(escape($s[$line])) . "
" : ""; $error .= "
tag) $is_html = ($html != strip_tags($html)); $allowed_tags = array_merge($permitted_html_tags, $tags); $allowed_attributes = array_merge($permitted_html_attributes, $attributes); // Step 1 - Check DOM libxml_use_internal_errors(true); $doc = new DOMDocument(); $doc->encoding = 'UTF-8'; $process_html = $doc->loadHTML($html); if ($process_html) { foreach ($doc->getElementsByTagName('*') as $tag) { if (!in_array($tag->tagName, $allowed_tags)) { $tag->parentNode->removeChild($tag); continue; } if (!$tag->hasAttributes()) { continue; } foreach ($tag->attributes as $attribute) { if (!in_array($attribute->nodeName, $allowed_attributes)) { $tag->removeAttribute($attribute->nodeName); } elseif ( preg_match_all( // Check for dangerous URI (lookalikes) '/[a-zA-Z][a-zA-Z\d+\-.]*:(\/\/)?[^\s]+/im', $attribute->value, $matches, PREG_SET_ORDER ) ) { foreach ($matches as $uri_match) { if (!preg_match('/^(https?):/i', $uri_match[0])) { $tag->removeAttribute($attribute->nodeName); break; } } } } } $html = $doc->saveHTML(); if (false !== strpos($html, '
')) { $body_o_tag_pos = strpos($html, ''); $body_c_tag_pos = strpos($html, ''); $html = substr($html, $body_o_tag_pos + 6, $body_c_tag_pos - ($body_o_tag_pos + 6)); } } // Step 2 - Use regular expressions // Note: this step is required because PHP built-in functions for DOM sometimes don't // pick up certain attributes. I was getting errors of "Not yet implemented." when debugging preg_match_all('/[a-z]+=".+"/iU', $html, $attributes); foreach ($attributes[0] as $attribute) { $attribute_name = stristr($attribute, '=', true); if (!in_array($attribute_name, $allowed_attributes)) { $html = str_replace(' ' . $attribute, '', $html); } } $html = trim($html, "\r\n"); if (!$is_html) { // DOMDocument::saveHTML() returns a text string as a string wrapped in atag $html = strip_tags($html); } $html = html_entity_decode($html, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); return $html; } /** * Remove paragraph tags from start and end of text. * Inner paragraph tags are untouched * * @param string $text HTML string * * @return string Returns the text without surrounding
and
tags. */ function strip_paragraph_tags(string $text): string { // Match eitherexactly, or
at the start of $text // Match
exactly at the end of $text // Everything else between is lazily matched across multiple lines, // so remains untouched return preg_replace('/^]*>(.*?)<\/p>$/s', '$1', $text);
}
/**
* Helper function to quickly return the inner HTML of a specific tag element from a DOM document.
* Example usage:
* get_inner_html_from_tag(strip_tags_and_attributes($unsafe_html), "p");
*
* @param string $txt HTML string
* @param string $tag DOM document tag element (e.g a, div, p)
*
* @return string Returns the inner HTML of the first tag requested and found. Returns empty string if caller code
* requested the wrong tag.
*/
function get_inner_html_from_tag(string $txt, string $tag)
{
//Convert to html before loading into libxml as we will lose non-ASCII characters otherwise
$html = htmlspecialchars_decode(htmlentities($txt, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8', false));
if ($html == strip_tags($txt)) {
return $txt;
}
$inner_html = "";
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->encoding = "UTF-8";
$process_html = $doc->loadHTML($html);
$found_tag_elements = $doc->getElementsByTagName($tag);
if ($process_html && $found_tag_elements->length > 0) {
$found_first_tag_el = $found_tag_elements->item(0);
foreach ($found_first_tag_el->childNodes as $child_node) {
$tmp_doc = new DOMDocument();
$tmp_doc->encoding = "UTF-8";
// Import the node, and all its children, to the temp document and then append it to the doc
$tmp_doc->appendChild($tmp_doc->importNode($child_node, true));
$inner_html .= $tmp_doc->saveHTML();
}
}
$inner_html = html_entity_decode($inner_html, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
return $inner_html;
}
/**
* Returns the page load time until this point.
*
*/
function show_pagetime(): string
{
global $pagetime_start;
$time = microtime();
$time = explode(' ', $time);
$time = $time[1] + $time[0];
$total_time = round(($time - $pagetime_start), 4);
return $total_time . " sec";
}
/**
* Determines where the debug log will live. Typically, same as tmp dir (See general.php: get_temp_dir().
* Since general.php may not be included, we cannot use that method so I have created this one too.
*
* @return string - The path to the debug_log directory.
*/
function get_debug_log_dir()
{
global $tempdir, $storagedir;
// Set up the default.
$result = dirname(__DIR__) . "/filestore/tmp";
// if $tempdir is explicity set, use it.
if (isset($tempdir)) {
// Make sure the dir exists.
if (!is_dir($tempdir)) {
// If it does not exist, create it.
mkdir($tempdir, 0777);
}
$result = $tempdir;
}
// Otherwise, if $storagedir is set, use it.
elseif (isset($storagedir)) {
// Make sure the dir exists.
if (!is_dir($storagedir . "/tmp")) {
// If it does not exist, create it.
mkdir($storagedir . "/tmp", 0777);
}
$result = $storagedir . "/tmp";
} else {
// Make sure the dir exists.
if (!is_dir($result)) {
// If it does not exist, create it.
mkdir($result, 0777);
}
}
// return the result.
return $result;
}
/**
* Output debug information to the debug log, if debugging is enabled.
*
* @param string $text
* @param mixed $resource_log_resource_ref Update the resource log if resource reference passed.
* @param string $resource_log_code If updating the resource log, the code to use
* @return boolean
*/
function debug($text, $resource_log_resource_ref = null, $resource_log_code = LOG_CODE_TRANSFORMED)
{
# Update the resource log if resource reference passed.
if (!is_null($resource_log_resource_ref)) {
resource_log($resource_log_resource_ref, $resource_log_code, '', '', '', $text);
}
# Output some text to a debug file.
# For developers only
global $debug_log, $debug_log_override, $debug_log_location, $debug_extended_info;
if (!$debug_log && !$debug_log_override) {
return true;
} # Do not execute if switched off.
# Cannot use the general.php: get_temp_dir() method here since general may not have been included.
$GLOBALS["use_error_exception"] = true;
try {
if (isset($debug_log_location)) {
$debugdir = dirname($debug_log_location);
if (!is_dir($debugdir)) {
mkdir($debugdir, 0755, true);
}
} else {
$debug_log_location = get_debug_log_dir() . "/debug.txt";
}
if (!file_exists($debug_log_location)) {
$f = fopen($debug_log_location, "a");
if (strpos($debug_log_location, $GLOBALS['storagedir']) !== false) {
// Probably in a browseable location. Set the permissions if we can to prevent browser access (will not work on Windows)
chmod($debug_log_location, 0222);
}
} else {
$f = fopen($debug_log_location, "a");
}
} catch (Exception $e) {
return false;
}
unset($GLOBALS["use_error_exception"]);
$extendedtext = "";
if (isset($debug_extended_info) && $debug_extended_info && function_exists("debug_backtrace")) {
$trace_id = isset($GLOBALS['debug_trace_id']) ? "[traceID {$GLOBALS['debug_trace_id']}]" : '';
$backtrace = debug_backtrace(0);
$btc = count($backtrace);
$callingfunctions = array();
$page = $backtrace[$btc - 1]['file'] ?? pagename();
$debug_line = $backtrace[0]['line'] ?? 0;
for ($n = $btc; $n > 0; $n--) {
if ($page == "" && isset($backtrace[$n]["file"])) {
$page = $backtrace[$n]["file"];
}
if (isset($backtrace[$n]["function"]) && !in_array($backtrace[$n]["function"], array("sql_connect","sql_query","sql_value","sql_array","ps_query","ps_value","ps_array"))) {
if (in_array($backtrace[$n]["function"], array("include","include_once","require","require_once")) && isset($backtrace[$n]["args"][0])) {
$callingfunctions[] = $backtrace[$n]["args"][0];
} else {
$trace_line = isset($backtrace[$n]['line']) ? ":{$backtrace[$n]['line']}" : '';
$callingfunctions[] = $backtrace[$n]["function"] . $trace_line;
}
}
}
$extendedtext .= "{$trace_id}[" . $page . "] "
. (count($callingfunctions) > 0 ? "(" . implode("->", $callingfunctions) . "::{$debug_line}) " : "(::{$debug_line}) ");
}
fwrite($f, date("Y-m-d H:i:s") . " " . $extendedtext . $text . "\n");
fclose($f);
return true;
}
/**
* Recursively removes a directory.
*
* @param string $path Directory path to remove.
* @param array $ignore List of directories to ignore.
*
* @return boolean
*/
function rcRmdir($path, $ignore = array())
{
if (!is_valid_rs_path($path)) {
// Not a valid path to a ResourceSpace file source
return false;
}
debug("rcRmdir: " . $path);
if (is_dir($path)) {
$foldercontents = new DirectoryIterator($path);
foreach ($foldercontents as $object) {
if ($object->isDot() || in_array($path, $ignore)) {
continue;
}
$objectname = $object->getFilename();
if ($object->isDir() && $object->isWritable()) {
$success = rcRmdir($path . DIRECTORY_SEPARATOR . $objectname, $ignore);
} else {
$success = try_unlink($path . DIRECTORY_SEPARATOR . $objectname);
}
if (!$success) {
debug("rcRmdir: Unable to delete " . $path . DIRECTORY_SEPARATOR . $objectname);
return false;
}
}
}
$GLOBALS['use_error_exception'] = true;
try {
$success = rmdir($path);
} catch (Throwable $t) {
$success = false;
debug(sprintf('rcRmdir: failed to remove directory "%s". Reason: %s', $path, $t->getMessage()));
}
unset($GLOBALS['use_error_exception']);
debug("rcRmdir: " . $path . " - " . ($success ? "SUCCESS" : "FAILED"));
return $success;
}
/**
* Update the daily statistics after a loggable event.
*
* The daily_stat table contains a counter for each 'activity type' (i.e. download) for each object (i.e. resource) per day.
*
* @param string $activity_type
* @param integer $object_ref
* @param integer $to_add Optional, how many counts to add, defaults to 1.
* @return void
*/
function daily_stat($activity_type, $object_ref, int $to_add = 1)
{
global $disable_daily_stat;
if ($disable_daily_stat === true) {
return;
} //can be used to speed up heavy scripts when stats are less important
$date = getdate();
$year = $date["year"];
$month = $date["mon"];
$day = $date["mday"];
if ($object_ref == "") {
$object_ref = 0;
}
# Find usergroup
global $usergroup;
if ((!isset($usergroup)) || ($usergroup == "")) {
$usergroup = 0;
}
# External or not?
global $k;
$external = 0;
if (getval("k", "") != "") {
$external = 1;
}
# First check to see if there's a row
$count = ps_value("select count(*) value from daily_stat where year = ? and month = ? and day = ? and usergroup = ? and activity_type = ? and object_ref = ? and external = ?", array("i", $year, "i", $month, "i", $day, "i", $usergroup, "s", $activity_type, "i", $object_ref, "i", $external), 0, "daily_stat"); // Cache this as it can be moderately intensive and is called often.
if ($count == 0) {
# insert
ps_query("insert into daily_stat (year, month, day, usergroup, activity_type, object_ref, external, count) values (? ,? ,? ,? ,? ,? ,? , ?)", array("i", $year, "i", $month, "i", $day, "i", $usergroup, "s", $activity_type, "i", $object_ref, "i", $external, "i", $to_add), false, -1, true, 0);
clear_query_cache("daily_stat"); // Clear the cache to flag there's a row to the query above.
} else {
# update
ps_query("update daily_stat set count = count+? where year = ? and month = ? and day = ? and usergroup = ? and activity_type = ? and object_ref = ? and external = ?", array("i",$to_add,"i", $year, "i", $month, "i", $day, "i", $usergroup, "s", $activity_type, "i", $object_ref, "i", $external), false, -1, true, 0);
}
}
/**
* Returns the current page name minus the extension, e.g. "home" for pages/home.php
*
* @return string
*/
function pagename()
{
$name = safe_file_name(getval('pagename', ''));
if (!empty($name)) {
return $name;
}
$url = str_replace("\\", "/", $_SERVER["PHP_SELF"]); // To work with Windows command line scripts
$urlparts = explode("/", $url);
return $urlparts[count($urlparts) - 1];
}
/**
* Returns the site content from the language strings. These will already be overridden with site_text content if present.
*
* @param string $name
* @return string
*/
function text($name)
{
global $pagename,$lang;
$key = $pagename . "__" . $name;
if (array_key_exists($key, $lang)) {
return $lang[$key];
} elseif (array_key_exists("all__" . $name, $lang)) {
return $lang["all__" . $name];
} elseif (array_key_exists($name, $lang)) {
return $lang[$name];
}
return "";
}
/**
* Gets a list of site text sections, used for a multi-page help area.
*
* @param mixed $page
*/
function get_section_list($page): array
{
global $usergroup;
return ps_array("select distinct name value from site_text where page = ? and name <> 'introtext' and (specific_to_group IS NULL or specific_to_group = ?) order by name", array("s", $page, "i", $usergroup));
}
/**
* Returns a more friendly user agent string based on the passed user agent. Used in the user area to establish browsers used.
*
* @param mixed $agent The user agent string
* @return string
*/
function resolve_user_agent($agent)
{
if ($agent == "") {
return "-";
}
$agent = strtolower($agent);
$bmatches = array( # Note - order is important - first come first matched
"firefox" => "Firefox",
"chrome" => "Chrome",
"opera" => "Opera",
"safari" => "Safari",
"applewebkit" => "Safari",
"msie 3." => "IE3",
"msie 4." => "IE4",
"msie 5.5" => "IE5.5",
"msie 5." => "IE5",
"msie 6." => "IE6",
"msie 7." => "IE7",
"msie 8." => "IE8",
"msie 9." => "IE9",
"msie 10." => "IE10",
"trident/7.0" => "IE11",
"msie" => "IE",
"trident" => "IE",
"netscape" => "Netscape",
"mozilla" => "Mozilla"
#catch all for mozilla references not specified above
);
$osmatches = array(
"iphone" => "iPhone",
"nt 10.0" => "Windows 10",
"nt 6.3" => "Windows 8.1",
"nt 6.2" => "Windows 8",
"nt 6.1" => "Windows 7",
"nt 6.0" => "Vista",
"nt 5.2" => "WS2003",
"nt 5.1" => "XP",
"nt 5.0" => "2000",
"nt 4.0" => "NT4",
"windows 98" => "98",
"linux" => "Linux",
"freebsd" => "FreeBSD",
"os x" => "OS X",
"mac_powerpc" => "Mac",
"sunos" => "Sun",
"psp" => "Sony PSP",
"api" => "Api Client"
);
$b = "???";
$os = "???";
foreach ($bmatches as $key => $value) {
if (!strpos($agent, $key) === false) {
$b = $value;
break;
}
}
foreach ($osmatches as $key => $value) {
if (!strpos($agent, $key) === false) {
$os = $value;
break;
}
}
return $os . " / " . $b;
}
/**
* Returns the current user's IP address, using HTTP proxy headers if present.
*
* @return string
*/
function get_ip()
{
global $ip_forwarded_for;
if (
$ip_forwarded_for
&& isset($_SERVER)
&& array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)
) {
return $_SERVER["HTTP_X_FORWARDED_FOR"];
}
# Returns the IP address for the current user.
if (array_key_exists("REMOTE_ADDR", $_SERVER)) {
return $_SERVER["REMOTE_ADDR"];
}
# Can't find an IP address.
return "???";
}
/**
* For a value such as 10M return the kilobyte equivalent such as 10240. Used by check.php
*
* @param mixed $value
*/
function ResolveKB($value): string
{
$value = trim(strtoupper($value));
if (substr($value, -1, 1) == "K") {
return substr($value, 0, strlen($value) - 1);
}
if (substr($value, -1, 1) == "M") {
return substr($value, 0, strlen($value) - 1) * 1024;
}
if (substr($value, -1, 1) == "G") {
return substr($value, 0, strlen($value) - 1) * 1024 * 1024;
}
return $value;
}
/**
* Trim a filename that is longer than 255 characters while keeping its extension (if present)
*
* @param string s File name to trim
*
* @return string
*/
function trim_filename(string $s)
{
$str_len = mb_strlen($s);
if ($str_len <= 255) {
return $s;
}
$extension = pathinfo($s, PATHINFO_EXTENSION);
if (is_null($extension) || $extension == "") {
return mb_strcut($s, 0, 255);
}
$ext_len = mb_strlen(".{$extension}");
$len = 255 - $ext_len;
$s = mb_strcut($s, 0, $len);
$s .= ".{$extension}";
return $s;
}
/**
* Flip array keys to use one of the keys of the values it contains. All elements (ie values) of the array must contain
* the key (ie. they are arrays). Helper function to greatly increase search performance on huge PHP arrays.
* Normal use is: array_flip_by_value_key($huge_array, 'ref');
*
*
* IMPORTANT: make sure that for the key you intend to use all elements will have a unique value set.
*
* Example: Result after calling array_flip_by_value_key($nodes, 'ref');
* [20382] => Array
* (
* [ref] => 20382
* [name] => Example node
* [parent] => 20381
* )
*
* @param array $a
* @param string $k A values' key to use as an index/key in the main array, ideally an integer
*
* @return array
*/
function array_flip_by_value_key(array $a, string $k)
{
$return = array();
foreach ($a as $val) {
$return[$val[$k]] = $val;
}
return $return;
}
/**
* Reshape array using the keys of its values. All values must contain the selected keys.
*
* @param array $a Array to reshape
* @param string $k The current elements' key to be used as the KEY in the new array. MUST be unique otherwise elements will be lost
* @param string $v The current elements' key to be used as the VALUE in the new array
*
* @return array
*/
function reshape_array_by_value_keys(array $a, string $k, string $v)
{
$return = array();
foreach ($a as $val) {
$return[$val[$k]] = $val[$v];
}
return $return;
}
/**
* Permission check for "j[ref]"
*
* @param integer $ref Featured collection category ref
*
* @return boolean
*/
function permission_j(int $ref)
{
return checkperm("j{$ref}");
}
/**
* Permission check for "-j[ref]"
*
* @param integer $ref Featured collection sub-category ref
*
* @return boolean
*/
function permission_negative_j(int $ref)
{
return checkperm("-j{$ref}");
}
/**
* Delete temporary files
*
* @param array $files array of file paths
* @return void
*/
function cleanup_files($files)
{
// Clean up any temporary files
foreach ($files as $deletefile) {
try_unlink($deletefile);
}
}
/**
* Validate if value is integer or string integer
*
* @param mixed $var - variable to check
* @return boolean true if variable resolves to integer value
*/
function is_int_loose($var)
{
if (is_array($var)) {
return false;
}
return (string)(int)$var === (string)$var;
}
/**
* Helper function to check if value is a positive integer looking type.
*
* @param int|float|string $V Value to be tested
*/
function is_positive_int_loose($V): bool
{
return is_int_loose($V) && $V > 0;
}
/**
* Helper function to check if value is a positive or zero integer looking type.
*
* @param int|float|string $V Value to be tested
*/
function is_positive_or_zero_int_loose($V): bool
{
return is_int_loose($V) && $V >= 0;
}
/**
* Helper function to check if value is an array containing only
* positive or zero integer looking types
*
* @param mixed $var value to be tested
*/
function is_array_of_pos_or_zero_ints($var): bool
{
return is_array($var) && count($var) === count(array_filter($var, 'is_positive_or_zero_int_loose'));
}
/**
* Helper function to check if a value is able to be cast to a string
*
* @param mixed $var value to be tested
*/
function is_string_loose($var): bool
{
return !is_array($var) && $var == (string)$var;
}
/**
* Does the provided $ip match the string $ip_restrict? Used for restricting user access by IP address.
*
* @param string $ip
* @param string $ip_restrict
* @return boolean|integer
*/
function ip_matches($ip, $ip_restrict)
{
global $system_login;
if ($system_login) {
return true;
}
if (substr($ip_restrict, 0, 1) == '!') {
return @preg_match('/' . substr($ip_restrict, 1) . '/su', $ip);
}
# Allow multiple IP addresses to be entered, comma separated.
$i = explode(",", $ip_restrict);
# Loop through all provided ranges
for ($n = 0; $n < count($i); $n++) {
$ip_restrict = trim($i[$n]);
# Match against the IP restriction.
$wildcard = strpos($ip_restrict, "*");
if ($wildcard !== false) {
# Wildcard
if (substr($ip, 0, $wildcard) == substr($ip_restrict, 0, $wildcard)) {
return true;
}
} else {
# No wildcard, straight match
if ($ip == $ip_restrict) {
return true;
}
}
}
return false;
}
/**
* Ensures filename is unique in $filenames array and adds resulting filename to the array
*
* @param string $filename Requested filename to be added. Passed by reference
* @param array $filenames Array of filenames already in use. Passed by reference
* @return string New filename
*/
function set_unique_filename(&$filename, &$filenames)
{
global $lang;
if (in_array($filename, $filenames)) {
$path_parts = pathinfo($filename);
if (isset($path_parts['extension']) && isset($path_parts['filename'])) {
$filename_ext = $path_parts['extension'];
$filename_wo = $path_parts['filename'];
// Run through function to guarantee unique filename
$filename = makeFilenameUnique($filenames, $filename_wo, $lang["_dupe"], $filename_ext);
}
}
$filenames[] = $filename;
return $filename;
}
/**
* Build a specific permission closure which can be applied to a list of items.
*
* @param string $perm Permission string to build (e.g f-, F, T, X, XU)
*
* @return Closure
*/
function build_permission(string $perm)
{
return function ($v) use ($perm) {
return "{$perm}{$v}";
};
}
/**
* Attempt to validate remote code.
*
* IMPORTANT: Never use this function or eval() on any code received externally from a source that can't be trusted!
*
* @param string $code Remote code to validate
*
* @return boolean
*/
function validate_remote_code(string $code)
{
$GLOBALS['use_error_exception'] = true;
try {
extract($GLOBALS, EXTR_SKIP);
eval($code);
} catch (Throwable $t) {
debug('validate_remote_code: Failed to validate remote code. Reason: ' . $t->getMessage());
$invalid = true;
}
unset($GLOBALS['use_error_exception']);
return !isset($invalid);
}
/**
* Get system status information
*
* @param bool $basic Optional, set to true to perform a quick "system up" check only.
* @return array
*/
function get_system_status(bool $basic = false)
{
$return = [
'results' => [
// Example of a test result
// 'name' => [
// 'status' => 'OK/FAIL',
// 'info' => 'Any relevant information',
// 'severity' => 'SEVERITY_CRITICAL/SEVERITY_WARNING/SEVERITY_NOTICE'
// 'severity_text' => Text for severity using language strings e.g. $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL]
// ]
],
'status' => 'FAIL',
];
$fail_tests = 0;
$rs_root = dirname(__DIR__);
// Checking requirements must be done before boot.php. If that's the case always stop after testing for required PHP modules
// otherwise the function will break because of undefined global variables or functions (as expected).
$check_requirements_only = false;
if (!defined('SYSTEM_REQUIRED_PHP_MODULES')) {
include_once $rs_root . '/include/definitions.php';
$check_requirements_only = true;
}
// Check database connectivity.
$check = ps_value('SELECT count(ref) value FROM resource_type', array(), 0);
if ($check <= 0) {
$return['results']['database_connection'] = [
'status' => 'FAIL',
'info' => 'SQL query produced unexpected result',
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
return $return;
}
// End of basic check.
if ($basic) {
// Return early, this is a rapid check of DB connectivity only.
return ['status' => 'OK'];
}
// Check required PHP modules
$missing_modules = [];
foreach (SYSTEM_REQUIRED_PHP_MODULES as $module => $test_fn) {
if (!function_exists($test_fn)) {
$missing_modules[] = $module;
}
}
if (count($missing_modules) > 0) {
$return['results']['required_php_modules'] = [
'status' => 'FAIL',
'info' => 'Missing PHP modules: ' . implode(', ', $missing_modules),
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
// Return now as this is considered fatal to the system. If not, later checks might crash process because of missing one of these modules.
return $return;
} elseif ($check_requirements_only) {
return ['results' => [], 'status' => 'OK'];
}
// Check PHP version is supported
if (PHP_VERSION_ID < PHP_VERSION_SUPPORTED) {
$return['results']['php_version'] = [
'status' => 'FAIL',
'info' => 'PHP version not supported',
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
}
// Check configured utility paths
$missing_utility_paths = [];
foreach (RS_SYSTEM_UTILITIES as $sysu_name => $sysu) {
// Check only required (core to ResourceSpace) and configured utilities
if ($sysu['required'] && isset($GLOBALS[$sysu['path_var_name']]) && get_utility_path($sysu_name) === false) {
$missing_utility_paths[$sysu_name] = $sysu['path_var_name'];
}
}
if (!empty($missing_utility_paths)) {
$return['results']['system_utilities'] = [
'status' => 'FAIL',
'info' => 'Unable to get utility path',
'affected_utilities' => array_unique(array_keys($missing_utility_paths)),
'affected_utility_paths' => array_unique(array_values($missing_utility_paths)),
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
return $return;
}
// Check database encoding.
global $mysql_db;
$badtables = ps_query("SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.tables WHERE TABLE_SCHEMA=? AND `TABLE_COLLATION` NOT LIKE 'utf8%';", array("s",$mysql_db));
if (count($badtables) > 0) {
$return['results']['database_encoding'] = [
'status' => 'FAIL',
'info' => 'Database encoding not utf8. e.g. ' . $badtables[0]["TABLE_NAME"] . ": " . $badtables[0]["TABLE_COLLATION"],
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
}
// Check write access to filestore
if (!is_writable($GLOBALS['storagedir'])) {
$return['results']['filestore_writable'] = [
'status' => 'FAIL',
'info' => '$storagedir is not writeable',
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
return $return;
}
// Check ability to create a file in filestore
$hash = md5(time());
$file = sprintf('%s/write_test_%s.txt', $GLOBALS['storagedir'], $hash);
if (file_put_contents($file, $hash) === false) {
$return['results']['create_file_in_filestore'] = [
'status' => 'FAIL',
'info' => 'Unable to write to configured $storagedir. Folder permissions are: ' . fileperms($GLOBALS['storagedir']),
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
return $return;
}
if (!file_exists($file) || !is_readable($file)) {
$return['results']['filestore_file_exists_and_is_readable'] = [
'status' => 'FAIL',
'info' => 'Hash not saved or unreadable in file ' . $file,
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
return $return;
}
$check = file_get_contents($file);
if (file_exists($file)) {
$GLOBALS['use_error_exception'] = true;
try {
unlink($file);
} catch (Throwable $t) {
$return['results']['filestore_file_delete'] = [
'status' => 'FAIL',
'info' => sprintf('Unable to delete file "%s". Reason: %s', $file, $t->getMessage()),
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
}
$GLOBALS['use_error_exception'] = false;
}
if ($check !== $hash) {
$return['results']['filestore_file_check_hash'] = [
'status' => 'FAIL',
'info' => sprintf('Test write to disk returned a different string ("%s" vs "%s")', $hash, $check),
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
return $return;
}
global $file_integrity_checks;
if ($file_integrity_checks) {
// Check for resources that have failed integrity checks
$exclude_sql = [];
$exclude_params = [];
$exclude_types = array_merge($GLOBALS["file_integrity_ignore_resource_types"], $GLOBALS["data_only_resource_types"]);
if (count($exclude_types) > 0) {
$exclude_sql[] = "resource_type NOT IN (" . ps_param_insert(count($exclude_types)) . ")";
$exclude_params = array_merge($exclude_params, ps_param_fill($exclude_types, "i"));
}
if (count($GLOBALS["file_integrity_ignore_states"]) > 0) {
$exclude_sql[] = "archive NOT IN (" . ps_param_insert(count($GLOBALS["file_integrity_ignore_states"])) . ")";
$exclude_params = array_merge($exclude_params, ps_param_fill($GLOBALS["file_integrity_ignore_states"], "i"));
}
$failedquery = "SELECT COUNT(*) value FROM resource WHERE ref>0 AND integrity_fail=1 AND no_file=0"
. (count($exclude_sql) > 0 ? " AND " . join(" AND ", $exclude_sql) : "");
$failed = ps_value($failedquery, $exclude_params, 0);
if ($failed > 0) {
$return['results']['files_integrity_fail'] = [
'status' => 'FAIL',
'info' => "Files have failed integrity checks",
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
}
// Also check for resources that have not been verified in the last two weeks
$norecentquery = "SELECT COUNT(*) value FROM resource WHERE ref>0 AND no_file=0 "
. (count($exclude_sql) > 0 ? " AND " . join(" AND ", $exclude_sql) : "")
. "AND DATEDIFF(NOW(),last_verified) > 14";
$notchecked = ps_value($norecentquery, $exclude_params, 0);
if ($notchecked > 0) {
$return['results']['recent_file_verification'] = [
'status' => 'FAIL',
'info' => $notchecked . " resources have not had recent file integrity checks",
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
}
}
// Check filestore folder browseability
$cfb = check_filestore_browseability();
if (!$cfb['index_disabled']) {
$return['results']['filestore_indexed'] = [
'status' => 'FAIL',
'info' => $cfb['info'],
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
return $return;
}
// Check write access to sql_log
if (isset($GLOBALS['mysql_log_transactions']) && $GLOBALS['mysql_log_transactions']) {
$mysql_log_location = $GLOBALS['mysql_log_location'] ?? '';
$mysql_log_dir = dirname($mysql_log_location);
if (!is_writeable($mysql_log_dir) || (file_exists($mysql_log_location) && !is_writeable($mysql_log_location))) {
$return['results']['mysql_log_location'] = [
'status' => 'FAIL',
'info' => 'Invalid $mysql_log_location specified in config file',
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
return $return;
}
}
// Check write access to debug_log
$debug_log_location = $GLOBALS['debug_log_location'] ?? get_debug_log_dir() . '/debug.txt';
$debug_log_dir = dirname($debug_log_location);
if (!is_writeable($debug_log_dir) || (file_exists($debug_log_location) && !is_writeable($debug_log_location))) {
$debug_log = isset($GLOBALS['debug_log']) && $GLOBALS['debug_log'];
$return['results']['debug_log_location'] = [
'status' => 'FAIL',
'info' => 'Invalid $debug_log_location specified in config file',
];
if ($debug_log) {
$return['results']['debug_log_location']['severity'] = SEVERITY_CRITICAL;
$return['results']['debug_log_location']['severity_text'] = $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL];
return $return;
} else {
++$fail_tests;
}
}
// Check that the cron process executed within the last day (FAIL)
$last_cron = strtotime(get_sysvar('last_cron', ''));
$diff_days = (time() - $last_cron) / (60 * 60 * 24);
if ($diff_days > 1.5) {
$return['results']['cron_process'] = [
'status' => 'FAIL',
'info' => 'Cron was executed ' . round($diff_days, 1) . ' days ago.',
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
}
// Check free disk space is sufficient - WARN
$avail = disk_total_space($GLOBALS['storagedir']);
$free = disk_free_space($GLOBALS['storagedir']);
$calc = $free / $avail;
if ($calc < 0.01) {
$return['results']['free_disk_space'] = [
'status' => 'FAIL',
'info' => 'Less than 1% disk space free.',
'severity' => SEVERITY_CRITICAL,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_CRITICAL],
];
return $return;
} elseif ($calc < 0.05) {
$return['results']['free_disk_space'] = [
'status' => 'FAIL',
'info' => 'Less than 5% disk space free.',
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
}
// Check the disk space against the quota limit - WARN (FAIL if exceeded)
if (isset($GLOBALS['disksize'])) {
$avail = $GLOBALS['disksize'] * (1000 * 1000 * 1000); # Get quota in bytes
$used = get_total_disk_usage(); # Total usage in bytes
$percent = ceil(((int) $used / $avail) * 100);
if ($percent >= 95) {
$return['results']['quota_limit'] = [
'status' => 'FAIL',
'info' => $percent . '% used',
'avail' => $avail, 'used' => $used, 'percent' => $percent,
'severity' => SEVERITY_WARNING,
'severity_text' => $GLOBALS["lang"]["severity-level_" . SEVERITY_WARNING],
];
++$fail_tests;
} else {
$return['results']['quota_limit'] = [
'status' => 'OK',
'info' => $percent . '% used.',
'avail' => $avail, 'used' => $used, 'percent' => $percent
];
}
}
// Return the version number
$return['results']['version'] = [
'status' => 'OK',
'info' => $GLOBALS['productversion'],
];
// Return the SVN information, if possible
$svn_data = '';
// - If a SVN branch, add on the branch name.
$svninfo = run_command('svn info ' . $rs_root);
$matches = [];
if (preg_match('/\nURL: .+\/branches\/(.+)\\n/', $svninfo, $matches) != 0) {
$svn_data .= ' BRANCH ' . $matches[1];
}
// - Add on the SVN revision if we can find it.
// If 'svnversion' is available, run this as it will produce a better output with 'M' signifying local modifications.
$matches = [];
$svnversion = run_command('svnversion ' . $rs_root);
if ($svnversion != '') {
# 'svnversion' worked - use this value and also flag local mods using a detectable string.
$svn_data .= ' r' . str_replace('M', '(mods)', $svnversion);
} elseif (preg_match('/\nRevision: (\d+)/i', $svninfo, $matches) != 0) {
// No 'svnversion' command, but we found the revision in the results from 'svn info'.
$svn_data .= ' r' . $matches[1];
}
if ($svn_data !== '') {
$return['results']['svn'] = [
'status' => 'OK',
'info' => $svn_data];
}
// Return a list with names of active plugins
$return['results']['plugins'] = [
'status' => 'OK',
'info' => implode(', ', array_column(get_active_plugins(), 'name')),
];
// Return active user count (last 7 days)
$return['results']['recent_user_count'] = [
'status' => 'OK',
'info' => get_recent_users(7),
'within_year' => get_recent_users(365),
'total_approved' => get_total_approved_users()
];
// Return current number of resources including count of
// non-ingested resources (staticsync)
$return['results']['resource_count'] = [
'status' => 'OK',
'total' => get_total_resources(),
'active' => get_total_resources(0),
'non_ingested' => get_non_ingested_resources(),
];
// Return last API access
$return['results']['last_api_access'] = [
'status' => 'OK',
'info' => get_sysvar("last_api_access")
];
// Return bandwidth usage last 30 days
$return['results']['download_bandwidth_last_30_days_gb'] = [
'status' => 'OK',
'total' => round(ps_value("select sum(`count`) value from daily_stat where
activity_type='Downloaded KB'
and (`year`=year(now()) or (month(now())=1 and `year`=year(now())-1))
and (`month`=month(now()) or `month`=month(now())-1 or (month(now())=1 and `month`=12))
and datediff(now(), concat(`year`,'-',lpad(`month`,2,'0'),'-',lpad(`day`,2,'0')))<=30
", [], 0) / (1024 * 1024), 3) // Note - limit to this month and last month before the concat to get the exact period; ensures not performing the concat on a large set of data.
];
// Return file extensions with counts
$return['results']['files_by_extension'] = [
'status' => 'OK',
'total' => ps_query("select file_extension,count(*) `count`,round(sum(disk_usage)/power(1024,3),2) disk_usage_gb from resource where length(file_extension)>0 group by file_extension order by `count` desc;", [])
];
// Check if plugins have any warnings
$extra_checks = hook('extra_checks');
if ($extra_checks !== false && is_array($extra_checks)) {
foreach ($extra_checks as $check_name => $extra_check) {
$return['results'][$check_name] = [
'status' => $extra_check['status'],
'info' => $extra_check['info'],
];
if (isset($extra_check['severity'])) {
// Severity is optional and may not be returned by some plugins
$return['results'][$check_name]['severity'] = $extra_check['severity'];
$return['results'][$check_name]['severity_text'] = $GLOBALS["lang"]["severity-level_" . $extra_check['severity']];
}
$warn_details = $extra_check['details'] ?? [];
if ($warn_details !== []) {
$return['results'][$check_name]['details'] = $warn_details;
}
if ($extra_check['status'] == 'FAIL') {
++$fail_tests;
}
}
}
if ($fail_tests > 0) {
$return['status'] = 'FAIL';
} else {
$return['status'] = 'OK';
}
return $return;
}
/**
* Try and delete a file without triggering a fatal error
*
* @param string $deletefile Full path to file
* @return bool|string Returns TRUE on success or a string containing error
*/
function try_unlink($deletefile)
{
$GLOBALS["use_error_exception"] = true;
try {
$deleted = unlink($deletefile);
} catch (Throwable $t) {
$message = "Unable to delete : " . $deletefile . ". Reason" . $t->getMessage();
debug($message);
return $message;
}
unset($GLOBALS["use_error_exception"]);
return $deleted;
}
function try_getimagesize(string $filename, &$image_info = null)
{
$GLOBALS["use_error_exception"] = true;
try {
$return = getimagesize($filename, $image_info);
} catch (Throwable $e) {
$return = false;
}
unset($GLOBALS["use_error_exception"]);
return $return;
}
/**
* Check filestore folder browseability.
* For security reasons (e.g data breach) the filestore location shouldn't be indexed by the web server (in Apache2 - disable autoindex module)
*
* @return array Returns data structure with following keys:-
* - status: An end user status of OK/FAIL
* - info: Any extra relevant information (aimed at end users)
* - filestore_url: ResourceSpace URL to the filestore location
* - index_disabled: PHP bool (used by code). FALSE if web server allows indexing/browsing the filestore, TRUE otherwise
*/
function check_filestore_browseability()
{
$filestore_url = $GLOBALS['storageurl'] ?? "{$GLOBALS['baseurl']}/filestore";
$timeout = 5;
$return = [
'status' => $GLOBALS['lang']['status-fail'],
'info' => $GLOBALS['lang']['noblockedbrowsingoffilestore'],
'filestore_url' => $filestore_url,
'index_disabled' => false,
];
$GLOBALS['use_error_exception'] = true;
try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $filestore_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 2);
curl_exec($ch);
$response_status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
} catch (Throwable $t) {
$return['status'] = $GLOBALS['lang']['unknown'];
$return['info'] = $GLOBALS['show_error_messages'] && $GLOBALS['show_detailed_errors'] ? $t->getMessage() : '';
return $return;
}
unset($GLOBALS['use_error_exception']);
// Web servers (RFC 2616) shouldn't return a "200 OK" if the server has indexes disabled. Usually it's "404 Not Found".
if ($response_status_code !== 200) {
$return['status'] = $GLOBALS['lang']['status-ok'];
$return['info'] = '';
$return['index_disabled'] = true;
}
return $return;
}
/**
* Check CLI version found for ImageMagick is as expected.
*
* @param string $version_output The version output for ImageMagick
* @param array $utility Utility structure. {@see RS_SYSTEM_UTILITIES}
*
* @return array Returns array as expected by the check.php page
* - utility - New utility value for its display name
* - found - PHP bool representing whether we've found what we were expecting in the version output.
*/
function check_imagemagick_cli_version_found(string $version_output, array $utility)
{
$expected = ['ImageMagick', 'GraphicsMagick'];
foreach ($expected as $utility_name) {
if (mb_strpos($version_output, $utility_name) !== false) {
$utility['display_name'] = $utility_name;
}
}
return [
'utility' => $utility,
'found' => in_array($utility['display_name'], $expected),
];
}
/**
* Check CLI version found for Exiftool is as expected.
*
* @param string $version_output The version output for ImageMagick
* @param array $utility Utility structure. {@see RS_SYSTEM_UTILITIES}
*
* @return array Returns array as expected by the check.php page
* - utility - New utility value for its display name
* - found - PHP bool representing whether we've found what we were expecting in the version output.
* - error_message - optional error message if an issue is detected
*/
function check_exiftool_cli_version_found(string $version_output, array $utility): array
{
global $lang;
if (preg_match('/Warning: Library/', $version_output) === 1) {
return [
'utility' => $utility,
'found' => false,
'error_message' => "{$lang['status-warning']}: {$lang['exiftoolconflictingversions']}
{$version_output}",
];
} else {
return [
'utility' => $utility,
'found' => preg_match("/^([0-9]+)+\.([0-9]+)/", $version_output) === 1,
];
}
}
/**
* Check CLI numeric version found for a utility is as expected.
*
* @param string $version_output The version output
* @param array $utility Utility structure. {@see RS_SYSTEM_UTILITIES}
*
* @return array Returns array as expected by the check.php page
* - utility - not used
* - found - PHP bool representing whether we've found what we were expecting in the version output.
*/
function check_numeric_cli_version_found(string $version_output, array $utility)
{
return [
'utility' => $utility,
'found' => preg_match("/^([0-9]+)+\.([0-9]+)/", $version_output) === 1,
];
}
/**
* Check CLI version found for a utility is as expected by looking up for its name.
*
* @param string $version_output The version output for the utility
* @param array $utility Utility structure. {@see RS_SYSTEM_UTILITIES}
*
* @return array Returns array as expected by the check.php page
* - utility - not used
* - found - PHP bool representing whether we've found what we were expecting in the version output.
*/
function check_utility_cli_version_found_by_name(string $version_output, array $utility, array $lookup_names)
{
$version_output = strtolower($version_output);
$lookup_names = array_filter($lookup_names);
foreach ($lookup_names as $utility_name) {
if (mb_strpos($version_output, strtolower($utility_name)) !== false) {
$found = true;
break;
}
}
return [
'utility' => $utility,
'found' => isset($found),
];
}
/**
* Check we're running on the command line, exit otherwise. Security feature for the scripts in /pages/tools/
*
*
* @return void
*/
function command_line_only()
{
if ('cli' != PHP_SAPI) {
http_response_code(401);
exit('Access denied - Command line only!');
}
}
/**
* Helper function to quickly build a list of values, all prefixed the same way.
*
* ```php
* $fieldXs = array_map(prefix_value('field'), [3, 88]);
* ```
*
* @param string $prefix Prefix value to prepend.
*/
function prefix_value(string $prefix): Closure
{
return static fn (string $value): string => $prefix . $value;
}
/**
* Utility function to check string is a valid date/time with a specific format.
*
* @param string $datetime Date/time value
* @param string $format The format that date/time value should be in. {@see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php}
* @return boolean
*/
function validateDatetime(string $datetime, string $format = 'Y-m-d H:i:s'): bool
{
$date = DateTimeImmutable::createFromFormat($format, $datetime);
return $date && $date->format($format) === $datetime;
}
/**
* @param string $haystack Value to be checked
* @param string $needle Substing to seach for in the haystack
*
* @return bool True if the haystack ends with the needle otherwise false
*/
function string_ends_with($haystack, $needle)
{
return substr($haystack, strlen($haystack) - strlen($needle), strlen($needle)) === $needle;
}
/**
* Helper function to set the order_by key of an array to zero.
*
* @param array item Item for which we need to set the order_by
* @return array Same item with the order_by key zero.
*/
function set_order_by_to_zero(array $item): array
{
$item['order_by'] = 0;
return $item;
}
/**
* Helper function to cast functions that only echo things out (e.g render functions) to string type.
*
* @param callable $fn Function to cast
* @param array $args Provide function's arguments (if applicable)
*/
function cast_echo_to_string(callable $fn, array $args = []): string
{
ob_start();
$fn(...$args);
$result = ob_get_contents();
ob_end_clean();
return $result;
}
/**
* Helper function to parse input to a list of a particular type.
*
* @example include/api_bindings.php Used in api_get_resource_type_fields() or api_create_resource_type_field()
*
* @param string $csv CSV of raw data
* @param callable(string) $type Function checking each CSV item, as required by your context, to determine if it should
* be allowed in the result set
*/
function parse_csv_to_list_of_type(string $csv, callable $type): array
{
$list = explode(',', $csv);
$return = [];
foreach ($list as $value) {
$value = trim($value);
if ($type($value)) {
$return[] = $value;
}
}
return $return;
}
/**
* Remove metadata field properties during execution lockout
*
* @param array $rtf Resource type field data structure
* @return array Returns without the relevant properties if execution lockout is enabled
*/
function execution_lockout_remove_resource_type_field_props(array $rtf): array
{
$props = [
'autocomplete_macro',
'value_filter',
'exiftool_filter',
'onchange_macro',
];
return $GLOBALS['execution_lockout'] ? array_diff_key($rtf, array_flip($props)) : $rtf;
}
/**
* Update global variable watermark to point to the correct file. Watermark set on System Configuration page will override a watermark
* set in config.php. config.default.php will apply otherwise (blank) so no watermark will be applied.
*
* @return void
*/
function set_watermark_image()
{
global $watermark, $storagedir;
$wm = (string) $watermark;
if (trim($wm) !== '' && substr($wm, 0, 13) == '[storage_url]') {
$GLOBALS["watermark"] = str_replace('[storage_url]', $storagedir, $watermark); # Watermark from system configuration page
} elseif (trim($wm) !== '') {
$GLOBALS["watermark"] = __DIR__ . "/../" . $watermark; # Watermark from config.php - typically "gfx/watermark.png"
}
}
/** DPI calculations */
function compute_dpi($width, $height, &$dpi, &$dpi_unit, &$dpi_w, &$dpi_h)
{
global $lang, $imperial_measurements,$sizes,$n;
if (isset($sizes[$n]['resolution']) && $sizes[$n]['resolution'] != 0 && is_int($sizes[$n]['resolution'])) {
$dpi = $sizes[$n]['resolution'];
} elseif (!isset($dpi) || $dpi == 0) {
$dpi = 300;
}
if ((isset($sizes[$n]['unit']) && trim(strtolower($sizes[$n]['unit'])) == "inches") || $imperial_measurements) {
# Imperial measurements
$dpi_unit = $lang["inch-short"];
$dpi_w = round($width / $dpi, 1);
$dpi_h = round($height / $dpi, 1);
} else {
$dpi_unit = $lang["centimetre-short"];
$dpi_w = round(($width / $dpi) * 2.54, 1);
$dpi_h = round(($height / $dpi) * 2.54, 1);
}
}
/** MP calculation */
function compute_megapixel(int $width, int $height): float
{
return round(($width * $height) / 1000000, 2);
}
/**
* Get size info as a paragraphs HTML tag
* @param array $size Preview size information
* @param array|null $originalSize Original preview size information
*/
function get_size_info(array $size, ?array $originalSize = null): string
{
global $lang, $ffmpeg_supported_extensions;
$newWidth = intval($size['width']);
$newHeight = intval($size['height']);
if ($originalSize != null && $size !== $originalSize) {
// Compute actual pixel size
$imageWidth = $originalSize['width'];
$imageHeight = $originalSize['height'];
if ($imageWidth > $imageHeight) {
// landscape
if ($imageWidth == 0) {
return '
–
'; } $newWidth = $size['width']; $newHeight = round(($imageHeight * $newWidth + $imageWidth - 1) / $imageWidth); } else { // portrait or square if ($imageHeight == 0) { return '–
'; } $newHeight = $size['height']; $newWidth = round(($imageWidth * $newHeight + $imageHeight - 1) / $imageHeight); } } $output = sprintf( '%s × %s %s', escape($newWidth), escape($newHeight), escape($lang['pixels']), ); $mp = compute_megapixel($newWidth, $newHeight); if ($mp >= 0) { $output .= sprintf( ' (%s %s)', escape($mp), escape($lang['megapixel-short']), ); } $output .= '
'; if ( !isset($size['extension']) || !in_array(strtolower($size['extension']), $ffmpeg_supported_extensions) ) { # Do DPI calculation only for non-videos compute_dpi($newWidth, $newHeight, $dpi, $dpi_unit, $dpi_w, $dpi_h); $output .= sprintf( '%1$s %2$s × %3$s %2$s %4$s %5$s %6$s
', escape($dpi_w), escape($dpi_unit), escape($dpi_h), escape($lang['at-resolution']), escape($dpi), escape($lang['ppi']), ); } if (isset($size["filesize"])) { $output .= sprintf('%s
', strip_tags_and_attributes($size["filesize"])); } return $output; } /** * Simple function to check if a given extension is associated with a JPG file * * @param string $extension File extension */ function is_jpeg_extension(string $extension): bool { return in_array(mb_strtolower($extension), ["jpg","jpeg"]); } /** * Input validation helper function to check a URL is ours (e.g. if it's our base URL). Mostly used for redirect URLs. * * @param string $base The value the URL is expected to start with. Due to the structure of a URL, you can also check * for (partial) paths. * @param mixed $val URL to check */ function url_starts_with(string $base, $val): bool { return is_string($val) && filter_var($val, FILTER_VALIDATE_URL) && mb_strpos($val, $base) === 0; } /** * Input validation helper function to check if a URL is safe (from XSS). Mostly intended for redirect URLs. * @param mixed $val URL to check */ function is_safe_url($url): bool { if (!(is_string($url) && filter_var($url, FILTER_VALIDATE_URL))) { return false; } $url_parts = parse_url($url); if ($url_parts === false) { return false; } elseif (!in_array($url_parts['scheme'], ['http', 'https'])) { return false; } // Check URL components (except the port and query strings) don't contain XSS payloads foreach (array_diff_key($url_parts, ['port' => 1, 'query' => 1]) as $value) { if ($value !== escape($value)) { return false; } } // Check query strings, if applicable $qs_params = []; parse_str($url_parts['query'] ?? '', $qs_params); foreach ($qs_params as $param => $value) { if ($param !== escape($param) || $value !== escape($value)) { debug("[WARN] Suspicious query string parameter ({$param} with value: {$value}) found in URL - {$url}"); return false; } } return true; } /** * Input validation helper function for sorting (ASC/DESC). * @param mixed $val User input value to be validated */ function validate_sort_value($val): bool { return is_string($val) && in_array(mb_strtolower($val), ['asc', 'desc']); } /** * Input validation helper function for a CSV of integers (mostly used for IDs). * @param mixed $val User input value to be validated */ function validate_digit_csv($val): bool { return is_string($val) && preg_match('/^\d+,? ?(, ?\d+ ?,? ?)*$/', $val) === 1; } /** * Helper function to get an array of values with a subset of their original keys. * * @param list