5683 lines
189 KiB
PHP
Executable File
5683 lines
189 KiB
PHP
Executable File
<?php
|
|
|
|
#
|
|
#
|
|
# General functions, useful across the whole solution; not specific to one area
|
|
#
|
|
# PLEASE NOTE - Don't add search/resource/collection/user etc. functions here - use the separate include files.
|
|
#
|
|
|
|
use Montala\ResourceSpace\CommandPlaceholderArg;
|
|
|
|
/**
|
|
* Retrieve a user-submitted parameter from the browser via post/get/cookies, in that order.
|
|
*
|
|
* @param string $param The parameter name
|
|
* @param string $default A default value to return if no matching parameter was found
|
|
* @param boolean $force_numeric Ensure a number is returned. (DEPRECATED)
|
|
* @param callable $type_check Validate param type. Default is to check param values are strings.
|
|
*/
|
|
function getval($param, $default, $force_numeric = false, ?callable $type_check = null)
|
|
{
|
|
/*
|
|
TODO: remove in favour of type_check. Example:
|
|
- getval('some_param_name', 0, true);
|
|
+ getval('some_param_name', 0, 'is_numeric'); # Once $force_numeric arg is removed
|
|
|
|
For now $force_numeric has a higher precedence while still in place.
|
|
*/
|
|
if ($force_numeric) {
|
|
$type_check = 'is_numeric';
|
|
} elseif ($type_check === null) {
|
|
$type_check = 'is_string';
|
|
}
|
|
|
|
if (array_key_exists($param, $_POST)) {
|
|
return $type_check($_POST[$param]) ? $_POST[$param] : $default;
|
|
} elseif (array_key_exists($param, $_GET)) {
|
|
return $type_check($_GET[$param]) ? $_GET[$param] : $default;
|
|
} elseif (array_key_exists($param, $_COOKIE)) {
|
|
return $type_check($_COOKIE[$param]) ? $_COOKIE[$param] : $default;
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape a value prior to using it in SQL.
|
|
* IMPORTANT! NO LONGER NEEDED with prepared statements. This is only used when exporting SQL scripts.
|
|
*
|
|
* @param string $text
|
|
* @return string
|
|
*/
|
|
function escape_check($text)
|
|
{
|
|
global $db;
|
|
|
|
$db_connection = $db["read_write"];
|
|
if (db_use_multiple_connection_modes() && db_get_connection_mode() == "read_only") {
|
|
$db_connection = $db["read_only"];
|
|
db_clear_connection_mode();
|
|
}
|
|
|
|
$text = mysqli_real_escape_string($db_connection, (string) $text);
|
|
|
|
# turn all \\' into \'
|
|
while (strpos($text, "\\\\'") !== false) {
|
|
$text = str_replace("\\\\'", "\\'", $text);
|
|
}
|
|
|
|
# Remove any backslashes that are not being used to escape single quotes.
|
|
$text = str_replace("\\'", "{bs}'", $text);
|
|
$text = str_replace("\\n", "{bs}n", $text);
|
|
$text = str_replace("\\r", "{bs}r", $text);
|
|
$text = str_replace("\\", "", $text);
|
|
$text = str_replace("{bs}'", "\\'", $text);
|
|
$text = str_replace("{bs}n", "\\n", $text);
|
|
$text = str_replace("{bs}r", "\\r", $text);
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* For comparing escape_checked strings against mysql content because
|
|
* just doing $text=str_replace("\\","",$text); does not undo escape_check
|
|
*
|
|
* @param mixed $text
|
|
* @return string
|
|
*/
|
|
function unescape($text)
|
|
{
|
|
# Remove any backslashes that are not being used to escape single quotes.
|
|
$text = str_replace("\\'", "\'", $text);
|
|
$text = str_replace("\\n", "\n", $text);
|
|
$text = str_replace("\\r", "\r", $text);
|
|
$text = str_replace("\\", "", $text);
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Formats a MySQL ISO date
|
|
*
|
|
* Always use the 'wordy' style from now on as this works better internationally.
|
|
*
|
|
* @uses offset_user_local_timezone()
|
|
*
|
|
* @var string $date ISO format date which can be a BCE date (ie. with negative year -yyyy)
|
|
* @var boolean $time When TRUE and full date is present then append the hh:mm time part if present
|
|
* @var boolean $wordy When TRUE return month name, otherwise return month number
|
|
* @var boolean $offset_tz Set to TRUE to offset based on time zone, FALSE otherwise
|
|
*
|
|
* @return string Returns an empty string if date not set/invalid
|
|
*/
|
|
function nicedate($date, $time = false, $wordy = true, $offset_tz = false)
|
|
{
|
|
global $lang, $date_d_m_y, $date_yyyy;
|
|
|
|
$date = trim((string)$date);
|
|
if ($date == '') {
|
|
return '';
|
|
}
|
|
|
|
// Pad out a date time value so that it can pass through strtotime if time part is incomplete
|
|
if ($time && strlen($date) < 16) {
|
|
$date_parts = explode(' ', $date);
|
|
$time_parts = explode(':', $date_parts[1] ?? '');
|
|
$time_parts = [
|
|
$time_parts[0] ?? '' ?: '00',
|
|
$time_parts[1] ?? '' ?: '00'
|
|
];
|
|
$date = $date_parts[0] . ' ' . implode(':', $time_parts);
|
|
}
|
|
|
|
$date_timestamp = strtotime($date);
|
|
if ($date_timestamp === false) {
|
|
return '';
|
|
}
|
|
|
|
// Check whether unix timestamp is a BCE date
|
|
$year_zero = PHP_INT_MIN === (int)-2147483648 ? PHP_INT_MIN : strtotime("0000-00-00");
|
|
$bce_offset = ($date_timestamp < $year_zero) ? 1 : 0;
|
|
// BCE dates cannot return year in truncated form
|
|
if ($bce_offset == 1 && !$date_yyyy) {
|
|
return '';
|
|
}
|
|
|
|
$original_time_part = substr($date, $bce_offset + 11, 5);
|
|
if ($offset_tz && ($original_time_part !== false || $original_time_part != '')) {
|
|
$date = offset_user_local_timezone($date, 'Y-m-d H:i');
|
|
}
|
|
|
|
$y = substr($date, 0, $bce_offset + 4);
|
|
if (!$date_yyyy) {
|
|
$y = substr($y, 2, 2); // Only truncate year for non-BCE dates
|
|
}
|
|
|
|
if ($y == "") {
|
|
return "-";
|
|
}
|
|
|
|
$month_part = substr($date, $bce_offset + 5, 2);
|
|
if (!is_numeric($month_part)) {
|
|
return $y;
|
|
}
|
|
$m = $wordy ? ($lang["months"][$month_part - 1] ?? "") : $month_part;
|
|
if ($m == "") {
|
|
return $y;
|
|
}
|
|
|
|
$d = substr($date, $bce_offset + 8, 2);
|
|
if ($d == "" || $d == "00") {
|
|
return "{$m} {$y}";
|
|
}
|
|
|
|
$t = $time ? " @ " . substr($date, $bce_offset + 11, 5) : "";
|
|
|
|
if ($date_d_m_y) {
|
|
return $d . " " . $m . " " . $y . $t;
|
|
} else {
|
|
return $m . " " . $d . " " . $y . $t;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redirect to the provided URL using a HTTP header Location directive. Exits after redirect
|
|
*
|
|
* @param string $url URL to redirect to
|
|
* @return never
|
|
*/
|
|
function redirect(string $url)
|
|
{
|
|
global $baseurl,$baseurl_short;
|
|
|
|
// Header may not contain NUL bytes
|
|
$url = str_replace("\0", '', $url);
|
|
|
|
if (getval("ajax", "") != "") {
|
|
# When redirecting from an AJAX loaded page, forward the AJAX parameter automatically so headers and footers are removed.
|
|
if (strpos($url, "?") !== false) {
|
|
$url .= "&ajax=true";
|
|
} else {
|
|
$url .= "?ajax=true";
|
|
}
|
|
}
|
|
|
|
if (substr($url, 0, 1) == "/") {
|
|
# redirect to an absolute URL
|
|
header("Location: " . $baseurl . str_replace($baseurl_short, "/", $url));
|
|
} else {
|
|
if (url_starts_with($baseurl, $url)) {
|
|
// Base url has already been added
|
|
header("Location: " . $url);
|
|
exit();
|
|
}
|
|
|
|
# redirect to a relative URL
|
|
header("Location: " . $baseurl . "/" . $url);
|
|
}
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* replace multiple spaces with a single space
|
|
*
|
|
* @param mixed $text
|
|
* @return string
|
|
*/
|
|
function trim_spaces($text)
|
|
{
|
|
while (strpos($text, " ") !== false) {
|
|
$text = str_replace(" ", " ", $text);
|
|
}
|
|
return trim($text);
|
|
}
|
|
|
|
/**
|
|
* Removes whitespace from the beginning/end of all elements in an array
|
|
*
|
|
* @param array $array
|
|
* @param string $trimchars
|
|
* @return array
|
|
*/
|
|
function trim_array($array, $trimchars = '')
|
|
{
|
|
if (isset($array[0]) && empty($array[0]) && !(emptyiszero($array[0]))) {
|
|
$unshiftblank = true;
|
|
}
|
|
$array = array_filter($array, 'emptyiszero');
|
|
$array_trimmed = array();
|
|
$index = 0;
|
|
|
|
foreach ($array as $el) {
|
|
$el = trim($el);
|
|
if (strlen($trimchars) > 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<string>
|
|
*/
|
|
function get_mime_types_by_extension(string $extension): array
|
|
{
|
|
$extension = mb_strtolower(trim($extension));
|
|
$mime_types = $GLOBALS['mime_types_by_extension'] ?? [];
|
|
$matches = isset($mime_types[$extension])
|
|
? (is_string($mime_types[$extension]) ? [$mime_types[$extension]] : $mime_types[$extension])
|
|
: [];
|
|
|
|
return array_values(array_filter(array_map(trim(...), $matches)));
|
|
}
|
|
|
|
/**
|
|
* Convert the permitted resource type extension to MIME type. Used by upload_batch.php
|
|
*
|
|
* @param string $extension File extension
|
|
* @return string MIME type e.g. image/jpeg
|
|
*/
|
|
function allowed_type_mime($allowedtype)
|
|
{
|
|
if (strpos($allowedtype, "/") === false) {
|
|
// Get extended list of mime types to convert legacy extensions to Uppy mime type syntax.
|
|
$found_types = get_mime_types_by_extension($allowedtype);
|
|
/** {@see include/mime_types.php} */
|
|
return $found_types === [] ? $allowedtype : reset($found_types);
|
|
} else {
|
|
// already a mime type
|
|
return $allowedtype;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a mail - but correctly encode the message/subject in quoted-printable UTF-8.
|
|
*
|
|
* NOTE: $from is the name of the user sending the email,
|
|
* while $from_name is the name that should be put in the header, which can be the system name
|
|
* It is necessary to specify two since in all cases the email should be able to contain the user's name.
|
|
*
|
|
* Old mail function remains the same to avoid possible issues with phpmailer
|
|
* send_mail_phpmailer allows for the use of text and html (multipart) emails,
|
|
* and the use of email templates in Manage Content.
|
|
*
|
|
* @param string $email Email address to send to
|
|
* @param string $subject Email subject
|
|
* @param string $message Message text
|
|
* @param string $from From address - defaults to $email_from
|
|
* @param string $reply_to Reply to address - defaults to $email_from
|
|
* @param string $html_template Optional template (this is a $lang entry with placeholders)
|
|
* @param array $templatevars Used to populate email template placeholders
|
|
* @param string $from_name Email from name
|
|
* @param string $cc Optional CC addresses
|
|
* @param string $bcc Optional BCC addresses
|
|
* @param array $files Optional array of file paths to attach in the format [filename.txt => /path/to/file.txt]
|
|
*/
|
|
function send_mail($email, $subject, $message, $from = "", $reply_to = "", $html_template = "", $templatevars = array(), $from_name = "", $cc = "", $bcc = "", $files = array())
|
|
{
|
|
global $applicationname, $use_phpmailer, $email_from, $email_notify, $always_email_copy_admin, $baseurl, $userfullname;
|
|
global $email_footer, $header_colour_style_override, $userref, $email_rate_limit, $lang, $useremail_rate_limit_active;
|
|
|
|
if (defined("RS_TEST_MODE")) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Checking email is valid. Email argument can be an RFC 2822 compliant string so handle multi addresses as well
|
|
IMPORTANT: FILTER_VALIDATE_EMAIL is not fully RFC 2822 compliant, an email like "Another User <anotheruser@example.com>"
|
|
will be invalid
|
|
*/
|
|
$rfc_2822_multi_delimiters = array(', ', ',');
|
|
$email = str_replace($rfc_2822_multi_delimiters, '**', $email);
|
|
$check_emails = explode('**', $email);
|
|
$valid_emails = array();
|
|
foreach ($check_emails as $check_email) {
|
|
if (!filter_var($check_email, FILTER_VALIDATE_EMAIL) || check_email_invalid($check_email)) {
|
|
debug("send_mail: Invalid e-mail address - '{$check_email}'");
|
|
continue;
|
|
}
|
|
|
|
$valid_emails[] = $check_email;
|
|
}
|
|
// No/invalid email address? Exit.
|
|
if (empty($valid_emails)) {
|
|
debug("send_mail: No valid e-mail address found!");
|
|
return false;
|
|
}
|
|
// Valid emails? then make it back into an RFC 2822 compliant string
|
|
$email = implode(', ', $valid_emails);
|
|
|
|
if (isset($email_rate_limit)) {
|
|
// Limit the number of e-mails sent across the system per hour.
|
|
$count = ps_value("select count(*) value from mail_log where date >= DATE_SUB(now(),interval 1 hour)", [], 0);
|
|
if (($count + count($valid_emails)) > $email_rate_limit) {
|
|
if (isset($userref) && !($useremail_rate_limit_active ?? false)) {
|
|
// Rate limit not previously active, activate and warn them.
|
|
ps_query("update user set email_rate_limit_active=1 where ref=?", ["i",$userref]);
|
|
// MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL to prevent sending email via message_add() as this will cause loop.
|
|
message_add([$userref], $lang["email_rate_limit_active"], '', null, MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL);
|
|
}
|
|
debug("E-mail not sent due to email_rate_limit being exceeded");
|
|
return $lang["email_rate_limit_active"]; // Don't send the e-mail and return the error.
|
|
} else {
|
|
// It's OK to send mail, if rate limit was previously active, reset it
|
|
if ($useremail_rate_limit_active ?? false) {
|
|
ps_query("update user set email_rate_limit_active=0 where ref=?", ["i",$userref]);
|
|
// Send them a message
|
|
// MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL to prevent sending email via message_add() as this will cause loop.
|
|
message_add([$userref], $lang["email_rate_limit_inactive"], '', null, MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($always_email_copy_admin) {
|
|
$bcc .= "," . $email_notify;
|
|
}
|
|
|
|
$subject = strip_tags($subject);
|
|
|
|
// Validate all files to attach are valid and copy any that are URLs locally
|
|
$attachfiles = array();
|
|
$deletefiles = array();
|
|
foreach ($files as $filename => $file) {
|
|
if (substr($file, 0, 4) == "http") {
|
|
$ctx = stream_context_create(array(
|
|
'http' => array(
|
|
'method' => 'POST',
|
|
'timeout' => 2,
|
|
"ignore_errors" => true,
|
|
)
|
|
));
|
|
$filedata = file_get_contents($file, false, $ctx); # File is a URL, not a binary object. Go and fetch the file.
|
|
$file = get_temp_dir() . "/mail_" . uniqid() . ".bin";
|
|
file_put_contents($file, $filedata);
|
|
$deletefiles[] = $file;
|
|
} elseif (!file_exists($file)) {
|
|
debug("file missing: " . $file);
|
|
continue;
|
|
}
|
|
$attachfiles[$filename] = $file;
|
|
}
|
|
|
|
# Send a mail - but correctly encode the message/subject in quoted-printable UTF-8.
|
|
if ($use_phpmailer) {
|
|
send_mail_phpmailer($email, $subject, $message, $from, $reply_to, $html_template, $templatevars, $from_name, $cc, $bcc, $attachfiles);
|
|
cleanup_files($deletefiles);
|
|
return true;
|
|
}
|
|
|
|
# Include footer
|
|
|
|
# Work out correct EOL to use for mails (should use the system EOL).
|
|
if (defined("PHP_EOL")) {
|
|
$eol = PHP_EOL;
|
|
} else {
|
|
$eol = "\r\n";
|
|
}
|
|
|
|
$headers = '';
|
|
$quoted_printable_encoding = true;
|
|
|
|
if (count($attachfiles) > 0) {
|
|
//add boundary string and mime type specification
|
|
$random_hash = md5(date('r', time()));
|
|
$headers .= "Content-Type: multipart/mixed; boundary=\"PHP-mixed-" . $random_hash . "\"" . $eol;
|
|
|
|
$body = "This is a multi-part message in MIME format." . $eol . "--PHP-mixed-" . $random_hash . $eol;
|
|
$body .= "Content-Type: text/plain; charset=\"utf-8\"" . $eol . "Content-Transfer-Encoding: 8bit" . $eol . $eol;
|
|
$body .= $message . $eol . $eol . $eol;
|
|
# Attach all the files (paths have already been checked)
|
|
foreach ($attachfiles as $filename => $file) {
|
|
$filedata = file_get_contents($file);
|
|
$attachment = chunk_split(base64_encode($filedata));
|
|
$body .= "--PHP-mixed-" . $random_hash . $eol;
|
|
$body .= "Content-Type: application/octet-stream; name=\"" . $filename . "\"" . $eol;
|
|
$body .= "Content-Transfer-Encoding: base64" . $eol;
|
|
$body .= "Content-Disposition: attachment; filename=\"" . $filename . "\"" . $eol . $eol;
|
|
$body .= $attachment;
|
|
}
|
|
$body .= "--PHP-mixed-" . $random_hash . "--" . $eol; # Final terminating boundary.
|
|
|
|
$message = $body;
|
|
$quoted_printable_encoding = false; // Ensure attachment names and utf8 text do not get corrupted
|
|
}
|
|
|
|
$message .= $eol . $eol . $eol . $email_footer;
|
|
|
|
$subject_for_mail_log = $subject;
|
|
if ($quoted_printable_encoding) {
|
|
$message = rs_quoted_printable_encode($message);
|
|
$subject = rs_quoted_printable_encode_subject($subject);
|
|
}
|
|
|
|
if ($from == "") {
|
|
$from = $email_from;
|
|
}
|
|
if ($reply_to == "") {
|
|
$reply_to = $email_from;
|
|
}
|
|
if ($from_name == "") {
|
|
$from_name = $applicationname;
|
|
}
|
|
|
|
if (substr($reply_to, -1) == ",") {
|
|
$reply_to = substr($reply_to, 0, -1);
|
|
}
|
|
|
|
$reply_tos = explode(",", $reply_to);
|
|
|
|
$headers .= "From: ";
|
|
#allow multiple emails, and fix for long format emails
|
|
for ($n = 0; $n < count($reply_tos); $n++) {
|
|
if ($n != 0) {
|
|
$headers .= ",";
|
|
}
|
|
if (strstr($reply_tos[$n], "<")) {
|
|
$rtparts = explode("<", $reply_tos[$n]);
|
|
$headers .= $rtparts[0] . " <" . $rtparts[1];
|
|
} else {
|
|
mb_internal_encoding("UTF-8");
|
|
$headers .= mb_encode_mimeheader($from_name, "UTF-8") . " <" . $reply_tos[$n] . ">";
|
|
}
|
|
}
|
|
$headers .= $eol;
|
|
$headers .= "Reply-To: $reply_to" . $eol;
|
|
|
|
if ($cc != "") {
|
|
#allow multiple emails, and fix for long format emails
|
|
$ccs = explode(",", $cc);
|
|
$headers .= "Cc: ";
|
|
for ($n = 0; $n < count($ccs); $n++) {
|
|
if ($n != 0) {
|
|
$headers .= ",";
|
|
}
|
|
if (strstr($ccs[$n], "<")) {
|
|
$ccparts = explode("<", $ccs[$n]);
|
|
$headers .= $ccparts[0] . " <" . $ccparts[1];
|
|
} else {
|
|
mb_internal_encoding("UTF-8");
|
|
$headers .= mb_encode_mimeheader((string) $userfullname, "UTF-8") . " <" . $ccs[$n] . ">";
|
|
}
|
|
}
|
|
$headers .= $eol;
|
|
}
|
|
|
|
if ($bcc != "") {
|
|
#add bcc
|
|
$bccs = explode(",", $bcc);
|
|
$headers .= "Bcc: ";
|
|
for ($n = 0; $n < count($bccs); $n++) {
|
|
if ($n != 0) {
|
|
$headers .= ",";
|
|
}
|
|
if (strstr($bccs[$n], "<")) {
|
|
$bccparts = explode("<", $bccs[$n]);
|
|
$headers .= $bccparts[0] . " <" . $bccparts[1];
|
|
} else {
|
|
mb_internal_encoding("UTF-8");
|
|
$headers .= mb_encode_mimeheader($userfullname, "UTF-8") . " <" . $bccs[$n] . ">";
|
|
}
|
|
}
|
|
$headers .= $eol;
|
|
}
|
|
|
|
$headers .= "Date: " . date("r") . $eol;
|
|
$headers .= "Message-ID: <" . date("YmdHis") . $from . ">" . $eol;
|
|
$headers .= "MIME-Version: 1.0" . $eol;
|
|
$headers .= "X-Mailer: PHP Mail Function" . $eol;
|
|
if (!is_html($message)) {
|
|
$headers .= "Content-Type: text/plain; charset=\"UTF-8\"" . $eol;
|
|
} else {
|
|
$headers .= "Content-Type: text/html; charset=\"UTF-8\"" . $eol;
|
|
// Add CSS links so email can use the styles
|
|
$messageprefix = '<link href="' . $baseurl . '/css/global.css" rel="stylesheet" type="text/css" media="screen,projection,print" />';
|
|
$messageprefix .= '<link href="' . $baseurl . '/css/light.css" rel="stylesheet" type="text/css" media="screen,projection,print" />';
|
|
$messageprefix .= '<link href="' . $baseurl . '/css/css_override.php" rel="stylesheet" type="text/css" media="screen,projection,print" />';
|
|
$message = $messageprefix . $message;
|
|
}
|
|
$headers .= "Content-Transfer-Encoding: quoted-printable" . $eol;
|
|
log_mail($email, $subject_for_mail_log, $reply_to);
|
|
mail($email, $subject, $message, $headers);
|
|
cleanup_files($deletefiles);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* if ($use_phpmailer==true) this function is used instead.
|
|
*
|
|
* Mail templates can include lang, server, site_text, and POST variables by default
|
|
* ex ( [lang_mycollections], [server_REMOTE_ADDR], [text_footer] , [message]
|
|
*
|
|
* additional values must be made available through $templatevars
|
|
* For example, a complex url or image path that may be sent in an
|
|
* email should be added to the templatevars array and passed into send_mail.
|
|
* available templatevars need to be well-documented, and sample templates
|
|
* need to be available.
|
|
*
|
|
* @param string $email Email address to send to
|
|
* @param string $subject Email subject
|
|
* @param string $message Message text
|
|
* @param string $from From address - defaults to $email_from
|
|
* @param string $reply_to Reply to address - defaults to $email_from
|
|
* @param string $html_template Optional template (this is a $lang entry with placeholders)
|
|
* @param array $templatevars Used to populate email template placeholders
|
|
* @param string $from_name Email from name
|
|
* @param string $cc Optional CC addresses
|
|
* @param string $bcc Optional BCC addresses
|
|
* @param array $files Optional array of file paths to attach in the format [filename.txt => /path/to/file.txt]
|
|
* @return void
|
|
*/
|
|
|
|
function send_mail_phpmailer($email, $subject, $message = "", $from = "", $reply_to = "", $html_template = "", $templatevars = array(), $from_name = "", $cc = "", $bcc = "", $files = array())
|
|
{
|
|
# Include footer
|
|
global $header_colour_style_override, $email_from;
|
|
include_once __DIR__ . '/../lib/PHPMailer/PHPMailer.php';
|
|
include_once __DIR__ . '/../lib/PHPMailer/Exception.php';
|
|
include_once __DIR__ . '/../lib/PHPMailer/SMTP.php';
|
|
|
|
if (check_email_invalid($email)) {
|
|
return false;
|
|
}
|
|
|
|
$from_system = false;
|
|
if ($from == "") {
|
|
$from = $email_from;
|
|
$from_system = true;
|
|
}
|
|
if ($reply_to == "") {
|
|
$reply_to = $email_from;
|
|
}
|
|
global $applicationname;
|
|
if ($from_name == "") {
|
|
$from_name = $applicationname;
|
|
}
|
|
|
|
#check for html template. If exists, attempt to include vars into message
|
|
if ($html_template != "") {
|
|
# Attempt to verify users by email, which allows us to get the email template by lang and usergroup
|
|
$to_usergroup = ps_query("select lang, usergroup from user where email = ?", array("s", $email), "");
|
|
|
|
if (count($to_usergroup) != 0) {
|
|
$to_usergroupref = $to_usergroup[0]['usergroup'];
|
|
$to_usergrouplang = $to_usergroup[0]['lang'];
|
|
} else {
|
|
$to_usergrouplang = "";
|
|
}
|
|
|
|
if ($to_usergrouplang == "") {
|
|
global $defaultlanguage;
|
|
$to_usergrouplang = $defaultlanguage;
|
|
}
|
|
|
|
if (isset($to_usergroupref)) {
|
|
$modified_to_usergroupref = hook("modifytousergroup", "", $to_usergroupref);
|
|
if (is_int($modified_to_usergroupref)) {
|
|
$to_usergroupref = $modified_to_usergroupref;
|
|
}
|
|
$results = ps_query("SELECT `language`, `name`, `text` FROM site_text WHERE (`page` = 'all' OR `page` = '') AND `name` = ? AND specific_to_group = ?;", array("s", $html_template, "i", $to_usergroupref));
|
|
} else {
|
|
$results = ps_query("SELECT `language`, `name`, `text` FROM site_text WHERE (`page` = 'all' OR `page` = '') AND `name` = ? AND specific_to_group IS NULL;", array("s", $html_template));
|
|
}
|
|
|
|
global $site_text;
|
|
for ($n = 0; $n < count($results); $n++) {
|
|
$site_text[$results[$n]["language"] . "-" . $results[$n]["name"]] = $results[$n]["text"];
|
|
}
|
|
|
|
$language = $to_usergrouplang;
|
|
|
|
if (array_key_exists($language . "-" . $html_template, $site_text)) {
|
|
$template = $site_text[$language . "-" . $html_template];
|
|
} else {
|
|
global $languages;
|
|
|
|
# Can't find the language key? Look for it in other languages.
|
|
reset($languages);
|
|
foreach ($languages as $key => $value) {
|
|
if (array_key_exists($key . "-" . $html_template, $site_text)) {
|
|
$template = $site_text[$key . "-" . $html_template];
|
|
break;
|
|
}
|
|
}
|
|
// Fall back to language file if not in site text
|
|
global $lang;
|
|
if (!isset($template)) {
|
|
if (isset($lang["all__" . $html_template])) {
|
|
$template = $lang["all__" . $html_template];
|
|
} elseif (isset($lang[$html_template])) {
|
|
$template = $lang[$html_template];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($template) && $template != "") {
|
|
preg_match_all('/\[[^\]]*\]/', $template, $test);
|
|
$setvalues = [];
|
|
foreach ($test[0] as $placeholder) {
|
|
$placeholder = str_replace("[", "", $placeholder);
|
|
$placeholder = str_replace("]", "", $placeholder);
|
|
|
|
if (substr($placeholder, 0, 5) == "lang_") {
|
|
// Get lang variables (ex. [lang_mycollections])
|
|
global $lang;
|
|
switch (substr($placeholder, 5)) {
|
|
case "emailcollectionmessageexternal":
|
|
$setvalues[$placeholder] = str_replace('[applicationname]', $applicationname, $lang["emailcollectionmessageexternal"]);
|
|
break;
|
|
case "emailcollectionmessage":
|
|
$setvalues[$placeholder] = str_replace('[applicationname]', $applicationname, $lang["emailcollectionmessage"]);
|
|
break;
|
|
default:
|
|
$setvalues[$placeholder] = $lang[substr($placeholder, 5)];
|
|
break;
|
|
}
|
|
} elseif (substr($placeholder, 0, 5) == "text_") {
|
|
// Get text string (legacy)
|
|
$setvalues[$placeholder] = text(substr($placeholder, 5));
|
|
} elseif ($placeholder == "client_ip") {
|
|
// Get server variables (ex. [server_REMOTE_ADDR] for a user request)
|
|
$setvalues[$placeholder] = get_ip();
|
|
} elseif ($placeholder == 'img_headerlogo') {
|
|
// Add header image to email if not using template
|
|
$img_url = get_header_image(true);
|
|
$img_div_style = "max-height:50px;padding: 5px;";
|
|
$img_div_style .= "background: " . ((isset($header_colour_style_override) && $header_colour_style_override != '') ? $header_colour_style_override : "rgba(0, 0, 0, 0.6)") . ";";
|
|
$setvalues[$placeholder] = '<div style="' . $img_div_style . '"><img src="' . $img_url . '" style="max-height:50px;" /></div><br /><br />';
|
|
} elseif ($placeholder == "embed_thumbnail") {
|
|
# [embed_thumbnail] (requires url in templatevars['thumbnail'])
|
|
$thumbcid = uniqid('thumb');
|
|
$embed_thumbnail = true;
|
|
$setvalues[$placeholder] = "<img style='border:1px solid #d1d1d1;' src='cid:$thumbcid' />";
|
|
} 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","<br/>","<br>"], "<br />", $body);
|
|
|
|
// Remove any sequences of three or more line breaks with doubles
|
|
while (strpos($body, "<br /><br /><br />") !== false) {
|
|
$body = str_replace("<br /><br /><br />", "<br /><br />", $body);
|
|
}
|
|
|
|
// Also remove any unnecessary line breaks that were already formatted by HTML paragraphs
|
|
$body = str_replace(["</p><br /><br />","</p><br />"], "</p>", $body);
|
|
} else {
|
|
$mail->IsHTML(false);
|
|
}
|
|
|
|
$mail->Subject = $subject;
|
|
$mail->Body = $body;
|
|
|
|
if (isset($embed_thumbnail) && isset($templatevars['thumbnail'])) {
|
|
$mail->AddEmbeddedImage($templatevars['thumbnail'], $thumbcid, $thumbcid, 'base64', 'image/jpeg');
|
|
}
|
|
|
|
if (isset($images)) {
|
|
foreach ($images as $image) {
|
|
/** {@see include/mime_types.php} */
|
|
$found_types = get_mime_types_by_extension(parse_filename_extension($image));
|
|
$mime_type = $found_types === [] ? '' : reset($found_types);
|
|
|
|
$mail->AddEmbeddedImage($image, basename($image), basename($image), 'base64', $mime_type);
|
|
}
|
|
}
|
|
|
|
if (isset($attachments)) {
|
|
foreach ($attachments as $attachment) {
|
|
$mail->AddAttachment($attachment, basename($attachment));
|
|
}
|
|
}
|
|
|
|
if (count($files) > 0) {
|
|
# Attach all the files
|
|
foreach ($files as $filename => $file) {
|
|
if (substr($file, 0, 4) == "http") {
|
|
$ctx = stream_context_create(array(
|
|
'http' => array(
|
|
'method' => 'POST',
|
|
'timeout' => 2,
|
|
"ignore_errors" => true,
|
|
)
|
|
));
|
|
$file = file_get_contents($file, false, $ctx); # File is a URL, not a binary object. Go and fetch the file.
|
|
} elseif (!file_exists($file)) {
|
|
debug("file missing: " . $file);
|
|
continue;
|
|
}
|
|
|
|
$mail->AddAttachment($file, $filename);
|
|
}
|
|
}
|
|
|
|
if (is_html($body)) {
|
|
$mail->AltBody = $mail->html2text($body);
|
|
}
|
|
|
|
log_mail($email, $subject, $reply_to);
|
|
|
|
$GLOBALS["use_error_exception"] = true;
|
|
try {
|
|
$mail->Send();
|
|
} catch (Exception $e) {
|
|
echo "Message could not be sent. <p>";
|
|
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) { ?>
|
|
<span class="TopInpageNavRight">
|
|
<?php if ($break) { ?>
|
|
<br />
|
|
<?php }
|
|
|
|
if ($curpage > 1) { ?>
|
|
<a
|
|
class="prevPageLink"
|
|
title="<?php echo escape($lang["previous"]) ?>"
|
|
href="<?php echo generateURL($url, (isset($url_params) ? $url_params : array()), array("go" => "prev","offset" => ($offset - $per_page)));?>"
|
|
onclick="<?php echo $confirm_page_change;?> return <?php echo $modal ? 'Modal' : 'CentralSpace'; ?>Load(this, <?php echo $scroll; ?>);"
|
|
>
|
|
<?php } ?>
|
|
<i aria-hidden="true" class="fa fa-arrow-left"></i><?php echo ($curpage > 1) ? "</a>" : ''; ?>
|
|
|
|
<div class="JumpPanel" id="jumppanel<?php echo $jumpcount?>" style="display:none;">
|
|
<?php echo escape($lang["jumptopage"]) ?>:
|
|
<input
|
|
type="text"
|
|
size="1"
|
|
id="jumpto<?php echo $jumpcount?>"
|
|
onkeydown="
|
|
var evt = event || window.event;
|
|
if (evt.keyCode == 13) {
|
|
var jumpto = document.getElementById('jumpto<?php echo $jumpcount?>').value;
|
|
if (jumpto < 1) {
|
|
jumpto = 1;
|
|
};
|
|
if (jumpto > <?php echo $totalpages?>) {
|
|
jumpto = <?php echo $totalpages?>;
|
|
};
|
|
<?php echo $modal ? 'Modal' : 'CentralSpace'; ?>Load('<?php echo generateURL($url, (isset($url_params) ? $url_params : array()), array("go" => "page")); ?>&offset=' + ((jumpto - 1) * <?php echo urlencode($per_page) ?>), <?php echo $scroll; ?>);
|
|
}">
|
|
|
|
<a
|
|
aria-hidden="true"
|
|
class="fa fa-times-circle"
|
|
href="#"
|
|
onclick="
|
|
document.getElementById('jumppanel<?php echo $jumpcount?>').style.display='none';
|
|
document.getElementById('jumplink<?php echo $jumpcount?>').style.display='inline';">
|
|
</a>
|
|
</div>
|
|
|
|
<a
|
|
href="#"
|
|
id="jumplink<?php echo $jumpcount?>"
|
|
title="<?php echo escape($lang["jumptopage"]) ?>"
|
|
onclick="
|
|
document.getElementById('jumppanel<?php echo $jumpcount?>').style.display='inline';
|
|
document.getElementById('jumplink<?php echo $jumpcount?>').style.display='none';
|
|
document.getElementById('jumpto<?php echo $jumpcount?>').focus();
|
|
return false;">
|
|
<?php echo escape($lang["page"]) ?> <?php echo escape($curpage) ?> <?php echo escape($lang["of"]) ?> <?php echo $totalpages?>
|
|
</a>
|
|
|
|
<?php if ($curpage < $totalpages) { ?>
|
|
<a
|
|
class="nextPageLink"
|
|
title="<?php echo escape($lang["next"]) ?>"
|
|
href="<?php echo generateURL($url, (isset($url_params) ? $url_params : array()), array("go" => "next","offset" => ($offset + $per_page)));?>"
|
|
onclick="<?php echo $confirm_page_change;?> return <?php echo $modal ? 'Modal' : 'CentralSpace'; ?>Load(this, <?php echo $scroll; ?>);">
|
|
<?php } ?>
|
|
|
|
<i aria-hidden="true" class="fa fa-arrow-right"></i>
|
|
|
|
<?php if ($curpage < $totalpages) { ?>
|
|
</a>
|
|
<?php } ?>
|
|
</span>
|
|
|
|
<?php } else { ?>
|
|
<span class="HorizontalWhiteNav"> </span>
|
|
<div style="display: <?php echo ($pagename == "search") ? "block" : "inline"; ?>"> </div>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the extension part of a filename
|
|
*
|
|
* @param mixed $strName The filename
|
|
* @return string The filename minus the extension
|
|
*/
|
|
function remove_extension($strName)
|
|
{
|
|
$ext = strrchr((string) $strName, '.');
|
|
if ($ext !== false) {
|
|
$strName = substr($strName, 0, -strlen($ext));
|
|
}
|
|
return $strName;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a list of permitted extensions for the given resource type.
|
|
*
|
|
* @param integer $resource_type
|
|
* @return string
|
|
*/
|
|
function get_allowed_extensions_by_type($resource_type)
|
|
{
|
|
return ps_value("select allowed_extensions value from resource_type where ref = ?", array("i", $resource_type), "", "schema");
|
|
}
|
|
|
|
/**
|
|
* Detect if a path is relative or absolute.
|
|
* If it is relative, we compute its absolute location by assuming it is
|
|
* relative to the application root (parent folder).
|
|
*
|
|
* @param string $path A relative or absolute path
|
|
* @param boolean $create_if_not_exists Try to create the path if it does not exists. Default to False.
|
|
* @access public
|
|
* @return string A absolute path
|
|
*/
|
|
function getAbsolutePath($path, $create_if_not_exists = false)
|
|
{
|
|
if (preg_match('/^(\/|[a-zA-Z]:[\\/]{1})/', $path)) { // If the path start by a '/' or 'c:\', it is an absolute path.
|
|
$folder = $path;
|
|
} else // It is a relative path.
|
|
{
|
|
$folder = sprintf('%s%s..%s%s', __DIR__, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $path);
|
|
}
|
|
|
|
if ($create_if_not_exists && !file_exists($folder)) { // Test if the path need to be created.
|
|
mkdir($folder, 0777);
|
|
} // Test if the path need to be created.
|
|
|
|
return $folder;
|
|
}
|
|
|
|
/**
|
|
* Find the files present in a folder, and sub-folder.
|
|
*
|
|
* @param string $path The path to look into.
|
|
* @param boolean $recurse Trigger the recursion, default to True.
|
|
* @param boolean $include_hidden Trigger the listing of hidden files / hidden directories, default to False.
|
|
* @access public
|
|
* @return array A list of files present in the inspected folder (paths are relative to the inspected folder path).
|
|
*/
|
|
function getFolderContents($path, $recurse = true, $include_hidden = false)
|
|
{
|
|
if (!is_dir($path)) { // Test if the path is not a folder.
|
|
return array();
|
|
} // Test if the path is not a folder.
|
|
|
|
$directory_handle = opendir($path);
|
|
if ($directory_handle === false) { // Test if the directory listing failed.
|
|
return array();
|
|
} // Test if the directory listing failed.
|
|
|
|
$files = array();
|
|
while (($file = readdir($directory_handle)) !== false) { // For each directory listing entry.
|
|
if (
|
|
! in_array($file, array('.', '..')) // Test if file is not unix parent and current path.
|
|
&& ($include_hidden || ! preg_match('/^\./', $file)) // Test if the file can be listed.
|
|
) {
|
|
$complete_path = $path . DIRECTORY_SEPARATOR . $file;
|
|
if (is_dir($complete_path) && $recurse) { // If the path is a directory, and need to be explored.
|
|
$sub_dir_files = getFolderContents($complete_path, $recurse, $include_hidden);
|
|
foreach ($sub_dir_files as $sub_dir_file) { // For each subdirectory contents.
|
|
$files[] = $file . DIRECTORY_SEPARATOR . $sub_dir_file;
|
|
} // For each subdirectory contents.
|
|
} elseif (is_file($complete_path)) { // If the path is a file.
|
|
$files[] = $file;
|
|
}
|
|
} // Test if file is not unix parent and current path.
|
|
} // For each directory listing entry.
|
|
|
|
// We close the directory handle:
|
|
closedir($directory_handle);
|
|
|
|
// We sort the files alphabetically.
|
|
natsort($files);
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* Returns filename component of path
|
|
* This version is UTF-8 proof.
|
|
*
|
|
* @param string $file A path.
|
|
* @access public
|
|
* @return string Returns the base name of the given path.
|
|
*/
|
|
function mb_basename($file)
|
|
{
|
|
$regex_file = preg_split('/[\\/]+/', $file);
|
|
return end($regex_file);
|
|
}
|
|
|
|
/**
|
|
* Remove the extension part of a filename.
|
|
*
|
|
* @param string $name A file name.
|
|
* @access public
|
|
* @return string Return the file name without the extension part.
|
|
*/
|
|
function strip_extension($name, $use_ext_list = false)
|
|
{
|
|
$s = strrpos($name, '.');
|
|
if ($s === false) {
|
|
return $name;
|
|
} else {
|
|
global $download_filename_strip_extensions;
|
|
if ($use_ext_list && isset($download_filename_strip_extensions)) {
|
|
// Use list of specified extensions if config present.
|
|
$fn_extension = substr($name, $s + 1);
|
|
if (in_array(strtolower($fn_extension), $download_filename_strip_extensions)) {
|
|
return substr($name, 0, $s);
|
|
} else {
|
|
return $name;
|
|
}
|
|
} else {
|
|
// Attempt to remove file extension from string where download_filename_strip_extensions is not configured.
|
|
return substr($name, 0, $s);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a process lock exists for the given process name.
|
|
*
|
|
* @param string $name Name of lock to check
|
|
* @return boolean TRUE if a current process lock is in place, false if not
|
|
*/
|
|
function is_process_lock($name)
|
|
{
|
|
global $storagedir,$process_locks_max_seconds;
|
|
|
|
# Check that tmp/process_locks exists, create if not.
|
|
# Since the get_temp_dir() method does this checking, omit: if(!is_dir($storagedir . "/tmp")){mkdir($storagedir . "/tmp",0777);}
|
|
if (!is_dir(get_temp_dir() . "/process_locks")) {
|
|
mkdir(get_temp_dir() . "/process_locks", 0777);
|
|
}
|
|
|
|
$file = get_temp_dir() . "/process_locks/" . $name;
|
|
if (!file_exists($file)) {
|
|
return false;
|
|
}
|
|
if (!is_readable($file)) {
|
|
return true;
|
|
} // Lock exists and cannot read it so must assume it's still valid
|
|
|
|
$GLOBALS["use_error_exception"] = true;
|
|
try {
|
|
$time = trim(file_get_contents($file));
|
|
if ((time() - (int) $time) > $process_locks_max_seconds) {
|
|
return false;
|
|
} # Lock has expired
|
|
} catch (Exception $e) {
|
|
debug("is_process_lock: Attempt to get file contents '$file' failed. Reason: {$e->getMessage()}");
|
|
}
|
|
unset($GLOBALS["use_error_exception"]);
|
|
|
|
return true; # Lock is valid
|
|
}
|
|
|
|
/**
|
|
* Set a process lock
|
|
*
|
|
* @param string $name
|
|
* @return boolean
|
|
*/
|
|
function set_process_lock($name)
|
|
{
|
|
file_put_contents(get_temp_dir() . "/process_locks/" . $name, time());
|
|
// make sure this is editable by the server in case a process lock could be set by different system users
|
|
chmod(get_temp_dir() . "/process_locks/" . $name, 0777);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clear a process lock
|
|
*
|
|
* @param string $name
|
|
* @return boolean
|
|
*/
|
|
function clear_process_lock($name)
|
|
{
|
|
if (!file_exists(get_temp_dir() . "/process_locks/" . $name)) {
|
|
return false;
|
|
}
|
|
unlink(get_temp_dir() . "/process_locks/" . $name);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Custom function for retrieving a file size. A resolution for PHP's issue with large files and filesize().
|
|
*
|
|
* @param string $path
|
|
* @return integer|bool The file size in bytes
|
|
*/
|
|
function filesize_unlimited($path)
|
|
{
|
|
if ('WINNT' == PHP_OS) {
|
|
if (class_exists('COM')) {
|
|
try {
|
|
$filesystem = new COM('Scripting.FileSystemObject');
|
|
$file = $filesystem->GetFile($path);
|
|
|
|
return $file->Size();
|
|
} catch (com_exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return exec('for %I in (' . escapeshellarg($path) . ') do @echo %~zI');
|
|
} elseif ('Darwin' == PHP_OS || 'FreeBSD' == PHP_OS) {
|
|
$bytesize = exec("stat -f '%z' " . escapeshellarg($path));
|
|
} else {
|
|
$bytesize = exec("stat -c '%s' " . escapeshellarg($path));
|
|
}
|
|
|
|
if (!is_int_loose($bytesize)) {
|
|
$GLOBALS["use_error_exception"] = true;
|
|
try {
|
|
$bytesize = filesize($path); # Bomb out, the output wasn't as we expected. Return the filesize() output.
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
unset($GLOBALS["use_error_exception"]);
|
|
}
|
|
|
|
if (is_int_loose($bytesize)) {
|
|
return (int) $bytesize;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Strip the leading comma from a string
|
|
*
|
|
* @param string $val
|
|
* @return string
|
|
*/
|
|
function strip_leading_comma($val)
|
|
{
|
|
return preg_replace('/^\,/', '', (string) $val);
|
|
}
|
|
|
|
/**
|
|
* Determines where the tmp directory is. There are three options here:
|
|
* 1. tempdir - If set in config.php, use this value.
|
|
* 2. storagedir ."/tmp" - If storagedir is set in config.php, use it and create a subfolder tmp.
|
|
* 3. generate default path - use filestore/tmp if all other attempts fail.
|
|
* 4. if a uniqid is provided, create a folder within tmp and return the full path
|
|
*
|
|
* @param bool $asUrl - If we want the return to be like http://my.resourcespace.install/path set this as true.
|
|
* @return string Path to the tmp directory.
|
|
*/
|
|
function get_temp_dir($asUrl = false, $uniqid = "")
|
|
{
|
|
global $storagedir, $tempdir;
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
if ($uniqid != "") {
|
|
$uniqid = md5($uniqid);
|
|
$result .= "/$uniqid";
|
|
if (!is_dir($result)) {
|
|
$GLOBALS["use_error_exception"] = true;
|
|
try {
|
|
mkdir($result, 0777, true);
|
|
} catch (Exception $e) {
|
|
debug("get_temp_dir: {$result} ERROR-" . $e->getMessage());
|
|
}
|
|
unset($GLOBALS["use_error_exception"]);
|
|
}
|
|
}
|
|
|
|
// return the result.
|
|
if ($asUrl) {
|
|
$result = convert_path_to_url($result);
|
|
$result = str_replace('\\', '/', $result);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Converts a path to a url relative to the installation.
|
|
*
|
|
* @param string $abs_path: The absolute path.
|
|
* @return Url that is the relative path.
|
|
*/
|
|
function convert_path_to_url($abs_path)
|
|
{
|
|
// Get the root directory of the app:
|
|
$rootDir = dirname(__DIR__);
|
|
// Get the baseurl:
|
|
global $baseurl;
|
|
// Replace the $rootDir with $baseurl in the path given:
|
|
return str_ireplace($rootDir, $baseurl, $abs_path);
|
|
}
|
|
|
|
/**
|
|
* Escaping an unsafe command string
|
|
*
|
|
* @param string $cmd Unsafe command to run
|
|
* @param array $args List of placeholders and their values which will have to be escapedshellarg()d.
|
|
*
|
|
* @return string Escaped command string
|
|
*/
|
|
function escape_command_args($cmd, array $args): string
|
|
{
|
|
debug_function_call(__FUNCTION__, func_get_args());
|
|
if ($args === []) {
|
|
return $cmd;
|
|
}
|
|
|
|
foreach ($args as $placeholder => $value) {
|
|
if (strpos($cmd, $placeholder) === false) {
|
|
debug("Unable to find arg '{$placeholder}' in '{$cmd}'. Make sure the placeholder exists in the command string");
|
|
continue;
|
|
} elseif (!($value instanceof CommandPlaceholderArg)) {
|
|
$value = new CommandPlaceholderArg($value, null);
|
|
}
|
|
|
|
$cmd = str_replace($placeholder, escapeshellarg($value->__toString()), $cmd);
|
|
}
|
|
|
|
return $cmd;
|
|
}
|
|
|
|
/**
|
|
* Utility function which works like system(), but returns the complete output string rather than just the last line of it.
|
|
*
|
|
* @uses escape_command_args()
|
|
*
|
|
* @param string $command Command to run
|
|
* @param boolean $geterrors Set to TRUE to include errors in the output
|
|
* @param array $params List of placeholders and their values which will have to be escapedshellarg()d.
|
|
* @param int $timeout Maximum time in seconds before a command is forcibly stopped
|
|
*
|
|
* @return string Command output
|
|
*/
|
|
function run_command($command, $geterrors = false, array $params = array(), int $timeout = 0)
|
|
{
|
|
global $debug_log,$config_windows;
|
|
|
|
$command = escape_command_args($command, $params);
|
|
debug("CLI command: $command");
|
|
|
|
$cmd_tmp_file = false;
|
|
if ($config_windows && mb_strlen($command) > 8191) {
|
|
// Windows systems have a hard time with the long paths (often used for video generation)
|
|
// This work-around creates a batch file containing the command, then executes that.
|
|
$unique_key = generateSecureKey(32);
|
|
$cmd_tmp_file = get_temp_dir() . "/run_command_" . $unique_key . ".bat";
|
|
file_put_contents($cmd_tmp_file, $command);
|
|
$command = $cmd_tmp_file;
|
|
}
|
|
|
|
$descriptorspec = array(
|
|
1 => array("pipe", "w") // stdout is a pipe that the child will write to
|
|
);
|
|
if ($debug_log || $geterrors) {
|
|
if ($config_windows) {
|
|
$pid = getmypid();
|
|
$log_location = get_temp_dir() . "/error_" . md5($command . serialize($params) . $pid) . ".txt";
|
|
$descriptorspec[2] = array("file", $log_location, "w"); // stderr is a file that the child will write to
|
|
} else {
|
|
$descriptorspec[2] = array("pipe", "w"); // stderr is a pipe that the child will write to
|
|
}
|
|
}
|
|
|
|
$child_process = -1;
|
|
if (
|
|
!$config_windows && $timeout > 0
|
|
&& shell_exec('which timeout') !== null
|
|
) {
|
|
$command = 'timeout ' . escapeshellarg($timeout) . ' ' . $command;
|
|
} elseif ($timeout > 0 && function_exists('pcntl_fork')) {
|
|
// Branch child process if a timeout is specified and the timeout utility isn't available
|
|
// Create output file so that we can retrieve the output of the child process from the parent process
|
|
$pid = getmypid();
|
|
$command_output = get_temp_dir() . '/output_' . md5($command . serialize($params) . $pid) . ".txt";
|
|
$child_process = pcntl_fork();
|
|
}
|
|
// Await output from child, or timeout.
|
|
if ($child_process > 0) {
|
|
$start_time = time();
|
|
while ((time() - $start_time) < $timeout) {
|
|
if (file_exists($command_output)) {
|
|
$output = file_get_contents($command_output);
|
|
unlink($command_output);
|
|
return $output;
|
|
}
|
|
}
|
|
// Kill the child process to free up resources
|
|
exec(($config_windows ? 'taskkill /F /PID ' : 'kill ') . escapeshellarg($child_process));
|
|
return "";
|
|
}
|
|
|
|
$process = @proc_open($command, $descriptorspec, $pipe, null, null, array('bypass_shell' => true));
|
|
|
|
if (!is_resource($process)) {
|
|
if ($cmd_tmp_file) {
|
|
unlink($cmd_tmp_file);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
$output = trim(stream_get_contents($pipe[1]));
|
|
if ($geterrors) {
|
|
$output .= trim($config_windows ? file_get_contents($log_location) : stream_get_contents($pipe[2]));
|
|
}
|
|
if ($debug_log) {
|
|
debug("CLI output: $output");
|
|
debug("CLI errors: " . trim($config_windows ? file_get_contents($log_location) : stream_get_contents($pipe[2])));
|
|
}
|
|
if ($config_windows && isset($log_location)) {
|
|
unlink($log_location);
|
|
}
|
|
proc_close($process);
|
|
if ($cmd_tmp_file) {
|
|
unlink($cmd_tmp_file);
|
|
}
|
|
// If this is a child process put the output into the output file and kill the process here.
|
|
if ($child_process === 0) {
|
|
file_put_contents($command_output, $output);
|
|
exit();
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Similar to run_command but returns an array with the resulting output (stdout & stderr) fetched concurrently
|
|
* for improved performance.
|
|
*
|
|
* @param mixed $command Command to run
|
|
*
|
|
* @return array Command output
|
|
*/
|
|
function run_external($command)
|
|
{
|
|
global $debug_log;
|
|
|
|
$pipes = array();
|
|
$output = array();
|
|
# Pipes for stdin, stdout and stderr
|
|
$descriptorspec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w"));
|
|
|
|
debug("CLI command: $command");
|
|
|
|
# Execute the command
|
|
$process = proc_open($command, $descriptorspec, $pipes);
|
|
|
|
# Ensure command returns an external PHP resource
|
|
if (!is_resource($process)) {
|
|
return false;
|
|
}
|
|
|
|
# Immediately close the input pipe
|
|
fclose($pipes[0]);
|
|
|
|
# Set both output streams to non-blocking mode
|
|
stream_set_blocking($pipes[1], false);
|
|
stream_set_blocking($pipes[2], false);
|
|
|
|
while (true) {
|
|
$read = array();
|
|
|
|
if (!feof($pipes[1])) {
|
|
$read[] = $pipes[1];
|
|
}
|
|
|
|
if (!feof($pipes[2])) {
|
|
$read[] = $pipes[2];
|
|
}
|
|
|
|
if (!$read) {
|
|
break;
|
|
}
|
|
|
|
$write = null;
|
|
$except = null;
|
|
$ready = stream_select($read, $write, $except, 2);
|
|
|
|
if ($ready === false) {
|
|
break;
|
|
}
|
|
|
|
foreach ($read as $r) {
|
|
# Read a line and strip newline and carriage return from the end
|
|
$line = rtrim(fgets($r, 1024), "\r\n");
|
|
$output[] = $line;
|
|
}
|
|
}
|
|
|
|
# Close the output pipes
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
|
|
debug("CLI output: " . implode("\n", $output));
|
|
|
|
proc_close($process);
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Display a styledalert() modal error and optionally return the browser to the previous page after 2 seconds
|
|
*
|
|
* @param string $error Error text to display
|
|
* @param boolean $back Return to previous page?
|
|
* @param integer $code (Optional) HTTP code to return
|
|
*
|
|
* @return void
|
|
*/
|
|
function error_alert($error, $back = true, $code = 403)
|
|
{
|
|
http_response_code($code);
|
|
|
|
extract($GLOBALS, EXTR_SKIP);
|
|
if ($back) {
|
|
include __DIR__ . "/header.php";
|
|
}
|
|
?>
|
|
|
|
<script type='text/javascript'>
|
|
if (typeof ModalClose === 'function') {
|
|
ModalClose();
|
|
}
|
|
|
|
if (typeof styledalert === 'function') {
|
|
styledalert('<?php echo escape($lang["error"]) ?>', '<?php echo escape($error) ?>');
|
|
} else {
|
|
alert('<?php echo escape($error) ?>');
|
|
}
|
|
|
|
<?php if ($back) { ?>
|
|
window.setTimeout(function(){history.go(-1);},2000);
|
|
<?php } ?>
|
|
</script>
|
|
|
|
<?php
|
|
if ($back) {
|
|
include __DIR__ . "/footer.php";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When displaying metadata, applies trim/wordwrap/highlights.
|
|
*
|
|
* @param string $value
|
|
* @return string
|
|
*/
|
|
function format_display_field($value)
|
|
{
|
|
global $results_title_trim,$results_title_wordwrap,$df,$x,$search;
|
|
|
|
$value = strip_tags_and_attributes($value);
|
|
|
|
$string = i18n_get_translated($value);
|
|
$string = TidyList($string);
|
|
|
|
if (isset($df[$x]['type']) && $df[$x]['type'] == FIELD_TYPE_TEXT_BOX_FORMATTED_AND_TINYMCE) {
|
|
$string = strip_tags_and_attributes($string); // This will allow permitted tags and attributes, no links
|
|
} else {
|
|
$string = escape($string);
|
|
}
|
|
|
|
$string = highlightkeywords($string, $search, $df[$x]['partial_index'], $df[$x]['name'], $df[$x]['indexed']);
|
|
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Formats a string with a collapsible more / less section
|
|
*
|
|
* @param string $string
|
|
* @param integer $max_words_before_more
|
|
* @return string
|
|
*/
|
|
function format_string_more_link($string, $max_words_before_more = 30)
|
|
{
|
|
$words = preg_split('/[\t\f ]/', $string);
|
|
if (count($words) < $max_words_before_more) {
|
|
return $string;
|
|
}
|
|
global $lang;
|
|
$unique_id = uniqid();
|
|
$return_value = "";
|
|
for ($i = 0; $i < count($words); $i++) {
|
|
if ($i > 0) {
|
|
$return_value .= ' ';
|
|
}
|
|
if ($i == $max_words_before_more) {
|
|
$return_value .= '<a id="' . $unique_id . 'morelink" href="#" onclick="jQuery(\'#' . $unique_id . 'morecontent\').show(); jQuery(this).hide();">' .
|
|
strtoupper($lang["action-more"]) . ' ></a><span id="' . $unique_id . 'morecontent" style="display:none;">';
|
|
}
|
|
$return_value .= $words[$i];
|
|
}
|
|
$return_value .= ' <a href="#" onclick="jQuery(\'#' . $unique_id . 'morelink\').show(); jQuery(\'#' . $unique_id . 'morecontent\').hide();">< ' .
|
|
strtoupper($lang["action-less"]) . '</a></span>';
|
|
return $return_value;
|
|
}
|
|
|
|
/**
|
|
* Render a performance footer with metrics.
|
|
*
|
|
* @return void
|
|
*/
|
|
function draw_performance_footer()
|
|
{
|
|
global $config_show_performance_footer,$querycount,$querytime,$querylog,$pagename,$hook_cache_hits,$hook_cache;
|
|
$performance_footer_id = uniqid("performance");
|
|
if ($config_show_performance_footer) {
|
|
# --- If configured (for debug/development only) show query statistics
|
|
|
|
if ($pagename == "collections") { ?>
|
|
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
|
|
<div style="float:left;">
|
|
<?php } else { ?>
|
|
<div style="float:right; margin-right: 10px;">
|
|
<?php } ?>
|
|
|
|
<table class="InfoTable" style="float: right;margin-right: 10px;">
|
|
<tr>
|
|
<td>Page Load</td>
|
|
<td><?php echo show_pagetime();?></td>
|
|
</tr>
|
|
<?php if (isset($hook_cache_hits) && isset($hook_cache)) { ?>
|
|
<tr>
|
|
<td>Hook cache hits</td>
|
|
<td><?php echo $hook_cache_hits;?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Hook cache entries</td>
|
|
<td><?php echo count($hook_cache); ?></td>
|
|
</tr>
|
|
<?php
|
|
} ?>
|
|
<tr>
|
|
<td>Query count</td>
|
|
<td><?php echo $querycount?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Query time</td>
|
|
<td><?php echo round($querytime, 4)?></td>
|
|
</tr>
|
|
<?php $dupes = 0;
|
|
foreach ($querylog as $query => $values) {
|
|
if ($values['dupe'] > 1) {
|
|
$dupes++;
|
|
}
|
|
}
|
|
?>
|
|
<tr>
|
|
<td>Dupes</td>
|
|
<td><?php echo $dupes?></td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan=2>
|
|
<a href="#" onClick="document.getElementById('querylog<?php echo $performance_footer_id?>').style.display='block';return false;">
|
|
<?php echo LINK_CARET ?>details
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<table class="InfoTable" style="float: right;margin-right: 10px;display:none;" id="querylog<?php echo $performance_footer_id?>">
|
|
<?php foreach ($querylog as $query => $details) { ?>
|
|
<tr>
|
|
<td><?php echo escape($query); ?></td>
|
|
</tr>
|
|
<?php } ?>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abstracted mysqli_affected_rows()
|
|
*
|
|
* @return mixed
|
|
*/
|
|
function sql_affected_rows()
|
|
{
|
|
global $db;
|
|
return mysqli_affected_rows($db["read_write"]);
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the ImageMagick utilities such as 'convert'.
|
|
*
|
|
* @param string $utilityname
|
|
* @param string $exeNames
|
|
* @param string $checked_path
|
|
* @return string
|
|
*/
|
|
function get_imagemagick_path($utilityname, $exeNames, &$checked_path)
|
|
{
|
|
global $imagemagick_path;
|
|
if (!isset($imagemagick_path)) {
|
|
# ImageMagick convert path not configured.
|
|
return false;
|
|
}
|
|
$path = get_executable_path($imagemagick_path, $exeNames, $checked_path);
|
|
if ($path === false) {
|
|
# Support 'magick' also, ie. ImageMagick 7+
|
|
return get_executable_path(
|
|
$imagemagick_path,
|
|
array("unix" => "magick", "win" => "magick.exe"),
|
|
$checked_path
|
|
) . ' ' . $utilityname;
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Returns the full path to a utility, if installed or FALSE otherwise.
|
|
* Note: this function doesn't check that the utility is working.
|
|
*
|
|
* @uses get_imagemagick_path()
|
|
* @uses get_executable_path()
|
|
*
|
|
* @param string $utilityname
|
|
* @param string $checked_path
|
|
*
|
|
* @return string|boolean Returns full path to utility tool or FALSE
|
|
*/
|
|
function get_utility_path($utilityname, &$checked_path = null)
|
|
{
|
|
global $ghostscript_path, $ghostscript_executable, $ffmpeg_path, $exiftool_path, $antiword_path, $pdftotext_path,
|
|
$blender_path, $archiver_path, $archiver_executable, $python_path, $fits_path;
|
|
|
|
$checked_path = null;
|
|
|
|
switch (strtolower($utilityname)) {
|
|
case 'im-convert':
|
|
return get_imagemagick_path(
|
|
'convert',
|
|
array(
|
|
'unix' => 'convert',
|
|
'win' => 'convert.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'im-identify':
|
|
return get_imagemagick_path(
|
|
'identify',
|
|
array(
|
|
'unix' => 'identify',
|
|
'win' => 'identify.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'im-composite':
|
|
return get_imagemagick_path(
|
|
'composite',
|
|
array(
|
|
'unix' => 'composite',
|
|
'win' => 'composite.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'im-mogrify':
|
|
return get_imagemagick_path(
|
|
'mogrify',
|
|
array(
|
|
'unix' => 'mogrify',
|
|
'win' => 'mogrify.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'ghostscript':
|
|
// Ghostscript path not configured
|
|
if (!isset($ghostscript_path)) {
|
|
return false;
|
|
}
|
|
|
|
// Ghostscript executable not configured
|
|
if (!isset($ghostscript_executable)) {
|
|
return false;
|
|
}
|
|
|
|
// Note that $check_exe is set to true. In that way get_utility_path()
|
|
// becomes backwards compatible with get_ghostscript_command()
|
|
$path = get_executable_path(
|
|
$ghostscript_path,
|
|
array(
|
|
'unix' => $ghostscript_executable,
|
|
'win' => $ghostscript_executable
|
|
),
|
|
$checked_path,
|
|
true
|
|
);
|
|
if ($path === false) {
|
|
return false;
|
|
}
|
|
return $path . ' -dPARANOIDSAFER';
|
|
|
|
case 'ffmpeg':
|
|
// FFmpeg path not configured
|
|
if (!isset($ffmpeg_path)) {
|
|
return false;
|
|
}
|
|
|
|
$return = get_executable_path(
|
|
$ffmpeg_path,
|
|
array(
|
|
'unix' => 'ffmpeg',
|
|
'win' => 'ffmpeg.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
// Support 'avconv' as well
|
|
if (false === $return) {
|
|
return get_executable_path(
|
|
$ffmpeg_path,
|
|
array(
|
|
'unix' => 'avconv',
|
|
'win' => 'avconv.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
}
|
|
return $return;
|
|
|
|
case 'ffprobe':
|
|
// FFmpeg path not configured
|
|
if (!isset($ffmpeg_path)) {
|
|
return false;
|
|
}
|
|
|
|
$return = get_executable_path(
|
|
$ffmpeg_path,
|
|
array(
|
|
'unix' => 'ffprobe',
|
|
'win' => 'ffprobe.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
// Support 'avconv' as well
|
|
if (false === $return) {
|
|
return get_executable_path(
|
|
$ffmpeg_path,
|
|
array(
|
|
'unix' => 'avprobe',
|
|
'win' => 'avprobe.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
}
|
|
return $return;
|
|
|
|
case 'exiftool':
|
|
global $exiftool_global_options;
|
|
|
|
$path = get_executable_path(
|
|
$exiftool_path,
|
|
array(
|
|
'unix' => 'exiftool',
|
|
'win' => 'exiftool.exe'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
if ($path === false) {
|
|
return false;
|
|
}
|
|
return $path . " {$exiftool_global_options} ";
|
|
|
|
case 'antiword':
|
|
if (!isset($antiword_path) || $antiword_path === '') {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$antiword_path,
|
|
[
|
|
'unix' => 'antiword',
|
|
'win' => 'antiword.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
|
|
case 'pdftotext':
|
|
if (!isset($pdftotext_path) || $pdftotext_path === '') {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$pdftotext_path,
|
|
[
|
|
'unix' => 'pdftotext',
|
|
'win' => 'pdftotext.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
|
|
case 'blender':
|
|
if (!isset($GLOBALS['blender_path']) || $GLOBALS['blender_path'] === '') {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$GLOBALS['blender_path'],
|
|
[
|
|
'unix' => 'blender',
|
|
'win' => 'blender.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
|
|
case 'archiver':
|
|
// Archiver path not configured
|
|
if (!isset($archiver_path)) {
|
|
return false;
|
|
}
|
|
|
|
// Archiver executable not configured
|
|
if (!isset($archiver_executable)) {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$archiver_path,
|
|
array(
|
|
'unix' => $archiver_executable,
|
|
'win' => $archiver_executable
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'python':
|
|
case 'opencv':
|
|
// Python path not configured
|
|
if (!isset($python_path) || '' == $python_path) {
|
|
return false;
|
|
}
|
|
|
|
$python3 = get_executable_path(
|
|
$python_path,
|
|
[
|
|
'unix' => 'python3',
|
|
'win' => 'python.exe'
|
|
],
|
|
$checked_path,
|
|
true
|
|
);
|
|
|
|
return $python3 ?: get_executable_path(
|
|
$python_path,
|
|
[
|
|
'unix' => 'python',
|
|
'win' => 'python.exe'
|
|
],
|
|
$checked_path,
|
|
true
|
|
);
|
|
|
|
case 'fits':
|
|
// FITS path not configured
|
|
if (!isset($fits_path) || '' == $fits_path) {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$fits_path,
|
|
array(
|
|
'unix' => 'fits.sh',
|
|
'win' => 'fits.bat'
|
|
),
|
|
$checked_path
|
|
);
|
|
|
|
case 'php':
|
|
global $php_path;
|
|
|
|
if (!isset($php_path) || $php_path == '') {
|
|
return false;
|
|
}
|
|
|
|
$executable = array(
|
|
'unix' => 'php',
|
|
'win' => 'php.exe'
|
|
);
|
|
|
|
return get_executable_path($php_path, $executable, $checked_path);
|
|
|
|
case 'unoconv':
|
|
if (
|
|
// On Windows, the utility is available only via Python's package
|
|
($GLOBALS['config_windows'] && (!isset($GLOBALS['unoconv_python_path']) || $GLOBALS['unoconv_python_path'] === ''))
|
|
|| (!isset($GLOBALS['unoconv_path']) || $GLOBALS['unoconv_path'] === '')
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$GLOBALS['config_windows'] ? $GLOBALS['unoconv_python_path'] : $GLOBALS['unoconv_path'],
|
|
[
|
|
'unix' => 'unoconv',
|
|
'win' => 'python.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
|
|
case 'unoconvert':
|
|
return get_executable_path(
|
|
$GLOBALS['config_windows'] ? $GLOBALS['unoconv_python_path'] : $GLOBALS['unoconv_path'],
|
|
[
|
|
'unix' => 'unoconvert',
|
|
'win' => 'unoconvert.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
|
|
case 'calibre':
|
|
if (!isset($GLOBALS['calibre_path']) || $GLOBALS['calibre_path'] === '') {
|
|
return false;
|
|
}
|
|
|
|
return get_executable_path(
|
|
$GLOBALS['calibre_path'],
|
|
[
|
|
'unix' => 'ebook-convert',
|
|
'win' => 'ebook-convert.exe'
|
|
],
|
|
$checked_path
|
|
);
|
|
}
|
|
|
|
// No utility path found
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get full path to utility
|
|
*
|
|
* @param string $path
|
|
* @param array $executable
|
|
* @param string $checked_path
|
|
* @param boolean $check_exe
|
|
*
|
|
* @return string|boolean
|
|
*/
|
|
function get_executable_path($path, $executable, &$checked_path, $check_exe = false)
|
|
{
|
|
global $config_windows;
|
|
|
|
$os = php_uname('s');
|
|
|
|
if ($config_windows || stristr($os, 'windows')) {
|
|
$checked_path = $path . "\\" . $executable['win'];
|
|
|
|
if (file_exists($checked_path)) {
|
|
return escapeshellarg($checked_path) . hook('executable_add', '', array($path, $executable, $checked_path, $check_exe));
|
|
}
|
|
|
|
// Also check the path with a suffixed ".exe"
|
|
if ($check_exe) {
|
|
$checked_path_without_exe = $checked_path;
|
|
$checked_path = $path . "\\" . $executable['win'] . '.exe';
|
|
|
|
if (file_exists($checked_path)) {
|
|
return escapeshellarg($checked_path) . hook('executable_add', '', array($path, $executable, $checked_path, $check_exe));
|
|
}
|
|
|
|
// Return the checked path without the suffixed ".exe"
|
|
$checked_path = $checked_path_without_exe;
|
|
}
|
|
} else {
|
|
$checked_path = stripslashes($path) . '/' . $executable['unix'];
|
|
|
|
if (file_exists($checked_path)) {
|
|
return escapeshellarg($checked_path) . hook('executable_add', '', array($path, $executable, $checked_path, $check_exe));
|
|
}
|
|
}
|
|
|
|
// No path found
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clean up the resource data cache to keep within $cache_array_limit
|
|
*
|
|
* @return void
|
|
*/
|
|
function truncate_cache_arrays()
|
|
{
|
|
$cache_array_limit = 2000;
|
|
if (count($GLOBALS['get_resource_data_cache']) > $cache_array_limit) {
|
|
$GLOBALS['get_resource_data_cache'] = array();
|
|
// future improvement: get rid of only oldest, instead of clearing all?
|
|
// this would require a way to guage the age of the entry.
|
|
}
|
|
if (count($GLOBALS['get_resource_path_fpcache']) > $cache_array_limit) {
|
|
$GLOBALS['get_resource_path_fpcache'] = array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Work out of a string is likely to be in HTML format.
|
|
*
|
|
* @param mixed $string
|
|
* @return boolean
|
|
*/
|
|
function is_html($string)
|
|
{
|
|
return preg_match("/<[^<]+>/", $string, $m) != 0;
|
|
}
|
|
|
|
/**
|
|
* Set a cookie.
|
|
*
|
|
* Note: The argument $daysexpire is not the same as the argument $expire in the PHP internal function setcookie.
|
|
*
|
|
* @param string $name
|
|
* @param string $value
|
|
* @param integer $daysexpire
|
|
* @param string $path
|
|
* @param string $domain
|
|
* @param boolean $secure
|
|
* @param boolean $httponly
|
|
* @return void
|
|
*/
|
|
function rs_setcookie($name, $value, $daysexpire = 0, $path = "", $domain = "", $secure = false, $httponly = true)
|
|
{
|
|
|
|
if (!is_string_loose($value)) {
|
|
return;
|
|
}
|
|
|
|
global $baseurl_short, $baseurl_short;
|
|
if ($path == "") {
|
|
$path = $baseurl_short;
|
|
}
|
|
|
|
if (php_sapi_name() == "cli") {
|
|
return true;
|
|
} # Bypass when running from the command line (e.g. for the test scripts).
|
|
|
|
if ($daysexpire == 0) {
|
|
$expire = 0;
|
|
} else {
|
|
$expire = time() + (3600 * 24 * $daysexpire);
|
|
}
|
|
|
|
if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] === getservbyname("https", "tcp"))) {
|
|
$secure = true;
|
|
}
|
|
|
|
// Set new cookie, first remove any old previously set pages cookies to avoid clashes;
|
|
setcookie($name, "", time() - 3600, $path . "pages", $domain, $secure, $httponly);
|
|
setcookie($name, (string) $value, (int) $expire, $path, $domain, $secure, $httponly);
|
|
}
|
|
|
|
/**
|
|
* Get an array of all the states that a user has edit access to
|
|
*
|
|
* @param integer $userref
|
|
* @return array
|
|
*/
|
|
function get_editable_states($userref)
|
|
{
|
|
global $additional_archive_states, $lang;
|
|
if ($userref == -1) {
|
|
return false;
|
|
}
|
|
$editable_states = array();
|
|
$x = 0;
|
|
for ($n = -2; $n <= 3; $n++) {
|
|
if (checkperm("e" . $n)) {
|
|
$editable_states[$x]['id'] = $n;
|
|
$editable_states[$x]['name'] = $lang["status" . $n];
|
|
$x++;
|
|
}
|
|
}
|
|
foreach ($additional_archive_states as $additional_archive_state) {
|
|
if (checkperm("e" . $additional_archive_state)) {
|
|
$editable_states[$x]['id'] = $additional_archive_state;
|
|
$editable_states[$x]['name'] = $lang["status" . $additional_archive_state];
|
|
$x++;
|
|
}
|
|
}
|
|
return $editable_states;
|
|
}
|
|
|
|
/**
|
|
* Returns true if $html is valid HTML, otherwise an error string describing the problem.
|
|
*
|
|
* @param mixed $html
|
|
* @return bool|string
|
|
*/
|
|
function validate_html($html)
|
|
{
|
|
$parser = xml_parser_create();
|
|
xml_parse_into_struct($parser, "<div>" . str_replace("&", "&", $html) . "</div>", $vals, $index);
|
|
$errcode = xml_get_error_code($parser);
|
|
if ($errcode !== 0) {
|
|
$line = xml_get_current_line_number($parser);
|
|
|
|
$error = escape(xml_error_string($errcode)) . "<br />Line: " . $line . "<br /><br />";
|
|
$s = explode("\n", $html);
|
|
$error .= "<pre>" ;
|
|
$error .= isset($s[$line - 2]) ? trim(escape($s[$line - 2])) . "<br />" : "";
|
|
$error .= isset($s[$line - 1]) ? "<strong>" . trim(escape($s[$line - 1])) . "</strong><br />" : "";
|
|
$error .= isset($s[$line]) ? trim(escape($s[$line])) . "<br />" : "";
|
|
$error .= "</pre>";
|
|
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);
|
|
?>
|
|
<input type="hidden" name="<?php echo $CSRF_token_identifier; ?>" value="<?php echo $token; ?>">
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render the CSRF Token for AJAX use
|
|
*
|
|
* @uses generateCSRFToken()
|
|
*
|
|
* @param string $form_id The id/ name attribute of the form or just the calling function for this type of request
|
|
*
|
|
* @return string
|
|
*/
|
|
function generateAjaxToken($form_id)
|
|
{
|
|
global $CSRF_enabled, $CSRF_token_identifier, $usersession;
|
|
|
|
if (!$CSRF_enabled) {
|
|
return "";
|
|
}
|
|
|
|
$token = generateCSRFToken($usersession, $form_id);
|
|
|
|
return "{$CSRF_token_identifier}: \"{$token}\"";
|
|
}
|
|
|
|
/**
|
|
* Create a CSRF token as a JS object
|
|
*
|
|
* @param string $name The name of the token identifier (e.g API function called)
|
|
* @return string JS object with CSRF data (identifier & token) if CSRF is enabled, empty object otherwise
|
|
*/
|
|
function generate_csrf_js_object(string $name): string
|
|
{
|
|
return $GLOBALS['CSRF_enabled']
|
|
? json_encode([$GLOBALS['CSRF_token_identifier'] => 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>')) {
|
|
$body_o_tag_pos = strpos($output_html, '<body>');
|
|
$body_c_tag_pos = strpos($output_html, '</body>');
|
|
|
|
$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 <p> 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>')) {
|
|
$body_o_tag_pos = strpos($html, '<body>');
|
|
$body_c_tag_pos = strpos($html, '</body>');
|
|
|
|
$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 a <p> tag
|
|
$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 <p> and </p> tags.
|
|
*/
|
|
function strip_paragraph_tags(string $text): string
|
|
{
|
|
// Match either <p> exactly, or <p ... attributes> at the start of $text
|
|
// Match </p> exactly at the end of $text
|
|
// Everything else between is lazily matched across multiple lines,
|
|
// so remains untouched
|
|
return preg_replace('/^<p\b[^>]*>(.*?)<\/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']}<br /> {$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 '<p>–</p>';
|
|
}
|
|
$newWidth = $size['width'];
|
|
$newHeight = round(($imageHeight * $newWidth + $imageWidth - 1) / $imageWidth);
|
|
} else {
|
|
// portrait or square
|
|
if ($imageHeight == 0) {
|
|
return '<p>–</p>';
|
|
}
|
|
$newHeight = $size['height'];
|
|
$newWidth = round(($imageWidth * $newHeight + $imageHeight - 1) / $imageHeight);
|
|
}
|
|
}
|
|
|
|
$output = sprintf(
|
|
'<p>%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 .= '</p>';
|
|
|
|
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(
|
|
'<p>%1$s %2$s × %3$s %2$s %4$s %5$s %6$s</p>',
|
|
escape($dpi_w),
|
|
escape($dpi_unit),
|
|
escape($dpi_h),
|
|
escape($lang['at-resolution']),
|
|
escape($dpi),
|
|
escape($lang['ppi']),
|
|
);
|
|
}
|
|
|
|
if (isset($size["filesize"])) {
|
|
$output .= sprintf('<p>%s</p>', 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<string> List of keys to extract from the values
|
|
*/
|
|
function get_sub_array_with(array $keys): callable
|
|
{
|
|
return fn(array $input): array => array_intersect_key($input, array_flip($keys));
|
|
}
|
|
|
|
/**
|
|
* Server side check to backup front end javascript validation.
|
|
*
|
|
* @param string $password Password supplied when creating or editing external share.
|
|
|
|
*/
|
|
function enforceSharePassword(string $password): void
|
|
{
|
|
global $share_password_required, $lang;
|
|
if ($share_password_required && trim($password) === '') {
|
|
exit(escape($lang["error-permissiondenied"]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to call the JS CentralSpaceLoad().
|
|
* @return never
|
|
*/
|
|
function js_call_CentralSpaceLoad(string $url)
|
|
{
|
|
exit("<script>CentralSpaceLoad('{$url}');</script>");
|
|
}
|
|
|
|
/**
|
|
* Get expiration date of a given PEM certificate
|
|
*
|
|
* @param string $cert Certificate text
|
|
*
|
|
* @return string|bool Expiry date. False if unable to parse certificate
|
|
*
|
|
*/
|
|
function getCertificateExpiry(string $cert)
|
|
{
|
|
/* Construct a PEM formatted certificate */
|
|
$pemCert = "-----BEGIN CERTIFICATE-----\n" . chunk_split($cert, 64) . "-----END CERTIFICATE-----\n";
|
|
$data = openssl_x509_parse($pemCert);
|
|
return $data ? date('Y-m-d H:i:s', $data['validTo_time_t']) : false;
|
|
}
|
|
|
|
/**
|
|
* Is the provided colour a valid CSS colour and therefore safe to display?
|
|
*
|
|
* @param string $color The colour string
|
|
* @return boolean True if a valid colour, false if not
|
|
*/
|
|
function isValidCssColor(string $colour)
|
|
{
|
|
$regex = '/^(#([a-fA-F0-9]{3}|[a-fA-F0-9]{4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})'
|
|
. '|rgb\((\d{1,3},\s?){2}\d{1,3}\)'
|
|
. '|rgba\((\d{1,3},\s?){3}(0|0?\.\d+|1)\)'
|
|
. '|hsl\(\d{1,3},\s?\d{1,3}%,\s?\d{1,3}%\)'
|
|
. '|hsla\(\d{1,3},\s?\d{1,3}%,\s?\d{1,3}%,\s?(0|0?\.\d+|1)\)'
|
|
. '|[a-zA-Z]+)$/';
|
|
return preg_match($regex, trim($colour)) === 1;
|
|
}
|
|
|
|
/**
|
|
* Convert HSL to RGB.
|
|
*
|
|
* @param float $h Hue (0-360).
|
|
* @param float $s Saturation (0-1).
|
|
* @param float $l Lightness (0-1).
|
|
* @return array Array with RGB values (0-255).
|
|
*/
|
|
function hslToRgb($h, $s, $l)
|
|
{
|
|
$c = (1 - abs(2 * $l - 1)) * $s;
|
|
$x = $c * (1 - abs(fmod($h / 60, 2) - 1));
|
|
$m = $l - $c / 2;
|
|
|
|
if ($h < 60) {
|
|
$r = $c;
|
|
$g = $x;
|
|
$b = 0;
|
|
} elseif ($h < 120) {
|
|
$r = $x;
|
|
$g = $c;
|
|
$b = 0;
|
|
} elseif ($h < 180) {
|
|
$r = 0;
|
|
$g = $c;
|
|
$b = $x;
|
|
} elseif ($h < 240) {
|
|
$r = 0;
|
|
$g = $x;
|
|
$b = $c;
|
|
} elseif ($h < 300) {
|
|
$r = $x;
|
|
$g = 0;
|
|
$b = $c;
|
|
} else {
|
|
$r = $c;
|
|
$g = 0;
|
|
$b = $x;
|
|
}
|
|
|
|
return [
|
|
(int)(($r + $m) * 255),
|
|
(int)(($g + $m) * 255),
|
|
(int)(($b + $m) * 255)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check TinyMCE plugin list against an array of valid options
|
|
* The passed list is checked against TINYMCE_VALID_PLUGINS.
|
|
* The autoresize plugin is also included by default.
|
|
*
|
|
* @param string $plugins A comma-separated list of plugins for TinyMCE
|
|
* @return string The list of plugins with any invalid options removed
|
|
*/
|
|
function check_tinymce_plugins(string $plugins = ""): string
|
|
{
|
|
|
|
// Ensure autoresize plugin is included so min_height can be used
|
|
if (mb_strpos($plugins, 'autoresize') === false) {
|
|
$plugins .= ',autoresize';
|
|
}
|
|
|
|
$configured_plugins = array_map('trim', explode(',', $plugins));
|
|
$valid_plugins = array_intersect_key(TINYMCE_VALID_PLUGINS, array_flip($configured_plugins));
|
|
|
|
return implode(', ', array_keys($valid_plugins));
|
|
}
|
|
|
|
/**
|
|
* Check TinyMCE toolbar configuration to ensure
|
|
* it contains only alphanumeric characters, spaces and
|
|
* the pipe (|) symbol.
|
|
*
|
|
* @param string $toolbar The requested configuration for the toolbar
|
|
* @return string The configured toolbar with any invalid characters removed
|
|
*/
|
|
function check_tinymce_toolbar(string $toolbar = ""): string
|
|
{
|
|
//Remove anything non-alphanumeric, pipes or spaces
|
|
return preg_replace('/[^a-zA-Z0-9|\s]/', '', $toolbar);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the number of resources in the system that are not ingested into the filestore i.e. with 'file_path' set
|
|
*
|
|
* @return int Number of non-ingested resources in the system
|
|
*/
|
|
function get_non_ingested_resources(): int
|
|
{
|
|
return ps_value("SELECT COUNT(*) value FROM resource WHERE file_path IS NOT NULL AND file_path <> ''", [], 0);
|
|
}
|
|
|
|
/**
|
|
* Return URL of the application favicon
|
|
*
|
|
* @return string Favicon URL
|
|
*/
|
|
function get_favicon_url(): string
|
|
{
|
|
global $header_favicon, $baseurl, $storageurl;
|
|
|
|
if ($header_favicon == '') {
|
|
$header_favicon = 'gfx/interface/favicon.png';
|
|
}
|
|
|
|
$favicon = "{$baseurl}/{$header_favicon}";
|
|
|
|
if (strpos($header_favicon, '[storage_url]') !== false) {
|
|
$favicon = str_replace('[storage_url]', $storageurl, $header_favicon);
|
|
}
|
|
|
|
return $favicon;
|
|
}
|