first commit

This commit is contained in:
2025-07-18 16:20:14 +07:00
commit 98af45c018
16382 changed files with 3148096 additions and 0 deletions

2
include/.htaccess Executable file
View File

@@ -0,0 +1,2 @@
Order deny,allow
Deny from all

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Montala\ResourceSpace;
use Exception;
final class CommandPlaceholderArg
{
/**
* A placeholders' value representing ONE command argument value (highly contextual).
*/
private string $value;
/**
* Constructor
*
* @param string $value The actual placeholder value
* @param null|callable $validator Use null for the default one, otherwise any callable where the value to be
* tested is the first argument.
*/
public function __construct(string $value, ?callable $validator)
{
$validator ??= [__CLASS__, 'defaultValidator'];
if ($validator($value)) {
$this->value = $value;
return;
}
debug('Invalid placeholder argument value: ' . $value);
throw new Exception('Invalid placeholder argument value: ' . $value);
}
/**
* Input validation helper function for determining if there are any blocked metacharacters which could be used to
* exploit OS command injections.
*/
public static function defaultValidator(string $val): bool
{
// ; & | $ > < ` \ ! ' " ( ) including white space
return !preg_match('/[;&|\$><`\\!\'"()\s]/', $val);
}
public function __toString(): string
{
return $this->value;
}
/**
* Basic function to enable bypassing of any validation of the provided value.
* Only to be used when the value has been constructed without any unpredictable user input or
* has been thoroughly pre-sanitized
*/
public static function alwaysValid(string $val): bool
{
return true;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Montala\ResourceSpace\Utils\Rector;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Name;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class EscapeLanguageStringsRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change instances where $lang is directly echoed without escaping for XSS.'
[new CodeSample('echo $lang["string"];', 'escape($lang["string"]);')]
);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
// What node types are we looking for? Pick from https://github.com/rectorphp/php-parser-nodes-docs/
return [Echo_::class];
}
/**
* @param Echo_ $node
*/
public function refactor(Node $node): ?Node
{
$expr = $node->exprs[0];
$var_name = $expr->var->name;
$dim_value = $expr->dim->value;
// Only look for this use case form: echo $lang['home'];
if (!(is_a($expr, ArrayDimFetch::class, false) && $var_name === 'lang')) {
return null;
}
// Only load the en version because we assume other translations follow its format (e.g. if a string contains
// HTML tags, all the other translations should do too).
require dirname(__DIR__) . '/languages/en.php';
$plugins_path = dirname(__DIR__) . '/plugins';
$valid_plugins = scandir($plugins_path) ?: [];
$plugin = array_pop(
array_reverse(
array_diff(explode('/', $this->file->getFilePath()), explode('/', $plugins_path))
)
);
$plugin_en_file = "{$plugins_path}/{$plugin}/languages/en.php";
if (in_array($plugin, $valid_plugins) && file_exists($plugin_en_file)) {
require $plugin_en_file;
}
if (!isset($lang[$dim_value])) {
return null;
}
$fct_name = $lang[$dim_value] !== strip_tags($lang[$dim_value]) ? 'strip_tags_and_attributes' : 'escape';
return new Echo_([new FuncCall(new Name($fct_name), [new Arg($expr)])]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Montala\ResourceSpace\Utils\Rector;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
// todo: make it configurable
final class ReplaceFunctionCallRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change function calls from htmlspecialchars to escape.',
[new CodeSample('htmlspecialchars("string");', 'escape("string");')]
);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
// what node types are we looking for?
// pick from https://github.com/rectorphp/php-parser-nodes-docs/
return [FuncCall::class];
}
/**
* @param FuncCall $node
*/
public function refactor(Node $node): ?Node
{
$fct_name = $this->getName($node->name);
if ($fct_name === null) {
return null;
}
return $fct_name === 'htmlspecialchars' && !$node->isFirstClassCallable()
? new FuncCall(new Name('escape'), [$node->getArgs()[0]])
: null;
}
}

View File

@@ -0,0 +1,283 @@
<?php
/**
* Retrieve a list of user actions for the My Actions area.
*
* @param boolean $countonly Return the count of actions instead of the actions themselves
* @param string $type Filter the actions based on action type
* The available inputs are:
* resourcereview
* resourcerequest
* userrequest
* @param string $order_by
* @param string $sort
*
* @return mixed Count or array of actions
*/
function get_user_actions($countonly = false, $type = "", $order_by = "date", $sort = "DESC")
{
global $default_display, $list_display_fields, $search_all_workflow_states, $actions_approve_hide_groups, $userref, $usergroup,
$actions_resource_requests, $actions_account_requests, $view_title_field, $actions_on, $messages_actions_usergroup, $actions_notify_states;
// Make sure all states are excluded if they had the legacy option $actions_resource_review set to false.
get_config_option(['user' => $userref, 'usergroup' => $usergroup], 'actions_resource_review', $actions_resource_review, true);
if (!$actions_resource_review) {
$actions_notify_states = "";
}
$actionsql = new PreparedStatementQuery();
$filtered = $type != "";
if (!$actions_on) {
return array();
}
if ((!$filtered || 'resourcereview' == $type) && trim($actions_notify_states) != "") {
$search_all_workflow_states = false;
$default_display = $list_display_fields;
if (is_int_loose($view_title_field)) {
$generated_title_field = "field" . $view_title_field;
} else {
$generated_title_field = "''";
}
# Function get_editable_resource_sql() now returns a query object
$editable_resource_query = get_editable_resource_sql();
$actionsql->sql .= "SELECT creation_date as date,ref, created_by as user, "
. $generated_title_field . " as description, 'resourcereview' as type FROM (" . $editable_resource_query->sql . ") resources" ;
$actionsql->parameters = array_merge($actionsql->parameters, $editable_resource_query->parameters);
}
if (checkperm("R") && $actions_resource_requests && (!$filtered || 'resourcerequest' == $type)) {
# This get_requests call now returns a query object with two properties; sql string and parameters array
$request_query = get_requests(true, true, true);
$actionsql->sql .= (($actionsql->sql != "") ? " UNION " : "") . "SELECT created
as date,ref, user, substring(comments,21) as description,'resourcerequest' as type FROM (" . $request_query->sql . ") requests";
$actionsql->parameters = array_merge($actionsql->parameters, $request_query->parameters);
}
if (checkperm("u") && $actions_account_requests && (!$filtered || 'userrequest' == $type)) {
$availgroups = get_usergroups(true);
$get_groups = implode(",", array_diff(array_column($availgroups, "ref"), explode(",", $actions_approve_hide_groups)));
$account_requests_query = get_users($get_groups, "", "u.created", true, -1, 0, true, "u.ref,u.created,u.fullname,u.email,u.username, u.comments");
$actionsql->sql .= (($actionsql->sql != "") ? " UNION " : "") . "SELECT created
as date,ref,ref as user,comments as description,'userrequest' as type FROM (" . $account_requests_query->sql . ") users";
$actionsql->parameters = array_merge($actionsql->parameters, $account_requests_query->parameters);
}
# Following hook now returns a query object
$hookactionsql = hook("addtoactions");
if ($hookactionsql != false) {
if ($actionsql->sql != "") {
$actionsql->sql .= " UNION ";
}
$actionsql->sql .= $hookactionsql->sql;
$actionsql->parameters = array_merge($actionsql->parameters, $hookactionsql->parameters);
}
if ($actionsql->sql == "") {
return $countonly ? 0 : array();
}
if ($countonly) {
return ps_value("SELECT COUNT(*) value FROM (" . $actionsql->sql . ") allactions", $actionsql->parameters, 0);
} else {
$final_action_sql = $actionsql;
$final_action_sql->sql = "SELECT date, allactions.ref,user.fullname as
user,"
. ($messages_actions_usergroup ? "usergroup.name as usergroup," : "") .
" description,
type FROM (" . $actionsql->sql . ") allactions LEFT JOIN user ON
allactions.user=user.ref"
. ($messages_actions_usergroup ? " LEFT JOIN usergroup ON
user.usergroup=usergroup.ref" : "") .
" ORDER BY " . $order_by . " " . $sort;
}
return ps_query($final_action_sql->sql, $final_action_sql->parameters);
}
/**
* Return an SQL statement to find all editable resources in $actions_notify_states.
*
* @return mixed
*/
function get_editable_resource_sql()
{
global $actions_notify_states, $actions_resource_types_hide, $default_display, $list_display_fields, $search_all_workflow_states;
$default_display = $list_display_fields;
$search_all_workflow_states = false;
$rtypes = get_resource_types();
$searchable_restypes = implode(",", array_diff(array_column($rtypes, "ref"), explode(",", $actions_resource_types_hide)));
return do_search("", $searchable_restypes, 'resourceid', $actions_notify_states, -1, 'desc', false, 0, false, false, '', false, false, false, true, true);
}
/**
* Get recent user actions, optionally for all users. For use by action notifications cron job.
*
* @param int $minutes Return actions that were created in the last $minutes minutes
* @param bool $allusers Return actions for all users? If false, or if the current user does not have
* the 'a' permission and the current script is not running from CLI then only the currently logged on
* user's actions will be returned
*
* @return array An array with the user id as the index and the following arrays of sub elements.
* Included columns are as per get_user_actions()
* - resourcerequest - array of resource requests
* - userrequest - array of user requests
* - resourcereview - array of resources to reviewdescription)
*/
function get_user_actions_recent(int $minutes, bool $allusers): array
{
debug_function_call(__FUNCTION__, func_get_args());
global $view_title_field, $userref;
$newactions = [];
// Find all resources that have changed archive state in the given number of minutes
if (is_int_loose($view_title_field)) {
$generated_title_field = "field" . $view_title_field;
} else {
$generated_title_field = "r.ref";
}
$sql = "SELECT r.ref, r.archive, r.resource_type, rl.user, rl.date AS date, $generated_title_field AS description, 'resourcereview' AS type
FROM resource_log rl
LEFT JOIN resource_log rl2 ON (rl.resource=rl2.resource AND rl.ref<rl2.ref)
LEFT JOIN resource r ON rl.resource=r.ref
WHERE rl2.ref IS NULL
AND rl.type IN('" . LOG_CODE_STATUS_CHANGED . "','" . LOG_CODE_CREATED . "')
AND TIMESTAMPDIFF(MINUTE,rl.date,NOW())<?
ORDER BY rl.ref DESC";
$params = ["i",$minutes];
$newactions["resourcereview"] = ps_query($sql, $params);
// Find all resource requests created in the given number of minutes
$sql = "SELECT r.ref, r.user, r.created AS date, r.expires, r.comments AS description, r.assigned_to, 'resourcerequest' as type
FROM request r
WHERE status = 0
AND TIMESTAMPDIFF(MINUTE,created,NOW())<?
ORDER BY r.ref ASC";
$params = ["i",$minutes];
$newactions["resourcerequest"] = ps_query($sql, $params);
// Find all account requests created in the last XX minutes
$sql = "SELECT ref, ref as user, created AS date, comments AS description, usergroup, 'userrequest' as type
FROM user
WHERE approved = 0
AND TIMESTAMPDIFF(MINUTE,created,NOW())<?
ORDER BY ref ASC";
$params = ["i",$minutes];
$newactions["userrequest"] = ps_query($sql, $params);
// Any actions that add actions to the array using the hook below should return an element including the function name, parameters and required value to be returned for a user to be able to see the action
// e.g.
// $newactions["access_callback"] =
// ["function"=>"get_edit_access",
// "parameters => 12345,
// "required => true,
// ]
$hookactions = hook("user_actions_recent", "", [$minutes,$newactions]);
if ($hookactions != false) {
$newactions = $hookactions;
}
$userrecent = [];
if ($allusers) {
$action_notify_users = get_users_by_preference("user_pref_new_action_emails", "1");
foreach ($action_notify_users as $action_notify_user) {
$userrecent[$action_notify_user] = actions_filter_by_user($action_notify_user, $newactions);
}
} else {
$userrecent[$userref] = actions_filter_by_user($userref, $newactions);
}
return $userrecent;
}
/**
* Filter actions in the provided array to return only those applicable to the given user
*
* @param int $actionuser User ref to get actions for
* @param array $actions Array of actions as returned by get_user_actions_recent()
*
* @return array Subset of actions for the given user as would be provided by get_user_actions()
*
*/
function actions_filter_by_user(int $actionuser, array $actions): array
{
debug_function_call(__FUNCTION__, func_get_args());
global $userref, $usergroup, $actions_resource_requests, $actions_account_requests, $actions_approve_hide_groups;
$return = [];
if (!isset($userref) || $actionuser != $userref) {
$saved_user = $userref ?? 0;
$actionuserdata = get_user($actionuser);
setup_user($actionuserdata);
}
foreach ($actions as $actiontype => $typeactions) {
switch ($actiontype) {
case "resourcereview":
get_config_option(['user' => $userref, 'usergroup' => $usergroup], 'actions_resource_review', $actions_resource_review, true);
if (!$actions_resource_review) {
$arrnotifystates = [];
} else {
get_config_option(['user' => $userref, 'usergroup' => $usergroup], "actions_notify_states", $notifystates);
if (is_null($notifystates)) {
$arrnotifystates = get_default_notify_states();
} else {
$arrnotifystates = explode(",", $notifystates);
}
get_config_option(['user' => $userref, 'usergroup' => $usergroup], "actions_resource_types_hide", $ignoretypes, "");
$arrignoretypes = explode(",", $ignoretypes);
}
foreach ($typeactions as $typeaction) {
if (
in_array($typeaction["archive"], $arrnotifystates)
&& !in_array($typeaction["resource_type"], $arrignoretypes)
&& get_edit_access($typeaction["ref"])
&& $typeaction["user"] != $actionuser // Filter out if the user changed the state themselves
) {
$return["resourcereview"][] = $typeaction;
}
}
break;
case "resourcerequest":
if ($actions_resource_requests) {
foreach ($typeactions as $typeaction) {
if (resource_request_visible($typeaction)) {
$return["resourcerequest"][] = $typeaction;
}
}
}
break;
case "userrequest":
if (checkperm("u") && $actions_account_requests) {
foreach ($typeactions as $typeaction) {
if (checkperm_user_edit($typeaction["ref"])) {
$return["userrequest"][] = $typeaction;
}
}
}
break;
default;
// Handle any actions added by plugins
foreach ($typeactions as $typeaction) {
if (
isset($typeaction["access_callback"])
&& call_user_func_array($typeaction["access_callback"]["function"], $typeaction["access_callback"]["parameters"]) == $typeaction["access_callback"]["required"]
) {
$return["userrequest"][] = $typeaction;
}
}
break;
}
}
if (isset($saved_user) && $saved_user != 0) {
$saveduserdata = get_user($saved_user);
setup_user($saveduserdata);
}
return $return;
}

135
include/ajax_functions.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
/**
* @package ResourceSpace
* @subpackage AJAX
*
* The functions available in this file will help developers provide a consistent response to AJAX requests.
*
* All functions (with the exception of ajax_permission_denied) will follow the JSEnd specification (@see https://github.com/omniti-labs/jsend)
*
*/
/**
* Returns a standard AJAX response for unauthorised access
*
* The function will return a 401 HTTP status code.
*
* @deprecated
*
* @return void
*/
function ajax_permission_denied()
{
$return['error'] = array(
'status' => 401,
'title' => $GLOBALS["lang"]["unauthorized"],
'detail' => $GLOBALS["lang"]['error-permissiondenied']);
http_response_code(401);
echo json_encode($return);
exit();
}
/**
* Send AJAX response back to the client together with the appropriate HTTP status code
*
* @param integer $code HTTP status code for this response
* @param array $response Response data (@see other ajax_response_* functions for expected structure)
*
* @return void
*/
function ajax_send_response($code, array $response)
{
http_response_code($code);
echo json_encode($response);
exit();
}
/**
* Send AJAX text/html response back to the client together with the appropriate HTTP status code
*
* @param integer $code HTTP status code for this response
* @param string $response Response data (text/html)
*
* @return void
*/
function ajax_send_text_response($code, $response)
{
http_response_code($code);
echo $response;
exit();
}
/**
* Builds the correct response expected for a success request where there is data to return (e.g getting search results)
*
* @param array $data Data to be returned back to the client
*
* @return array
*/
function ajax_response_ok(array $data)
{
return array(
"status" => "success",
"data" => $data);
}
/**
* Builds the correct response expected for failures.
*
* When a call is rejected due to invalid data or call conditions, the response data key contains an object explaining
* what went wrong, typically a hash of validation errors.
*
* @param array $data Provides details of why the request failed. If the reasons for failure correspond to POST values,
* the response objects' keys SHOULD correspond to those POST values. If generic, use message key
* instead (@see ajax_build_message() ).
*
* @return array
*/
function ajax_response_fail(array $data)
{
return array(
"status" => "fail",
"data" => $data);
}
/**
* Builds the correct response expected for a success request where there is no data to return (e.g when deleting a record)
*
* @return array
*/
function ajax_response_ok_no_data()
{
return array(
"status" => "success",
"data" => null);
}
/**
* Returns a standard AJAX response for unauthorised access with a 401 HTTP status code
*
* @return void
*/
function ajax_unauthorized()
{
global $lang;
ajax_send_response(401, ajax_response_fail(ajax_build_message($lang['error-permissiondenied'])));
}
/**
* Builds a message to be used in an AJAX response
*
* @param string $msg An end-user message explaining what happened (as a generic message for fails or part of errors)
*
* @return array Returns a message
*/
function ajax_build_message(string $msg)
{
$msg = trim($msg);
if ($msg == "") {
trigger_error("\$msg variable must not be empty.");
}
return array("message" => $msg);
}

View File

@@ -0,0 +1,556 @@
<?php
/**
* Get annotation by ID
*
* @param integer $ref Annotation ID
*
* @return array
*/
function getAnnotation($ref)
{
if (0 >= $ref) {
return array();
}
$return = ps_query("SELECT " . columns_in("annotation") . " FROM annotation WHERE ref = ?", array("i",$ref));
if (0 < count($return)) {
$return = $return[0];
}
return $return;
}
/**
* General annotations search functionality
*
* @uses ps_query()
*
* @param integer $resource
* @param integer $resource_type_field
* @param integer $user
* @param integer $page
*
* @return array
*/
function getAnnotations($resource = 0, $resource_type_field = 0, $user = 0, $page = 0)
{
if (!is_numeric($resource) || !is_numeric($resource_type_field) || !is_numeric($user) || !is_numeric($page)) {
return array();
}
$sql_where_clause = '';
$parameters = array();
if (0 < $resource) {
$sql_where_clause = " resource = ?";
$parameters = array("i",$resource);
}
if (0 < $resource_type_field) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " resource_type_field = ?";
$parameters = array_merge($parameters, array("i",$resource_type_field));
}
if (0 < $user) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " user = ?";
$parameters = array_merge($parameters, array("i",$user));
}
if (0 < $page) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " page = ?";
$parameters = array_merge($parameters, array("i",$page));
}
if ('' != $sql_where_clause) {
$sql_where_clause = "WHERE {$sql_where_clause}";
}
return ps_query("SELECT " . columns_in("annotation") . " FROM annotation {$sql_where_clause}", $parameters);
}
/**
* Get number of annotations available for a resource.
*
* Note: multi page resources will show the total number (ie. all pages)
*
* @uses ps_value()
*
* @param integer $resource Resource ID
*
* @return integer
*/
function getResourceAnnotationsCount($resource)
{
if (!is_numeric($resource) || 0 >= $resource) {
return 0;
}
return (int) ps_value("SELECT count(ref) AS `value` FROM annotation WHERE resource = ?", array("i",$resource), 0);
}
/**
* Get annotations for a specific resource
*
* @param integer $resource Resource ID
* @param integer $page Page number of a document. Non documents will have 0
*
* @return array
*/
function getResourceAnnotations($resource, $page = 0)
{
if (0 >= $resource) {
return array();
}
$parameters = array("i",$resource);
$sql_page_filter = 'AND `page` IS NULL';
if (0 < $page) {
$sql_page_filter = "AND `page` IS NOT NULL AND `page` = ?";
$parameters = array_merge($parameters, array("i",$page));
}
return ps_query("SELECT " . columns_in("annotation") . " FROM annotation WHERE resource = ? {$sql_page_filter}", $parameters);
}
/**
* Create an array of Annotorious annotation objects which can be JSON encoded and passed
* directly to Annotorious
*
* @param integer $resource Resource ID
* @param integer $page Page number of a document
*
* @return array
*/
function getAnnotoriousResourceAnnotations($resource, $page = 0)
{
global $baseurl;
$annotations = array();
$can_view_fields = canSeeAnnotationsFields();
/*
Build an annotations array of Annotorious annotation objects
IMPORTANT: until ResourceSpace will have text fields implemented as nodes, text should be left blank.
NOTE: src attribute is generated per resource (dummy source) to avoid issues when source is
loaded from download.php
*/
foreach (getResourceAnnotations($resource, $page) as $annotation) {
if (in_array($annotation['resource_type_field'], $can_view_fields)) {
$annotations[] = array(
'src' => "{$baseurl}/annotation/resource/{$resource}",
'text' => '',
'shapes' => array(
array(
'type' => 'rect',
'geometry' => array(
'x' => (float) $annotation['x'],
'y' => (float) $annotation['y'],
'width' => (float) $annotation['width'],
'height' => (float) $annotation['height'],
)
)
),
'editable' => annotationEditable($annotation),
// Custom ResourceSpace properties for Annotation object
'ref' => (int) $annotation['ref'],
'resource' => (int) $annotation['resource'],
'resource_type_field' => (int) $annotation['resource_type_field'],
'page' => (int) $annotation['page'],
'tags' => getAnnotationTags($annotation),
);
}
}
return $annotations;
}
/**
* Check if an annotation can be editable (add/ edit/ remove) by the user
*
* @uses checkPermission_anonymoususer()
*
* @param array $annotation
*
* @return boolean
*/
function annotationEditable(array $annotation)
{
debug(sprintf('[annotations][fct=annotationEditable] $annotation = %s', json_encode($annotation)));
global $userref;
$add_operation = !isset($annotation['user']);
$field_edit_access = metadata_field_edit_access($annotation['resource_type_field']);
/* Non-admin edit authorisation is valid when:
- user is just adding a new annotation
- when editing/removing an existing annotation, the annotation was created by the user itself
*/
$non_admin_athz = ($add_operation || $userref == $annotation['user']);
// Anonymous users cannot edit by default. They can only edit if they are allowed CRUD operations
if (checkPermission_anonymoususer()) {
return $non_admin_athz && $field_edit_access;
}
return (checkperm('a') || $non_admin_athz) && $field_edit_access;
}
/**
* Get all tags of an annotation. Checks if a tag is attached to the resource,
* allowing the user to search by it which is represented by the virtual column
* "tag_searchable"
*
* @param array $annotation
*
* @return array
*/
function getAnnotationTags(array $annotation)
{
$resource_ref = $annotation['resource'];
$annotation_ref = $annotation['ref'];
$parameters = array("i", $resource_ref, "i", $annotation_ref);
return ps_query("
SELECT " . columns_in("node", "n") . ",
(SELECT 'yes' FROM resource_node WHERE resource = ? AND node = ref) AS tag_searchable
FROM node AS n
WHERE ref IN (SELECT node FROM annotation_node WHERE annotation = ?);", $parameters);
}
/**
* Delete annotation
*
* @see getAnnotation()
*
* @uses annotationEditable()
* @uses getAnnotationTags()
* @uses delete_resource_nodes()
* @uses db_begin_transaction()
* @uses db_end_transaction()
*
* @param array $annotation Annotation array as returned by getAnnotation()
*
* @return boolean
*/
function deleteAnnotation(array $annotation)
{
if (!annotationEditable($annotation)) {
return false;
}
$annotation_ref = $annotation['ref'];
$parameters = array("i",$annotation_ref);
$nodes_to_remove = array();
foreach (getAnnotationTags($annotation) as $tag) {
$nodes_to_remove[] = $tag['ref'];
}
db_begin_transaction("deleteAnnotation");
if (0 < count($nodes_to_remove)) {
delete_resource_nodes($annotation['resource'], $nodes_to_remove);
}
ps_query("DELETE FROM annotation_node WHERE annotation = ?", $parameters);
ps_query("DELETE FROM annotation WHERE ref = ?", $parameters);
db_end_transaction("deleteAnnotation");
return true;
}
/**
* Create new annotations based on Annotorious annotation
*
* NOTE: Annotorious annotation shape is an array but at the moment they use only the first shape found
*
* @param array $annotation
*
* @return boolean|integer Returns false on failure OR the ref of the newly created annotation
*/
function createAnnotation(array $annotation)
{
debug(sprintf('[annotations][fct=createAnnotation] Param $annotation = %s', json_encode($annotation)));
global $userref;
if (!annotationEditable($annotation)) {
debug('[annotations][fct=createAnnotation][warn] annotation not editable');
return false;
}
debug('[annotations][fct=createAnnotation] attempting to create annotation...');
// ResourceSpace specific properties
$resource = $annotation['resource'];
$resource_type_field = $annotation['resource_type_field'];
$page = (isset($annotation['page']) && 0 < $annotation['page'] ? $annotation['page'] : null);
$tags = $annotation['tags'] ?? [];
// Annotorious annotation
$x = $annotation['shapes'][0]['geometry']['x'];
$y = $annotation['shapes'][0]['geometry']['y'];
$width = $annotation['shapes'][0]['geometry']['width'];
$height = $annotation['shapes'][0]['geometry']['height'];
ps_query(
'INSERT INTO annotation (resource, resource_type_field, user, x, y, width, height, page) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[
'i', $resource,
'i', $resource_type_field,
'i', $userref,
'd', $x,
'd', $y,
'd', $width,
'd', $height,
'i', $page,
]
);
$annotation_ref = sql_insert_id();
debug('[annotations][fct=createAnnotation] annotation_ref = ' . json_encode($annotation_ref));
if (0 == $annotation_ref) {
debug('[annotations][fct=createAnnotation][warn] Unable to create annotation');
return false;
}
// Prepare tags before association by adding new nodes to dynamic keywords list (if permissions allow it)
$prepared_tags = prepareTags($tags);
// Add any tags associated with it
if (0 < count($tags)) {
addAnnotationNodes($annotation_ref, $prepared_tags);
add_resource_nodes($resource, array_column($prepared_tags, 'ref'), false);
}
return $annotation_ref;
}
/**
* Update annotation and its tags if needed
*
* @uses annotationEditable()
* @uses getAnnotationTags()
* @uses delete_resource_nodes()
* @uses addAnnotationNodes()
* @uses add_resource_nodes()
* @uses db_begin_transaction()
* @uses db_end_transaction()
*
* @param array $annotation
*
* @return boolean
*/
function updateAnnotation(array $annotation)
{
debug(sprintf('[annotations][fct=updateAnnotation] Param $annotation = %s', json_encode($annotation)));
if (!isset($annotation['ref']) || !annotationEditable($annotation)) {
return false;
}
global $userref;
// ResourceSpace specific properties
$annotation_ref = $annotation['ref'];
$resource_type_field = $annotation['resource_type_field'];
$resource = $annotation['resource'];
$page = (isset($annotation['page']) && 0 < $annotation['page'] ? $annotation['page'] : null);
$tags = $annotation['tags'] ?? [];
// Annotorious annotation
$x = $annotation['shapes'][0]['geometry']['x'];
$y = $annotation['shapes'][0]['geometry']['y'];
$width = $annotation['shapes'][0]['geometry']['width'];
$height = $annotation['shapes'][0]['geometry']['height'];
ps_query(
'UPDATE annotation SET resource_type_field = ?, user = ?, x = ?, y = ?, width = ?, height = ?, page = ? WHERE ref = ?',
[
'i', $resource_type_field,
'i', $userref,
'd', $x,
'd', $y,
'd', $width,
'd', $height,
'i', $page,
'i', $annotation_ref,
]
);
// Delete existing associations
$nodes_to_remove = array();
foreach (getAnnotationTags($annotation) as $tag) {
$nodes_to_remove[] = $tag['ref'];
}
db_begin_transaction("updateAnnotation");
if (0 < count($nodes_to_remove)) {
delete_resource_nodes($resource, $nodes_to_remove);
}
ps_query("DELETE FROM annotation_node WHERE annotation = ?", ['i', $annotation_ref]);
// Add any tags associated with this annotation
if (0 < count($tags)) {
// Prepare tags before association by adding new nodes to
// dynamic keywords list (if permissions allow it)
$prepared_tags = prepareTags($tags);
// Add new associations
addAnnotationNodes($annotation_ref, $prepared_tags);
add_resource_nodes($resource, array_column($prepared_tags, 'ref'), false);
}
db_end_transaction("updateAnnotation");
return true;
}
/**
* Add relations between annotation and nodes
*
* @param integer $annotation_ref The annotation ID in ResourceSpace
* @param array $nodes List of node structures {@see get_nodes()}. Only the "ref" property is required.
*
* @return boolean
*/
function addAnnotationNodes($annotation_ref, array $nodes)
{
if (0 === count($nodes)) {
return false;
}
$query_insert_values = '';
$parameters = [];
foreach ($nodes as $node) {
$query_insert_values .= ',(?, ?)';
$parameters = array_merge($parameters, ['i', $annotation_ref, 'i', $node['ref']]);
}
$query_insert_values = substr($query_insert_values, 1);
ps_query("INSERT INTO annotation_node (annotation, node) VALUES {$query_insert_values}", $parameters);
return true;
}
/**
* Utility function which allows annotation tags to be prepared (i.e make sure they are all valid nodes)
* before creating associations between annotations and tags
*
* @uses checkperm()
* @uses get_resource_type_field()
* @uses set_node()
* @uses get_node()
*
* @param array $dirty_tags Original array of tags. These can be (in)valid tags/ new tags.
* IMPORTANT: a tag should have the same structure as a node
*
* @return array
*/
function prepareTags(array $dirty_tags)
{
if (0 === count($dirty_tags)) {
return array();
}
global $annotate_fields;
$clean_tags = array();
foreach ($dirty_tags as $dirty_tag) {
// Check minimum required information for a node
if (
!isset($dirty_tag['resource_type_field'])
|| 0 >= $dirty_tag['resource_type_field']
|| !in_array($dirty_tag['resource_type_field'], $annotate_fields)
) {
continue;
}
if (!isset($dirty_tag['name']) || '' == $dirty_tag['name']) {
continue;
}
// No access to field? Next...
if (!metadata_field_view_access($dirty_tag['resource_type_field'])) {
continue;
}
// New node?
if (is_null($dirty_tag['ref']) || (is_string($dirty_tag['ref']) && '' == $dirty_tag['ref'])) {
$dirty_field_data = get_resource_type_field($dirty_tag['resource_type_field']);
// Only dynamic keywords lists are allowed to create new options from annotations if permission allows it
if (
!(
FIELD_TYPE_DYNAMIC_KEYWORDS_LIST == $dirty_field_data['type']
&& !checkperm("bdk{$dirty_tag['resource_type_field']}")
)
) {
continue;
}
// Create new node but avoid duplicates
$new_node_id = set_node(null, $dirty_tag['resource_type_field'], $dirty_tag['name'], null, null);
if (false !== $new_node_id && is_numeric($new_node_id)) {
$dirty_tag['ref'] = $new_node_id;
$clean_tags[] = $dirty_tag;
}
continue;
}
// Existing tags
$found_node = array();
if (
get_node((int) $dirty_tag['ref'], $found_node)
&& $found_node['resource_type_field'] == $dirty_tag['resource_type_field']
) {
$clean_tags[] = $found_node;
}
}
return $clean_tags;
}
/**
* Add annotation count to a search result set
*
* @param array $items Array of search results returned by do_search()
*
*/
function search_add_annotation_count(&$result)
{
$annotations = ps_query(
"SELECT resource, count(*) as annocount
FROM annotation
WHERE resource IN (" . ps_param_insert(count($result)) . ")
GROUP BY resource",
ps_param_fill(array_column($result, "ref"), "i")
);
$res_annotations = array_column($annotations, "annocount", "resource");
foreach ($result as &$resource) {
$resource["annotation_count"] = $res_annotations[$resource["ref"]] ?? 0;
}
}

1561
include/api_bindings.php Normal file

File diff suppressed because it is too large Load Diff

308
include/api_functions.php Normal file
View File

@@ -0,0 +1,308 @@
<?php
/*
* API v2 functions
*
* Montala Limited, July 2016
*
* For documentation please see: http://www.resourcespace.com/knowledge-base/api/
*
*/
global $iiif_enabled;
if ($iiif_enabled) {
include_once __DIR__ . '/iiif_functions.php';
}
/**
* Return a private scramble key for this user.
*
* @param integer $user The user ID
* @return string|false
*/
function get_api_key($user)
{
global $api_scramble_key;
return hash("sha256", $user . $api_scramble_key);
}
/**
* Check a query is signed correctly.
*
* @param string $username The username of the calling user
* @param string $querystring The query being passed to the API
* @param string $sign The signature to check
* @param string $authmode The type of key being provided (user key or session key)
*/
function check_api_key($username, $querystring, $sign, $authmode = "userkey"): bool
{
// Fetch user ID and API key
$user = get_user_by_username($username);
if ($user === false) {
return false;
}
$aj = strpos($querystring, "&ajax=");
if ($aj !== false) {
$querystring = substr($querystring, 0, $aj);
}
if ($authmode == "sessionkey") {
$userkey = get_session_api_key($user);
} else {
$userkey = get_api_key($user);
}
# Calculate the expected signature and check it matches
$expected = hash("sha256", $userkey . $querystring);
if ($expected === $sign) {
return true;
}
# Also try matching against the username - allows remote API use without knowing the user ID, e.g. in the event of managing multiple systems each with a common username but different ID.
if (hash("sha256", get_api_key($username) . $querystring) === $sign) {
return true;
}
return false;
}
/**
* Execute the specified API function.
*
* @param string $query The query string passed to the API
* @param boolean $pretty Should the JSON encoded result be 'pretty' i.e. formatted for reading?
* @return bool|string
*/
function execute_api_call($query, $pretty = false)
{
$params = [];
parse_str($query, $params);
if (!array_key_exists("function", $params)) {
return false;
}
$function = $params["function"];
if (!function_exists("api_" . $function)) {
return false;
}
global $lang;
// Check if this is a whitelisted function for browser use (native mode bypasses $enable_remote_apis=false;)
if (
defined("API_AUTHMODE_NATIVE")
&& !in_array($function, API_NATIVE_WHITELIST)
) {
ajax_unauthorized();
}
// Construct an array of the real params, setting default values as necessary
$setparams = [];
$n = 0;
$fct = new ReflectionFunction("api_" . $function);
foreach ($fct->getParameters() as $fparam) {
$paramkey = $n + 1;
$param_name = $fparam->getName();
debug("API: Checking for parameter " . $param_name . " (param" . $paramkey . ")");
if (array_key_exists("param" . $paramkey, $params)) {
debug("API: " . $param_name . " - value has been passed : '" . $params["param" . $paramkey] . "'");
$setparams[$n] = $params["param" . $paramkey];
} elseif (array_key_exists($param_name, $params)) {
debug("API: {$param_name} - value has been passed (by name): '" . json_encode($params[$param_name]) . "'");
// Check if array;
$type = $fparam->getType();
if (gettype($type) == "object") {
// type is an object
$type = $type->getName();
}
if ($fparam->hasType() && gettype($type) == "string" && $type == "array" && !is_array($params[$param_name])) {
// Decode as must be json encoded if array
$GLOBALS["use_error_exception"] = true;
try {
$decoded = json_decode($params[$param_name], JSON_OBJECT_AS_ARRAY);
} catch (Exception $e) {
$error = str_replace(
array("%arg", "%expected-type", "%type"),
array($param_name, "array (json encoded)",$lang['unknown']),
$lang["error-type-mismatch"]
);
return json_encode($error);
}
unset($GLOBALS["use_error_exception"]);
// Check passed data type after decode
if (gettype($decoded) != "array") {
$error = str_replace(
array("%arg", "%expected-type", "%type"),
array($param_name, "array (json encoded)", $lang['unknown']),
$lang["error-type-mismatch"]
);
return json_encode($error);
}
$params[$param_name] = $decoded;
}
$setparams[$n] = $params[$param_name];
} elseif ($fparam->isOptional()) {
// Set default value if nothing passed e.g. from API test tool
debug("API: " . $param_name . " - setting default value = '" . $fparam->getDefaultValue() . "'");
$setparams[$n] = $fparam->getDefaultValue();
} else {
// Set as empty
debug("API: {$param_name} - setting empty value");
$setparams[$n] = "";
}
$n++;
}
debug("API: calling api_" . $function);
$result = call_user_func_array("api_" . $function, $setparams);
if ($pretty) {
debug("API: json_encode() using JSON_PRETTY_PRINT");
$json_encoded_result = json_encode($result, (defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0));
} else {
debug("API: json_encode()");
$json_encoded_result = json_encode($result);
}
if (json_last_error() !== JSON_ERROR_NONE) {
debug("API: JSON error: " . json_last_error_msg());
debug("API: JSON error when \$result = " . print_r($result, true));
$json_encoded_result = json_encode($result, JSON_UNESCAPED_UNICODE);
}
return $json_encoded_result;
}
/**
* Return the session specific key for the given user.
*
* @param integer $user The user ID
* @return string
*/
function get_session_api_key($user)
{
global $scramble_key;
$private_key = get_api_key($user);
$usersession = ps_value("SELECT session value FROM user where ref = ?", array("i",$user), "");
return hash_hmac("sha256", "{$usersession}{$private_key}", $scramble_key);
}
/**
* API login function
*
* @param string $username Username
* @param string $password Password to validate
* @return string|false FALSE if invalid, session API key if valid
*/
function api_login($username, $password)
{
global $session_hash, $scramble_key;
$user = get_user_by_username($username);
if ($user === false) {
return false;
}
$result = perform_login($username, $password);
$private_key = get_api_key($user);
if ((bool)$result['valid']) {
return hash_hmac("sha256", "{$session_hash}{$private_key}", $scramble_key);
}
return false;
}
/**
* Validate URL supplied in APIs create resource or upload by URL. Requires the URL hostname to be added in config $api_upload_urls
*
* @param string $url The full URL.
*
* @return bool Returns true if a valid URL is found.
*/
function api_validate_upload_url($url)
{
$url = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
if ($url === false) {
return false;
}
$url_parts = parse_url($url);
if (in_array($url_parts['scheme'], BLOCKED_STREAM_WRAPPERS)) {
return false;
}
global $api_upload_urls;
if (!isset($api_upload_urls)) {
return true; // For systems prior to this config.
}
if (in_array($url_parts['host'], $api_upload_urls)) {
return true;
}
return false;
}
/**
* Assert API request is using POST method.
*
* @param bool $force Force the assertion
*
* @return array Returns JSend data back {@see ajax_functions.php} if not POST method
*/
function assert_post_request(bool $force): array
{
// Legacy use cases we don't want to break backwards compatibility for (e.g JS api() makes only POST requests but
// other clients might only use GET because it was allowed if not authenticating with native mode)
if (!$force) {
return [];
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
return [];
} else {
http_response_code(405);
return ajax_response_fail(ajax_build_message($GLOBALS['lang']['error-method-not_allowed']));
}
}
/**
* Assert API sent the expected content type.
*
* @param string $expected MIME type
* @param string $received_raw MIME type
*
* @return array Returns JSend data back {@see ajax_functions.php} if received Content-Type is unexpected
*/
function assert_content_type(string $expected, string $received_raw): array
{
$expected = trim($expected);
if ($expected === '') {
trigger_error('Expected MIME type MUST not be a blank string', E_USER_ERROR);
}
$encoding = 'UTF-8';
$received = mb_strcut($received_raw, 0, mb_strlen($expected, $encoding), $encoding);
if ($expected === $received) {
return [];
}
http_response_code(415);
header("Accept: {$expected}");
return ajax_response_fail([]);
}
/**
* Return a summary of daily statistics
*
* @param int $days The number of days - note max 365 days as only the current and previous year's data is accessed.
*/
function api_get_daily_stat_summary(int $days = 30)
{
if (!checkperm("a")) {
return false;
} // Admin only
return ps_query("SELECT activity_type,sum(count) `count`
FROM daily_stat
WHERE
(`year`=year(NOW()) OR `year`=year(NOW())-1)
AND
concat(`year`,'-',`month`,'-',`day`,'-')>date_sub(NOW(), interval ? DAY)
GROUP BY activity_type
", ["i",$days]);
}

321
include/authenticate.php Executable file
View File

@@ -0,0 +1,321 @@
<?php
include_once __DIR__ . '/login_functions.php';
debug("[authenticate.php] Reached authenticate page...");
# authenticate user based on cookie
$valid = true;
$autologgedout = false;
$nocookies = false;
$is_authenticated = false;
if (array_key_exists("user", $_COOKIE) || array_key_exists("user", $_GET) || isset($anonymous_login) || hook('provideusercredentials')) {
debug("[authenticate.php] Attempting to resolve user session...");
$username = "";
// Resolve anonymous login user if it is configured at domain level
if (isset($anonymous_login) && is_array($anonymous_login)) {
foreach ($anonymous_login as $key => $val) {
if ($baseurl == $key) {
$anonymous_login = $val;
}
}
}
// Establish session hash
$session_hash = "";
if (array_key_exists("user", $_GET)) {
$session_hash = $_GET["user"];
} elseif (array_key_exists("user", $_COOKIE)) {
$session_hash = $_COOKIE["user"];
} elseif (isset($anonymous_login)) {
$username = $anonymous_login;
$rs_session = get_rs_session_id(true);
// Always check the browser for anonymous access
browser_check();
}
if (!is_string($session_hash)) {
http_response_code(400);
exit();
}
// Automatic anonymous login, do not require session hash.
$user_select_sql = new PreparedStatementQuery();
if (isset($anonymous_login) && $username == $anonymous_login) {
$user_select_sql->sql = "u.username = ? AND usergroup IN (SELECT ref FROM usergroup)";
$user_select_sql->parameters = ["s",$username];
} else {
$user_select_sql->sql = "u.session=?";
$user_select_sql->parameters = ["s",$session_hash];
}
hook('provideusercredentials');
$userdata = validate_user($user_select_sql, true); // validate user and get user details
if (count($userdata) > 0) {
debug("[authenticate.php] User valid!");
$valid = true;
setup_user($userdata[0]);
if (
$password_expiry > 0
&& !checkperm("p")
&& $allow_password_change
&& in_array($pagename, ["user_change_password","index","collections","user_home"]) === false
&& strlen(trim((string) $userdata[0]["password_last_change"])) > 0
&& getval("modal", "") == ""
&& trim((string) $userdata[0]["origin"]) === "" // Don't force change if ResourceSpace doesn't manage the user's password
) {
# Redirect the user to the password change page if their password has expired.
$last_password_change = time() - strtotime((string) $userdata[0]["password_last_change"]);
if ($last_password_change > ($password_expiry * 60 * 60 * 24)) {
debug("[authenticate.php] Redirecting user to change password...");
?>
<script>
top.location.href="<?php echo $baseurl_short?>pages/user/user_change_password.php?expired=true";
</script>
<?php
}
}
if (
!isset($system_login)
&& strlen(trim((string)$userdata[0]["last_active"])) > 0
&& $userdata[0]["idle_seconds"] > ($session_length * 60)
) {
debug("[authenticate.php] Session length expired!");
# Last active more than $session_length mins ago?
$al = "";
if (isset($anonymous_login)) {
$al = $anonymous_login;
}
if ($session_autologout && $username != $al) { # If auto logout enabled, but this is not the anonymous user, log them out.
debug("[authenticate.php] Autologging out user.");
# Reached the end of valid session time, auto log out the user.
# Remove session
ps_query("update user set logged_in = 0, session = '' where ref= ?", array("i",$userref));
hook("removeuseridcookie");
# Blank cookie / var
rs_setcookie("user", "", -1, "", "", substr($baseurl, 0, 5) == "https", true);
rs_setcookie("user", "", -1, "/pages", "", substr($baseurl, 0, 5) == "https", true);
unset($username);
if (isset($anonymous_login)) {
# If the system is set up with anonymous access, redirect to the home page after logging out.
redirect("pages/home.php");
} else {
$valid = false;
$autologgedout = true;
}
} else {
# Session end reached, but the user may still remain logged in.
# This is a new 'session' for the purposes of statistics.
daily_stat("User session", $userref);
}
}
} else {
$valid = false;
}
} else {
$valid = false;
$nocookies = true;
# Set a cookie that we'll check for again on the login page after the redirection.
# If this cookie is missing, it's assumed that cookies are switched off or blocked and a warning message is displayed.
rs_setcookie('cookiecheck', 'true', 0, '/');
hook("removeuseridcookie");
}
if (!$valid && !isset($system_login)) {
debug("[authenticate.php] User not valid!");
$_SERVER['REQUEST_URI'] = ( isset($_SERVER['REQUEST_URI']) ?
$_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME'] . ( isset($_SERVER
['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''));
$path = $_SERVER["REQUEST_URI"];
debug("[authenticate.php] path = $path");
if (strpos($path, "/ajax") !== false) {
if (isset($_COOKIE["user"])) {
http_response_code(401);
exit($lang['error-sessionexpired']);
} else {
http_response_code(403);
exit($lang['error-permissiondenied']);
}
}
$path = str_replace("ajax=", "ajax_disabled=", $path);# Disable forwarding of the AJAX parameter if this was an AJAX load, otherwise the redirected page will be missing the header/footer.
$redirparams = array();
$redirparams["url"] = isset($anonymous_login) ? "" : $path;
$redirparams["auto"] = $autologgedout ? "true" : "";
$redirparams["nocookies"] = $nocookies ? "true" : "";
if (strpos($path, "ajax") !== false || getval("ajax", "") != "") {
// Perform a javascript redirect as may be directly loading content directly into div.
$url = generateURL($baseurl . "/login.php", $redirparams);
?>
<script>
top.location.href="<?php echo $url ?>";
</script>
<?php
exit();
} else {
$url = generateURL($baseurl . "/login.php", $redirparams);
debug("[authenticate.php] Redirecting to $url");
redirect($url);
exit();
}
}
# Handle IP address restrictions
$ip = get_ip();
if (isset($ip_restrict_group)) {
$ip_restrict = $ip_restrict_group;
if ($ip_restrict_user != "") {
$ip_restrict = $ip_restrict_user;
} # User IP restriction overrides the group-wide setting.
if ($ip_restrict != "") {
$allow = false;
if (!hook('iprestrict')) {
$allow = ip_matches($ip, $ip_restrict);
}
if (!$allow) {
header("HTTP/1.0 403 Access Denied");
exit("Access denied.");
}
}
}
#update activity table
global $pagename;
/*
Login terms have not been accepted? Redirect until user does so
Note: it is considered safe to show the collection bar because even if we enable login terms
later on, when the user might have resources in it, they would not be able to do anything with them
unless they accept terms
*/
if ($terms_login && 0 == $useracceptedterms && in_array($pagename, array("reload_links","browsebar_js","css_override","category_tree_lazy_load","message","terms","collections","login","user_change_password", "user_home")) === false) {
redirect('pages/terms.php?noredir=true&url=' . urlencode("pages/home.php"));
}
if (isset($_SERVER["HTTP_USER_AGENT"])) {
$last_browser = substr($_SERVER["HTTP_USER_AGENT"], 0, 250);
} else {
$last_browser = "unknown";
}
// don't update this table if the System is doing its own operations
if (!isset($system_login)) {
update_user_access($userref, ["logged_in" => 1]);
}
# Add group specific text (if any) when logged in.
if (hook("replacesitetextloader")) {
# this hook expects $site_text to be modified and returned by the plugin
$site_text = hook("replacesitetextloader");
} else {
if (isset($usergroup)) {
// Fetch user group specific content.
$site_text_query = "
SELECT `name`,
`text`,
`page`
FROM site_text
WHERE language = ?
AND specific_to_group = ?
";
$parameters = array
(
"s",$language,
"i",$usergroup
);
if ($pagename != "admin_content") { // Load all content on the admin_content page to allow management.
$site_text_query .= "AND (page = ? OR page = 'all' OR page = '' " . (($pagename == "dash_tile") ? " OR page = 'home'" : "") . ")";
$parameters[] = "s";
$parameters[] = $pagename;
}
$results = ps_query($site_text_query, $parameters, "sitetext", -1, true, 0);
for ($n = 0; $n < count($results); $n++) {
if ($results[$n]['page'] == '') {
$lang[$results[$n]['name']] = $results[$n]['text'];
$customsitetext[$results[$n]['name']] = $results[$n]['text'];
} else {
$lang[$results[$n]['page'] . '__' . $results[$n]['name']] = $results[$n]['text'];
}
}
}
} /* end replacesitetextloader */
$GLOBALS['plugins'] = register_group_access_plugins($usergroup, $plugins ?? []);
// Load user config options
process_config_options(array('usergroup' => $usergroup));
process_config_options(array('user' => $userref));
// Once system wide/user preferences and user group config overrides have loaded, any config based dependencies should be checked and loaded.
if (!$disable_geocoding) {
include_once __DIR__ . '/map_functions.php';
}
hook('handleuserref', '', array($userref));
// Set a trace ID which can be used to correlate events within this request (requires $debug_extended_info)
$trace_id_components = [
getmypid(),
$_SERVER['REQUEST_TIME_FLOAT'],
$GLOBALS['pagename'], # already set in boot.php
http_build_query($_GET),
$GLOBALS['userref'],
];
$GLOBALS['debug_trace_id'] = generate_trace_id($trace_id_components);
debug(sprintf(
'User %s (ID %s) set its debug_trace_id to "%s" (components: %s)',
$GLOBALS['username'],
$GLOBALS['userref'],
$GLOBALS['debug_trace_id'],
json_encode($trace_id_components)
));
$is_authenticated = true;
// Check CSRF Token
$csrf_token = getval($CSRF_token_identifier, "");
if (
$_SERVER["REQUEST_METHOD"] === "POST"
&& !isValidCSRFToken($csrf_token, $usersession)
&& !(isset($anonymous_login) && $username == $anonymous_login)
&& !defined("API_CALL")
) {
http_response_code(400);
if (filter_var(getval("ajax", false), FILTER_VALIDATE_BOOLEAN)) {
include_once __DIR__ . "/ajax_functions.php";
$return['error'] = array(
'title' => $lang["error-csrf-verification"],
'detail' => $lang["error-csrf-verification-failed"]);
echo json_encode(array_merge($return, ajax_response_fail(ajax_build_message($lang["error-csrf-verification-failed"]))));
exit();
}
exit($lang["error-csrf-verification-failed"]);
} elseif (defined('API_CALL') && $_SERVER['REQUEST_METHOD'] === 'POST' && !isValidCSRFToken($csrf_token, $usersession)) {
ajax_send_response(
400,
ajax_response_fail(ajax_build_message("{$lang['error-csrf-verification']}: {$lang['error_invalid_input']}"))
);
}

510
include/boot.php Executable file
View File

@@ -0,0 +1,510 @@
<?php
/**
* boot.php
*
* Connects to the database, loads all the necessary things all pages need such as configuration, plugins, languages.
*/
# Include the most commonly used functions
include_once __DIR__ . '/definitions.php';
include_once __DIR__ . '/version.php';
include_once __DIR__ . '/general_functions.php';
include_once __DIR__ . '/database_functions.php';
include_once __DIR__ . '/search_functions.php';
include_once __DIR__ . '/do_search.php';
include_once __DIR__ . '/resource_functions.php';
include_once __DIR__ . '/collections_functions.php';
include_once __DIR__ . '/language_functions.php';
include_once __DIR__ . '/message_functions.php';
include_once __DIR__ . '/node_functions.php';
include_once __DIR__ . '/encryption_functions.php';
include_once __DIR__ . '/render_functions.php';
include_once __DIR__ . '/user_functions.php';
include_once __DIR__ . '/debug_functions.php';
include_once __DIR__ . '/log_functions.php';
include_once __DIR__ . '/file_functions.php';
include_once __DIR__ . '/config_functions.php';
include_once __DIR__ . '/plugin_functions.php';
include_once __DIR__ . '/migration_functions.php';
include_once __DIR__ . '/metadata_functions.php';
include_once __DIR__ . '/job_functions.php';
include_once __DIR__ . '/tab_functions.php';
include_once __DIR__ . '/mime_types.php';
include_once __DIR__ . '/CommandPlaceholderArg.php';
# Switch on output buffering.
ob_start(null, 4096);
$pagetime_start = microtime();
$pagetime_start = explode(' ', $pagetime_start);
$pagetime_start = $pagetime_start[1] + $pagetime_start[0];
if ((!isset($suppress_headers) || !$suppress_headers) && !isset($nocache)) {
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // always modified
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
}
set_error_handler("errorhandler");
// Check the PHP version.
if (PHP_VERSION_ID < PHP_VERSION_SUPPORTED) {
exit("PHP version not supported. Your version: " . PHP_VERSION_ID . ", minimum supported: " . PHP_VERSION_SUPPORTED);
}
# *** LOAD CONFIG ***
# Load the default config first, if it exists, so any new settings are present even if missing from config.php
if (file_exists(__DIR__ . "/config.default.php")) {
include __DIR__ . "/config.default.php";
}
if (file_exists(__DIR__ . "/config.deprecated.php")) {
include __DIR__ . "/config.deprecated.php";
}
# Load the real config
if (!file_exists(__DIR__ . "/config.php")) {
header("Location: pages/setup.php");
die(0);
}
include __DIR__ . "/config.php";
// Set exception_ignore_args so that if $log_error_messages_url is set it receives all the necessary
// information to perform troubleshooting
ini_set("zend.exception_ignore_args", "Off");
error_reporting($config_error_reporting);
// Check this is a real browser.
if ($browser_check) {browser_check();}
# -------------------------------------------------------------------------------------------
# Remote config support - possibility to load the configuration from a remote system.
#
debug('[boot.php] Remote config support...');
debug('[boot.php] isset($remote_config_url) = ' . json_encode(isset($remote_config_url)));
debug('[boot.php] isset($_SERVER["HTTP_HOST"]) = ' . json_encode(isset($_SERVER["HTTP_HOST"])));
debug('[boot.php] getenv("RESOURCESPACE_URL") != "") = ' . json_encode(getenv("RESOURCESPACE_URL") != ""));
if (isset($remote_config_url, $remote_config_key) && (isset($_SERVER["HTTP_HOST"]) || getenv("RESOURCESPACE_URL") != "")) {
debug("[boot.php] \$remote_config_url = {$remote_config_url}");
sql_connect(); # Connect a little earlier
if (isset($_SERVER['HTTP_HOST'])) {
$host = $_SERVER['HTTP_HOST'];
} else {
// If running scripts from command line the host will not be available and will need to be set as an environment variable
// e.g. export RESOURCESPACE_URL="www.yourresourcespacedomain.com";cd /var/www/pages/tools; php update_checksums.php
$host = getenv("RESOURCESPACE_URL");
}
$hostmd = md5($host);
debug("[boot.php] \$host = {$host}");
debug("[boot.php] \$hostmd = {$hostmd}");
# Look for configuration for this host (supports multiple hosts)
$remote_config_sysvar = "remote-config-" . $hostmd; # 46 chars (column is 50)
$remote_config = get_sysvar($remote_config_sysvar);
$remote_config_expiry = get_sysvar("remote_config-exp" . $hostmd, 0);
if ($remote_config !== false && $remote_config_expiry > time() && !isset($_GET["reload_remote_config"])) {
# Local cache exists and has not expired. Use this copy.
debug("[boot.php] Using local cached version of remote config. \$remote_config_expiry = {$remote_config_expiry}");
} elseif (function_exists('curl_init')) {
# Cache not present or has expired.
# Fetch new config and store. Set a very low timeout of 2 seconds so the config server going down does not take down the site.
# Attempt to fetch the remote contents but suppress errors.
if (isset($remote_config_function) && is_callable($remote_config_function)) {
$rc_url = $remote_config_function($remote_config_url, $host);
} else {
$rc_url = $remote_config_url . "?host=" . urlencode($host) . "&sign=" . md5($remote_config_key . $host);
}
$ch = curl_init();
$checktimeout = 2;
curl_setopt($ch, CURLOPT_URL, $rc_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, $checktimeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $checktimeout);
curl_setopt(
$ch,
CURLOPT_USERAGENT,
sprintf('ResourceSpace/%s Remote config for %s', mb_strcut($productversion, 4), $host)
);
$r = curl_exec($ch);
if (!curl_errno($ch)) {
# Fetch remote config was a success.
# Validate the return to make sure it's an expected config file
# The last 33 characters must be a hash and the sign of the previous characters.
if (isset($remote_config_decode) && is_callable($remote_config_decode)) {
$r = $remote_config_decode($r);
}
$sign = substr($r, -32); # Last 32 characters is a signature
$r = substr($r, 0, strlen($r) - 33);
if ($sign === md5($remote_config_key . $r)) {
$remote_config = $r;
set_sysvar($remote_config_sysvar, $remote_config);
} else {
# Validation of returned config failed. Possibly the remote config server is misconfigured or having issues.
# Do nothing; proceed with old config and try again later.
debug('[boot.php][warn] Failed to authenticate the signature of the remote config');
}
} else {
# The attempt to fetch the remote configuration failed.
# Do nothing; the cached copy will be used and we will try again later.
$errortext = curl_strerror(curl_errno($ch));
debug("[boot.php][warn] Remote config check failed from '" . $remote_config_url . "' : " . $errortext . " : " . $r);
}
curl_close($ch);
set_sysvar("remote_config-exp" . $hostmd, time() + (60 * 10)); # Load again (or try again if failed) in ten minutes
}
# Load and use the config
eval($remote_config);
// Cleanup
unset($remote_config_function, $remote_config_url, $remote_config_key);
}
if ($system_download_config_force_obfuscation && !defined("SYSTEM_DOWNLOAD_CONFIG_FORCE_OBFUSCATION")) {
// If this has been set in config.php it cannot be overridden by re.g group overrides
define("SYSTEM_DOWNLOAD_CONFIG_FORCE_OBFUSCATION", true);
}
#
# End of remote config support
# ---------------------------------------------------------------------------------------------
// Remove stream wrappers that aren't needed to reduce security vulnerabilities.
$wrappers = stream_get_wrappers();
foreach (UNREGISTER_WRAPPERS as $unregwrapper) {
if (in_array($unregwrapper, $wrappers)) {
stream_wrapper_unregister($unregwrapper);
}
}
if (!isset($suppress_headers) || !$suppress_headers) {
$default_csp_fa = "'self'";
if ($csp_frame_ancestors === [] && isset($xframe_options) && $xframe_options !== '') {
// Set CSP frame-ancestors based on legacy $xframe_options config
switch ($xframe_options) {
case "DENY":
$frame_ancestors = ["'none'"];
break;
case (bool) strpos($xframe_options, "ALLOW-FROM"):
$frame_ancestors = explode(" ", substr($xframe_options, 11));
break;
default:
$frame_ancestors = [$default_csp_fa];
break;
}
} else {
$frame_ancestors = $csp_frame_ancestors;
}
if (in_array("'none'", $frame_ancestors)) {
$frame_ancestors = ["'none'"];
} else {
array_unshift($frame_ancestors, $default_csp_fa);
}
header('Content-Security-Policy: frame-ancestors ' . implode(" ", array_unique(trim_array($frame_ancestors))));
}
if ($system_down_redirect && getval('show', '') === '') {
redirect($baseurl . '/pages/system_down.php?show=true');
}
# Set time limit
set_time_limit($php_time_limit);
# Set the storage directory and URL if not already set.
$storagedir ??= dirname(__DIR__) . '/filestore';
$storageurl ??= "{$baseurl}/filestore";
// Reset prepared statement cache before reconnecting
unset($prepared_statement_cache);
sql_connect();
// Set system to read only mode
if (isset($system_read_only) && $system_read_only) {
$global_permissions_mask = "a,t,c,d,e0,e1,e2,e-1,e-2,i,n,h,q,u,dtu,hdta";
$global_permissions_mask .= ',ert' . implode(',ert', array_column(get_all_resource_types(), 'ref'));
$global_permissions = "p";
$mysql_log_transactions = false;
$enable_collection_copy = false;
}
# Automatically set a HTTPS URL if running on the SSL port.
if (isset($_SERVER["SERVER_PORT"]) && $_SERVER["SERVER_PORT"] == 443) {
$baseurl = str_replace("http://", "https://", $baseurl);
}
# Set a base URL part consisting of the part after the server name, i.e. for absolute URLs and cookie paths.
$baseurl = str_replace(" ", "%20", $baseurl);
$bs = explode("/", $baseurl);
$bs = array_slice($bs, 3);
$baseurl_short = "/" . join("/", $bs) . (count($bs) > 0 ? "/" : "");
# statistics
$querycount = 0;
$querytime = 0;
$querylog = array();
# -----------LANGUAGES AND PLUGINS-------------------------------
$legacy_plugins = $plugins; # Make a copy of plugins activated via config.php
# Check that manually (via config.php) activated plugins are included in the plugins table.
foreach ($plugins as $plugin_name) {
if (
$plugin_name != ''
&& ps_value("SELECT inst_version AS value FROM plugins WHERE name=?", array("s",$plugin_name), '', "plugins") == ''
) {
# Installed plugin isn't marked as installed in the DB. Update it now.
# Check if there's a plugin.yaml file to get version and author info.
$p_y = get_plugin_yaml($plugin_name, false);
# Write what information we have to the plugin DB.
ps_query(
"REPLACE plugins(inst_version, author, descrip, name, info_url, update_url, config_url, priority, disable_group_select, title, icon) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
array
(
"s",$p_y['version'],
"s",$p_y['author'],
"s",$p_y['desc'],
"s",$plugin_name,
"s",$p_y['info_url'],
"s",$p_y['update_url'],
"s",$p_y['config_url'],
"s",$p_y['default_priority'],
"s",$p_y['disable_group_select'],
"s",$p_y['title'],
"s",$p_y['icon']
)
);
clear_query_cache("plugins");
}
}
# Need verbatim queries for this query
$active_plugins = get_active_plugins();
$active_yaml = array();
$plugins = array();
foreach ($active_plugins as $plugin) {
# Check group access && YAML, only enable for global access at this point
$py = get_plugin_yaml($plugin["name"], false);
array_push($active_yaml, $py);
if ($py['disable_group_select'] || $plugin['enabled_groups'] == '') {
# Add to the plugins array if not already present which is what we are working with
$plugins[] = $plugin['name'];
}
}
for ($n = count($active_plugins) - 1; $n >= 0; $n--) {
$plugin = $active_plugins[$n];
# Check group access && YAML, only enable for global access at this point
$py = get_plugin_yaml($plugin["name"], false);
if ($py['disable_group_select'] || $plugin['enabled_groups'] == '') {
include_plugin_config($plugin['name'], $plugin['config'], $plugin['config_json']);
}
}
// Load system wide config options from database and then store them to distinguish between the system wide and user preference
process_config_options(array());
$system_wide_config_options = get_defined_vars();
# Include the appropriate language file
$pagename = safe_file_name(str_replace(".php", "", pagename()));
// Allow plugins to set $language from config as we cannot run hooks at this point
if (!isset($language)) {
$language = setLanguage();
}
# Fix due to rename of US English language file
if (isset($language) && $language == "us") {
$language = "en-US";
}
# Always include the english pack (in case items have not yet been translated)
include __DIR__ . "/../languages/en.php";
if ($language != "en") {
if (substr($language, 2, 1) != '-') {
$language = substr($language, 0, 2);
}
$use_error_exception_cache = $GLOBALS["use_error_exception"] ?? false;
$GLOBALS["use_error_exception"] = true;
try {
include __DIR__ . "/../languages/" . safe_file_name($language) . ".php";
} catch (Throwable $e) {
debug("Unable to include language file $language.php");
}
$GLOBALS["use_error_exception"] = $use_error_exception_cache;
}
# Register all plugins
for ($n = 0; $n < count($plugins); $n++) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin($plugins[$n]);
}
# Register their languages in reverse order
for ($n = count($plugins) - 1; $n >= 0; $n--) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin_language($plugins[$n]);
}
global $suppress_headers;
# Set character set.
if (($pagename != "download") && ($pagename != "graph") && !$suppress_headers) {
header("Content-Type: text/html; charset=UTF-8");
} // Make sure we're using UTF-8.
#------------------------------------------------------
# ----------------------------------------------------------------------------------------------------------------------
# Basic CORS and CSRF protection
#
if (($iiif_enabled || hook('directdownloadaccess')) && $pagename == "download") {
// Required as direct links to media files may be served through download.php
// and may fail without the Access-Control-Allow-Origin header being set
$CORS_whitelist[] = $_SERVER['HTTP_ORIGIN'] ?? ($_SERVER['HTTP_REFERER'] ?? "");
}
if ($CSRF_enabled && PHP_SAPI != 'cli' && !$suppress_headers && !in_array($pagename, $CSRF_exempt_pages)) {
/*
Based on OWASP: General Recommendations For Automated CSRF Defense
(https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet)
==================================================================
# Verifying Same Origin with Standard Headers
There are two steps to this check:
1. Determining the origin the request is coming from (source origin)
2. Determining the origin the request is going to (target origin)
# What to do when Both Origin and Referer Headers Aren't Present
If neither of these headers is present, which should be VERY rare, you can either accept or block the request.
We recommend blocking, particularly if you aren't using a random CSRF token as your second check. You might want to
log when this happens for a while and if you basically never see it, start blocking such requests.
# Verifying the Two Origins Match
Once you've identified the source origin (from either the Origin or Referer header), and you've determined the target
origin, however you choose to do so, then you can simply compare the two values and if they don't match you know you
have a cross-origin request.
*/
$CSRF_source_origin = '';
$CSRF_target_origin = parse_url($baseurl, PHP_URL_SCHEME) . '://' . parse_url($baseurl, PHP_URL_HOST);
$CORS_whitelist = array_merge(array($CSRF_target_origin), $CORS_whitelist);
// Determining the origin the request is coming from (source origin)
if (isset($_SERVER['HTTP_ORIGIN'])) {
$CSRF_source_origin = $_SERVER['HTTP_ORIGIN'];
}
if ($CSRF_source_origin === '') {
debug('WARNING: Automated CSRF protection could not detect "Origin" or "Referer" headers in the request!');
debug("CSRF: Logging attempted request: {$_SERVER['REQUEST_URI']}");
// If source origin cannot be obtained, set to base URL. The reason we can do this is because we have a second
// check on the CSRF Token, so if this is a malicious request, the CSRF Token validation will fail.
// This can also be a genuine request when users go to ResourceSpace straight to login/ home page.
$CSRF_source_origin = $baseurl;
}
$CSRF_source_origin = parse_url($CSRF_source_origin, PHP_URL_SCHEME) . '://' . parse_url($CSRF_source_origin, PHP_URL_HOST);
debug("CSRF: \$CSRF_source_origin = {$CSRF_source_origin}");
debug("CSRF: \$CSRF_target_origin = {$CSRF_target_origin}");
// Whitelist match?
$cors_is_origin_allowed=cors_is_origin_allowed($CSRF_source_origin, $CORS_whitelist);
// Verifying the Two Origins Match
if (
!hook('modified_cors_process')
&& $CSRF_source_origin !== $CSRF_target_origin && !$cors_is_origin_allowed
) {
debug("CSRF: Cross-origin request detected and not white listed!");
debug("CSRF: Logging attempted request: {$_SERVER['REQUEST_URI']}");
http_response_code(403);
exit();
}
// Add CORS headers.
if ($cors_is_origin_allowed) {
debug("CORS: Origin: {$CSRF_source_origin}");
debug("CORS: Access-Control-Allow-Origin: {$CSRF_source_origin}");
header("Origin: {$CSRF_target_origin}");
header("Access-Control-Allow-Origin: {$CSRF_source_origin}");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Authorization, Content-Type");
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
}
header('Vary: Origin');
}
#
# End of basic CORS and automated CSRF protection
# ----------------------------------------------------------------------------------------------------------------------
set_watermark_image();
// Facial recognition setup
if ($facial_recognition) {
include_once __DIR__ . '/facial_recognition_functions.php';
$facial_recognition_active = initFacialRecognition();
} else {
$facial_recognition_active = false;
}
if (!$disable_geocoding) {
include_once __DIR__ . '/map_functions.php';
}
# Pre-load all text for this page.
global $site_text;
lang_load_site_text($lang, $pagename, $language);
# Blank the header insert
$headerinsert = "";
# Load the sysvars into an array. Useful so we can check migration status etc.
# Needs to be actioned before the 'initialise' hook or plugins can't use get_sysvar()
$systemvars = ps_query("SELECT name, value FROM sysvars", array(), "sysvars");
$sysvars = array();
foreach ($systemvars as $systemvar) {
$sysvars[$systemvar["name"]] = $systemvar["value"];
}
# Initialise hook for plugins
hook("initialise");
# Load the language specific stemming algorithm, if one exists
$stemming_file = __DIR__ . "/../lib/stemming/" . safe_file_name($defaultlanguage) . ".php"; # Important - use the system default language NOT the user selected language, because the stemmer must use the system defaults when indexing for all users.
if (file_exists($stemming_file)) {
include_once $stemming_file;
}
# Global hook cache and related hits counter
$hook_cache = array();
$hook_cache_hits = 0;
# Build array of valid upload paths
$valid_upload_paths = $valid_upload_paths ?? [];
$valid_upload_paths[] = $storagedir;
if (!empty($syncdir)) {
$valid_upload_paths[] = $syncdir;
}
if (!empty($batch_replace_local_folder)) {
$valid_upload_paths[] = $batch_replace_local_folder;
}
if (isset($tempdir)) {
$valid_upload_paths[] = $tempdir;
}
// IMPORTANT: make sure the upgrade.php is the last line in this file
include_once __DIR__ . '/../upgrade/upgrade.php';

6947
include/collections_functions.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,523 @@
<?php
/**
* Write comments to the database, also deals with hiding and flagging comments
*
* @return void
*/
function comments_submit()
{
global $username, $anonymous_login, $userref, $regex_email, $comments_max_characters, $lang, $email_notify, $comments_email_notification_address;
if (
$username == $anonymous_login
&& (getval("fullname", "") == ""
|| preg_match("/{$regex_email}/", getval("email", "")) === false)
) {
return;
}
$comment_to_hide = getval("comment_to_hide", 0, true);
if (($comment_to_hide != 0) && (checkPerm("o"))) {
$root = find_root_comment($comment_to_hide);
// Does this comment have any child comments?
if (ps_value("SELECT ref AS value FROM comment WHERE ref_parent = ?", array("i",$comment_to_hide), '') != '') {
ps_query("UPDATE comment SET hide = 1 WHERE ref = ?", array("i",$comment_to_hide));
} else {
ps_query("DELETE FROM comment WHERE ref = ?", array("i",$comment_to_hide));
}
if (!is_null($root)) {
clean_comment_tree($root);
}
return;
}
$comment_flag_ref = getval("comment_flag_ref", 0, true);
// --- process flag request
if ($comment_flag_ref != 0) {
$comment_flag_reason = getval("comment_flag_reason", "");
$comment_flag_url = getval("comment_flag_url", "");
if ($comment_flag_reason == "" || $comment_flag_url == "") {
return;
}
# the following line can be simplified using strstr (with before_needle boolean) but not supported < PHP 5.3.0
if (!strpos($comment_flag_url, "#") === false) {
$comment_flag_url = substr($comment_flag_url, 0, strpos($comment_flag_url, "#") - 1);
}
$comment_flag_url .= "#comment{$comment_flag_ref}"; // add comment anchor to end of URL
$comment_body = ps_query("select body from comment where ref=?", array("i",$comment_flag_ref));
$comment_body = (!empty($comment_body[0]['body'])) ? $comment_body[0]['body'] : "";
if ($comment_body == "") {
return;
}
$email_subject = (text("comments_flag_notification_email_subject") != "") ?
text("comments_flag_notification_email_subject") : $lang['comments_flag-email-default-subject'];
$email_body = (text("comments_flag_notification_email_body") != "") ?
text("comments_flag_notification_email_body") : $lang['comments_flag-email-default-body'];
$email_body .= "\r\n\r\n\"{$comment_body}\"";
$email_body .= "\r\n\r\n{$comment_flag_url}";
$email_body .= "\r\n\r\n{$lang['comments_flag-email-flagged-by']} {$username}";
$email_body .= "\r\n\r\n{$lang['comments_flag-email-flagged-reason']} \"{$comment_flag_reason}\"";
$email_to = (
empty($comments_email_notification_address)
// (preg_match ("/{$regex_email}/", $comments_email_notification_address) === false) // TODO: make this regex better
) ? $email_notify : $comments_email_notification_address;
rs_setcookie("comment{$comment_flag_ref}flagged", "true");
$_POST["comment{$comment_flag_ref}flagged"] = "true"; // we set this so that the subsequent getval() function will pick up this comment flagged in the show comments function (headers have already been sent before cookie set)
send_mail($email_to, $email_subject, $email_body);
return;
}
// --- process comment submission
// we don't want to insert an empty comment or an orphan
if (
(getval("body", "") == "")
|| (
(getval("collection_ref", "") == "")
&& (getval("resource_ref", "") == "")
&& (getval("ref_parent", "") == "")
)
) {
return;
}
if ($username == $anonymous_login) { // anonymous user
$sql_fields = "fullname, email, website_url";
$sql_values = array(
"s", getval("fullname", "") ,
"s", getval("email", ""),
"s", getval("website_url", "")
);
} else {
$sql_fields = "user_ref";
$sql_values = array("i", (int)$userref);
}
$body = getval("body", "");
if (strlen($body) > $comments_max_characters) {
$body = substr($body, 0, $comments_max_characters); // just in case not caught in submit form
}
$parent_ref = getval("ref_parent", 0, true);
$collection_ref = getval("collection_ref", 0, true);
$resource_ref = getval("resource_ref", 0, true);
$sql_values_prepend = array(
"i", ($parent_ref == 0 ? null : (int)$parent_ref),
"i", ($collection_ref == 0 ? null : (int)$collection_ref),
"i", ($resource_ref == 0 ? null : (int)$resource_ref)
);
$sql_values = array_merge($sql_values_prepend, $sql_values, array("s",$body));
ps_query("insert into comment (ref_parent, collection_ref, resource_ref, {$sql_fields}, body) values (" . ps_param_insert(count($sql_values) / 2) . ")", $sql_values);
// Notify anyone tagged.
comments_notify_tagged($body, $userref, $resource_ref, $collection_ref);
}
/**
* Check all comments that are children of the comment ref provided. If there is a branch made up entirely of hidden comments then remove the branch.
*
* @param int $ref Ref of the comment that is being deleted.
*
* @return int Number of child comments that are not hidden.
*/
function clean_comment_tree($ref)
{
$all_comments = ps_query("SELECT " . columns_in("comment") . " FROM comment WHERE ref_parent = ?", ['i', $ref]);
$remaining = 0;
if (count($all_comments) > 0) {
foreach ($all_comments as $comment) {
$remaining += clean_comment_tree($comment['ref']);
if ($remaining == 0 && $comment['hide'] == 1) {
ps_query("DELETE FROM comment WHERE ref = ?", ['i', $comment['ref']]);
}
}
}
$remaining += ps_value("SELECT count(*) as `value` FROM comment WHERE ref_parent = ? and hide = 0", ['i', $ref], 0);
if ($remaining == 0) {
ps_query("DELETE FROM comment WHERE hide = 1 and ref = ?", ['i', $ref]);
}
return $remaining;
}
/**
* Find the root of a comment tree that the ref provided is a part of
*
* @param int $ref ref of a comment
*
* @return int|null ref of the root comment or null if the comment tree has been completely removed / the comment being checked has already been deleted.
*/
function find_root_comment($ref)
{
$comment = ps_query('SELECT ref, ref_parent FROM comment WHERE ref = ?', ['i', $ref]);
if (is_array($comment) && !empty($comment)) {
$comment = $comment[0];
if (!empty($comment['ref_parent'])) {
return find_root_comment($comment['ref_parent']);
}
return $comment['ref'];
}
return null;
}
/**
* Parse a comment and replace and add links to any user, resource and collection tags
*
* @param string $text The input text e.g. the body of the comment
*
*/
function comments_tags_to_links($text): string
{
global $baseurl_short;
$text = preg_replace('/@(\S+)/s', '<a href="[BASEURLSHORT]pages/user/user_profile.php?username=$1">@$1</a>', $text);
$text = preg_replace('/r([0-9]{1,})/si', '<a href="[BASEURLSHORT]?r=$1">r$1</a>', $text); # r12345 to resource link
$text = preg_replace('/c([0-9]{1,})/si', '<a href="[BASEURLSHORT]?c=$1">c$1</a>', $text); # c12345 to collection link
$text = str_replace("[BASEURLSHORT]", $baseurl_short, $text); // Replacing this earlier can cause issues
return $text;
}
/**
* Display all comments for a resource or collection
*
* @param integer $ref The reference of the resource, collection or the comment (if called from itself recursively)
* @param boolean $bcollection_mode false == show comments for resources, true == show comments for collection
* @param boolean $bRecursive Recursively show comments, defaults to true, will be set to false if depth limit reached
* @param integer $level Used for recursion for display indentation etc.
*
* @return void
*/
function comments_show($ref, $bcollection_mode = false, $bRecursive = true, $level = 1)
{
if (!is_numeric($ref)) {
return false;
}
global $baseurl_short, $username, $anonymous_login, $lang, $comments_max_characters, $comments_flat_view, $regex_email, $comments_show_anonymous_email_address;
$anonymous_mode = (empty($username) || $username == $anonymous_login); // show extra fields if commenting anonymously
if ($comments_flat_view) {
$bRecursive = false;
}
$bRecursive = $bRecursive && ($level < $GLOBALS['comments_responses_max_level']);
// set 'name' to either user.fullname, comment.fullname or default 'Anonymous'
$sql = "select c.ref thisref, c.ref_parent, c.hide, c.created, c.body, c.website_url, c.email, u.username, u.ref, u.profile_image, parent.created 'responseToDateTime', " .
"IFNULL(IFNULL(c.fullname, u.fullname), ?) 'name' ," .
"IFNULL(IFNULL(parent.fullname, uparent.fullname), ?) 'responseToName' " .
"from comment c left join (user u) on (c.user_ref = u.ref) left join (comment parent) on (c.ref_parent = parent.ref) left join (user uparent) on (parent.user_ref = uparent.ref) ";
$sql_values = [
's', $lang['comments_anonymous-user'],
's', $lang['comments_anonymous-user'],
];
$collection_ref = ($bcollection_mode) ? $ref : "";
$resource_ref = ($bcollection_mode) ? "" : $ref;
$collection_mode = $bcollection_mode ? "collection_mode=true" : "";
if ($level == 1) {
// pass this JS function the "this" from the submit button in a form to post it via AJAX call, then refresh the "comments_container"
echo<<<EOT
<script src="{$baseurl_short}js/tagging.js"></script>
<script type="text/javascript">
var regexEmail = new RegExp ("{$regex_email}");
function validateAnonymousComment(obj) {
return (
regexEmail.test (String(obj.email.value).trim()) &&
String(obj.fullname.value).trim() != "" &&
validateComment(obj)
)
}
function validateComment(obj) {
return (String(obj.body.value).trim() != "");
}
function validateAnonymousFlag(obj) {
return (
regexEmail.test (String(obj.email.value).trim()) &&
String(obj.fullname.value).trim() != "" &&
validateFlag(obj)
)
}
function validateFlag(obj) {
return (String(obj.comment_flag_reason.value).trim() != "");
}
function submitForm(obj) {
jQuery.post(
'{$baseurl_short}pages/ajax/comments_handler.php?ref={$ref}&collection_mode={$collection_mode}',
jQuery(obj).serialize(),
function(data)
{
jQuery('#comments_container').replaceWith(data);
}
);
}
</script>
<div id="comments_container">
<div id="comment_form" class="comment_form_container">
<form class="comment_form" action="javascript:void(0);" method="">
EOT;
generateFormToken("comment_form");
hook("beforecommentbody");
$api_native_csrf_gu = generate_csrf_data_for_api_native_authmode('get_users');
echo <<<EOT
<input id="comment_form_collection_ref" type="hidden" name="collection_ref" value="{$collection_ref}"></input>
<input id="comment_form_resource_ref" type="hidden" name="resource_ref" value="{$resource_ref}"></input>
<textarea class="CommentFormBody" id="comment_form_body" name="body" maxlength="{$comments_max_characters}" placeholder="{$lang['comments_body-placeholder']}" onkeyup="TaggingProcess(this)" {$api_native_csrf_gu}></textarea>
EOT;
if ($anonymous_mode) {
echo <<<EOT
<br />
<input class="CommentFormFullname" id="comment_form_fullname" type="text" name="fullname" placeholder="{$lang['comments_fullname-placeholder']}"></input>
<input class="CommentFormEmail" id="comment_form_email" type="text" name="email" placeholder="{$lang['comments_email-placeholder']}"></input>
<input class="CommentFormWebsiteURL" id="comment_form_website_url" type="text" name="website_url" placeholder="{$lang['comments_website-url-placeholder']}"></input>
EOT;
}
$validateFunction = $anonymous_mode ? "if (validateAnonymousComment(this.parentNode))" : "if (validateComment(this.parentNode))";
echo<<<EOT
<br />
<input class="CommentFormSubmit" type="submit" value="{$lang['comments_submit-button-label']}" onClick="{$validateFunction} { submitForm(this.parentNode) } else { alert ('{$lang['comments_validation-fields-failed']}'); } ;"></input>
</form>
</div> <!-- end of comment_form -->
EOT;
$sql .= $bcollection_mode ? "where c.collection_ref=?" : "where c.resource_ref=?"; // first level will look for either collection or resource comments
$sql_values = array_merge($sql_values, array("i",$ref));
if (!$comments_flat_view) {
$sql .= " and c.ref_parent is NULL";
}
} else {
$sql .= "where c.ref_parent=?"; // look for child comments, regardless of what type of comment
$sql_values = array_merge($sql_values, array("i",$ref));
}
$sql .= " order by c.created desc";
$found_comments = ps_query($sql, $sql_values);
foreach ($found_comments as $comment) {
$thisRef = $comment['thisref'];
echo "<div class='CommentEntry' id='comment{$thisRef}' style='margin-left: " . ($level - 1) * 50 . "px;'>"; // indent for levels - this will always be zero if config $comments_flat_view=true
# ----- Information line
hook("beforecommentinfo", "all", array("ref" => $comment["ref"]));
echo "<div class='CommentEntryInfoContainer'>";
echo "<div class='CommentEntryInfo'>";
if ($comment['profile_image'] != "" && $anonymous_mode != true) {
echo "<div><img src='" . get_profile_image("", $comment['profile_image']) . "' id='CommentProfileImage'></div>";
}
echo "<div class='CommentEntryInfoCommenter'>";
if (empty($comment['name'])) {
$comment['name'] = $comment['username'];
}
if ($anonymous_mode) {
echo "<div class='CommentEntryInfoCommenterName'>" . escape($comment['name']) . "</div>";
} else {
echo "<a href='" . $baseurl_short . "pages/user/user_profile.php?username=" . escape((string)$comment['username']) . "'><div class='CommentEntryInfoCommenterName'>" . escape($comment['name']) . "</div></a>";
}
if ($comments_show_anonymous_email_address && !empty($comment['email'])) {
echo "<div class='CommentEntryInfoCommenterEmail'>" . escape($comment['email']) . "</div>";
}
if (!empty($comment['website_url'])) {
echo "<div class='CommentEntryInfoCommenterWebsite'>" . escape($comment['website_url']) . "</div>";
}
echo "</div>";
echo "<div class='CommentEntryInfoDetails'>" . date("D", strtotime($comment["created"])) . " " . nicedate($comment["created"], true, true, true) . " ";
echo "</div>"; // end of CommentEntryInfoDetails
echo "</div>"; // end of CommentEntryInfoLine
echo "</div>"; // end CommentEntryInfoContainer
echo "<div class='CommentBody'>";
if ($comment['hide']) {
if (text("comments_removal_message") != "") {
echo text("comments_removal_message");
} else {
echo "[" . escape($lang["deleted"]) . "]";
}
} else {
echo comments_tags_to_links(escape($comment['body']));
}
echo "</div>";
# ----- Form area
$validateFunction = $anonymous_mode ? "if (validateAnonymousFlag(this.parentNode))" : "if (validateFlag(this.parentNode))";
if (!getval("comment{$thisRef}flagged", "")) {
echo<<<EOT
<div id="CommentFlagContainer{$thisRef}" style="display: none;">
<form class="comment_form" action="javascript:void(0);" method="">
<input type="hidden" name="comment_flag_ref" value="{$thisRef}"></input>
<input type="hidden" name="comment_flag_url" value=""></input>
EOT;
hook("beforecommentflagreason");
generateFormToken("comment_form");
echo <<<EOT
<textarea class="CommentFlagReason" maxlength="{$comments_max_characters}" name="comment_flag_reason" placeholder="{$lang['comments_flag-reason-placeholder']}"></textarea><br />
EOT;
if ($anonymous_mode) { ?>
<input class="CommentFlagFullname"
id="comment_flag_fullname"
type="text"
name="fullname"
placeholder="<?php echo escape($lang['comments_fullname-placeholder']); ?>">
</input>
<input class="CommentFlagEmail"
id="comment_flag_email"
type="text"
name="email"
placeholder="<?php echo escape($lang['comments_email-placeholder']); ?>">
</input>
<br />
<?php }
echo<<<EOT
<input class="CommentFlagSubmit" type="submit" value="{$lang['comments_submit-button-label']}" onClick="comment_flag_url.value=document.URL; {$validateFunction} { submitForm(this.parentNode); } else { alert ('{$lang['comments_validation-fields-failed']}') }"></input>
</form>
</div>
EOT;
}
if (!$comment['hide']) {
$respond_button_id = "comment_respond_button_" . $thisRef;
$respond_div_id = "comment_respond_" . $thisRef;
echo "<div id='{$respond_button_id}' class='CommentRespond'>"; // start respond div
echo "<a href='javascript:void(0)' onClick='
jQuery(\"#comment_form\").clone().attr(\"id\",\"{$respond_div_id}\").css(\"margin-left\",\"" . ($level * 50) . 'px")' . ".insertAfter(\"#comment$thisRef\");
jQuery(\"<input>\").attr({type: \"hidden\", name: \"ref_parent\", value: \"$thisRef\"}).appendTo(\"#{$respond_div_id} .comment_form\");
jQuery(\"#{$respond_button_id} a\").removeAttr(\"onclick\");
'>" . '<i aria-hidden="true" class="fa fa-reply"></i>&nbsp;' . $lang['comments_respond-to-this-comment'] . "</a>";
echo "</div>"; // end respond
echo "<div class='CommentEntryInfoFlag'>";
if (getval("comment{$thisRef}flagged", "")) {
echo '<div class="CommentFlagged"><i aria-hidden="true" class="fa fa-fw fa-flag">&nbsp;</i>'
. escape($lang['comments_flag-has-been-flagged'])
. '</div>';
} else {
echo<<<EOT
<div class="CommentFlag">
<a href="javascript:void(0)" onclick="jQuery('#CommentFlagContainer{$thisRef}').toggle('fast');" ><i aria-hidden="true" class="fa fa-fw fa-flag">&nbsp;</i>{$lang['comments_flag-this-comment']}</a>
</div>
EOT;
}
if (checkPerm("o")) {
?>
<form class="comment_removal_form">
<?php generateFormToken("comment_removal_form"); ?>
<input type="hidden" name="comment_to_hide" value="<?php echo escape($thisRef); ?>"></input>
<a href="javascript:void(0)" onclick="if (confirm ('<?php echo escape($lang['comments_hide-comment-text-confirm']); ?>')) submitForm(this.parentNode);"><?php echo '<i aria-hidden="true" class="fa fa-trash-alt"></i>&nbsp;' . $lang['comments_hide-comment-text-link']; ?></a>
</form>
<?php
}
echo "</div>"; // end of CommentEntryInfoFlag
}
echo "</div>"; // end of CommentEntry
if ($bRecursive) {
comments_show($thisRef, $bcollection_mode, true, $level + 1);
}
}
if ($level == 1) {
echo "</div>"; // end of comments_container
}
}
/**
* Notify anyone tagged when a new comment is posted
*
* @param string $comment The comment body
* @param integer $from_user Who posted the comment
* @param integer $resource If commenting on a resource, the resource ID
* @param integer $collection If commenting on a collection, the collection ID
*
* @return void
*/
function comments_notify_tagged($comment, $from_user, $resource = null, $collection = null)
{
// Find tagged users.
$success = preg_match_all("/@.*? /", $comment . " ", $tagged, PREG_PATTERN_ORDER);
if (!$success) {
return true;
} // Nothing to do, return out.
foreach ($tagged[0] as $tag) {
$tag = substr($tag, 1);
$tag = trim($tag); // Get just the username.
$user = get_user_by_username($tag); // Find the matching user ID
// No match but there's an underscore? Try replacing the underscore with a space and search again. Spaces are swapped to underscores when tagging.
if ($user === false) {
$user = get_user_by_username(str_replace("_", " ", $tag));
}
if ($user > 0) {
// Notify them.
// Build a URL based on whether this is a resource or collection
global $baseurl,$userref,$lang;
$url = $baseurl . "/?" . (is_null($resource) ? "c" : "r") . "=" . (is_null($resource) ? $collection : $resource);
// Send the message.
message_add(array($user), $lang["tagged_notification"] . " " . $comment, $url, $userref);
}
}
return true;
}

View File

@@ -0,0 +1,63 @@
<!--Begin Resource Comments -->
<div class="RecordBox">
<div class="RecordPanel">
<div id="Comments">
<div id="CommentsPanelHeader">
<div id="CommentsPanelHeaderRow">
<div id="CommentsPanelHeaderRowTitle">
<div class="Title"><?php echo escape($lang['comments_box-title']); ?></div>
</div>
<?php if ($comments_policy_enable) { ?>
<div id="CommentsPanelHeaderRowPolicyLink">
<?php
if (isset($comments_policy_external_url) && $comments_policy_external_url != "") {
echo "<a href='$comments_policy_external_url' target='_blank'>"
. LINK_CARET
. escape($lang['comments_box-policy'])
. '</a>';
} else {
if (text("comments_policy") != "") {
echo "<a href='content.php?content=comments_policy' target='_blank'>"
. LINK_CARET
. escape($lang['comments_box-policy'])
. '</a>';
} else {
// show placeholder only if user has permission to change site text to sort it
if (checkPerm("o")) {
echo "<a href=\"javascript: void(0)\" onclick=\"alert ('"
. escape($lang['comments_box-policy-placeholder'])
. "}');\">"
. LINK_CARET
. escape($lang['comments_box-policy'])
. '</a>';
}
}
}
?>
</div>
<?php } ?>
</div>
</div>
<div id="CommentsContainer">
<!-- populated on completion of DOM load -->
</div>
</div>
</div>
</div>
<?php if (!$view_panels) { ?>
<script type="text/javascript">
jQuery(document).ready(function () {
jQuery("#CommentsContainer").load(
baseurl_short + "pages/ajax/comments_handler.php?ref=<?php echo $ref;?>",
function() {
if (jQuery.type(jQuery(window.location.hash)[0])!=="undefined")
jQuery(window.location.hash)[0].scrollIntoView();
}
);
});
</script>
<?php
}
?>
<!-- End Resource Comments -->

3220
include/config.default.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
<?php
/**
* This file contains the configuration settings that have been deprecated.
* They will be removed in a future release and the code will operate in line with the default values set below and code to handle the non-default case(s) will be removed.
*
* **** DO NOT ALTER THIS FILE! ****
*
* If you need to change any of the below values, copy them to config.php and change them there, although as these options will be removed in a future release, this is not advised.
*/

View File

@@ -0,0 +1,50 @@
/*
New Installation Defaults
-------------------------
The following configuration options are set for new installations only.
This provides a mechanism for enabling new features for new installations without affecting existing installations (as would occur with changes to config.default.php)
*/
// Set imagemagick default for new installs to expect the newer version with the sRGB bug fixed.
$imagemagick_colorspace = "sRGB";
$contact_link=false;
$themes_simple_view=true;
$stemming=true;
$case_insensitive_username=true;
$user_pref_user_management_notifications=true;
$themes_show_background_image = true;
$use_zip_extension=true;
$collection_download=true;
$ffmpeg_preview_force = true;
$ffmpeg_preview_extension = 'mp4';
$ffmpeg_preview_options = '-f mp4 -b:v 1200k -b:a 64k -ac 1 -c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3 -c:a aac -strict -2';
$daterange_search = true;
$upload_then_edit = true;
$purge_temp_folder_age=90;
$filestore_evenspread=true;
$comments_resource_enable=true;
$api_upload_urls = array();
$use_native_input_for_date_field = true;
$resource_view_use_pre = true;
$sort_tabs = false;
$maxyear_extends_current = 5;
$thumbs_display_archive_state = true;
$featured_collection_static_bg = true;
$file_checksums = true;
$hide_real_filepath = true;
$plugins[] = "brand_guidelines";

2301
include/config_functions.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,243 @@
<?php
/**
* Functions used (mostly) to generate the content needed for CSV files
*/
/**
* Generates the CSV content of the metadata for resources passed in the array
* The CSV is echoed to output for direct download or saved to a file
*
* @param array $resources array of resource ids to create a CSV for
* @param bool $personal flag to include only fields expected to include personal data
* @param bool $alldata flag to include extra data from the resource table
* @param string $outputfile optional file path to output CSV to
*
* @return bool|void TRUE if the file has been created, void if the data has been sent as a direct download
*/
function generateResourcesMetadataCSV(array $resources, $personal = false, $alldata = false, $outputfile = "")
{
global $lang, $csv_export_add_original_size_url_column, $file_checksums, $k, $scramble_key,
$get_resource_data_cache,$csv_export_add_data_fields;
// Write the CSV to a disk to avoid memory issues with large result sets
$tempcsv = trim($outputfile) != "" ? $outputfile : get_temp_dir() . "/csv_export_" . uniqid() . ".csv";
$csv_field_headers = array();
$csvoptions = array("csvexport" => true,"personal" => $personal,"alldata" => $alldata);
$allfields = get_resource_type_fields("", "order_by", "asc");
$cache_location = get_temp_dir();
$cache_data = array();
$restypearr = get_resource_types();
$resource_types = array();
// Sort into array with ids as keys
foreach ($restypearr as $restype) {
$resource_types[$restype["ref"]] = $restype;
}
$field_restypes = get_resource_type_field_resource_types();
// Break resources up into smaller arrays to avoid hitting memory limits
$resourcebatches = array_chunk($resources, 2000);
$csv_field_headers["resource_type"] = $lang["resourcetype"];
$csv_field_headers["status"] = $lang['status'];
$csv_field_headers["created_by"] = $lang["contributedby"];
if ($alldata) {
$csv_field_headers["access"] = $lang['access'];
}
if ($file_checksums && $alldata) {
$csv_field_headers["file_checksum"] = $lang["filechecksum"];
}
// Add original size URL column
if ($csv_export_add_original_size_url_column) {
$csv_field_headers['original_link'] = $lang['collection_download_original'];
}
for ($n = 0; $n < count($resourcebatches); $n++) {
$resources_fields_data = array();
$fullresdata = get_resource_field_data_batch($resourcebatches[$n], true, $k != '', true, $csvoptions);
// Get data for all resources
$resource_data_array = get_resource_data_batch($resourcebatches[$n]);
foreach ($resourcebatches[$n] as $resource) {
$resdata = isset($resource_data_array[$resource]) ? $resource_data_array[$resource] : false;
if (!$resdata || checkperm("T" . $resdata["resource_type"])) {
continue;
}
// Add resource type
$restype = get_resource_type_name($resdata["resource_type"]);
$resources_fields_data[$resource]["resource_type"] = $restype;
// Add resource status
$resources_fields_data[$resource]['status'] = $lang["status{$resource_data_array[$resource]['archive']}"] ?? $lang['unknown'];
if ($alldata) {
// Add resource access
$resources_fields_data[$resource]['access'] = $lang["access{$resource_data_array[$resource]['access']}"] ?? $lang['unknown'];
}
// Add contributor
$udata = get_user($resdata["created_by"]);
if ($udata !== false) {
$resources_fields_data[$resource]["created_by"] = (trim($udata["fullname"] ?? "") != "" ? $udata["fullname"] : $udata["username"]);
}
if ($alldata) {
if (isset($csv_export_add_data_fields)) {
foreach ($csv_export_add_data_fields as $addfield) {
$resources_fields_data[$resource][$addfield["column"]] = $resdata[$addfield["column"]];
$csv_field_headers[$addfield["column"]] = $addfield["title"];
}
}
if ($file_checksums) {
$resources_fields_data[$resource]["file_checksum"] = $resdata["file_checksum"];
}
}
foreach ($allfields as $restypefield) {
if (
metadata_field_view_access($restypefield["ref"])
&&
(!$personal || $restypefield["personal_data"])
&&
($alldata || $restypefield["include_in_csv_export"])
&&
(
isset($field_restypes[$restypefield["ref"]]) && in_array($resdata["resource_type"], $field_restypes[$restypefield["ref"]])
)
) {
if (!isset($csv_field_headers[$restypefield["ref"]])) {
$csv_field_headers[$restypefield["ref"]] = $restypefield['title'];
}
// Check if the resource has a value for this field in the data retrieved
if (isset($fullresdata[$resource])) {
$resdataidx = array_search($restypefield["ref"], array_column($fullresdata[$resource], 'ref'));
$fieldvalue = ($resdataidx !== false) ? $fullresdata[$resource][$resdataidx]["value"] : "";
$resources_fields_data[$resource][$restypefield['ref']] = $fieldvalue;
}
}
}
/*Provide the original URL only if we have access to the resource or the user group
doesn't have restricted access to the original size*/
$access = get_resource_access($resdata);
if (0 != $access || resource_has_access_denied_by_RT_size($resdata['resource_type'], '')) {
continue;
}
if ($csv_export_add_original_size_url_column) {
$filepath = get_resource_path($resource, true, '', false, $resdata['file_extension'], -1, 1, false, '', -1, false);
$original_link = get_resource_path($resource, false, '', false, $resdata['file_extension'], -1, 1, false, '', -1, false);
if (file_exists($filepath)) {
$resources_fields_data[$resource]['original_link'] = $original_link;
}
}
}
if (count($resources_fields_data) > 0) {
// Save data to temporay files in order to prevent memory limits being reached
$tempjson = json_encode($resources_fields_data);
$cache_data[$n] = $cache_location . "/csv_export_" . md5($scramble_key . $tempjson) . ".json"; // Scrambled path to cache
file_put_contents($cache_data[$n], $tempjson);
$tempjson = null;
}
}
$csv_field_headers = array_unique($csv_field_headers);
// Header
$header = "\"" . $lang['resourceids'] . "\",\"" . implode('","', $csv_field_headers) . "\"\n";
file_put_contents($tempcsv, $header);
// Results
for ($n = 0; $n < count($resourcebatches); $n++) {
$filedata = "";
$resources_fields_data = array();
if (file_exists($cache_data[$n])) {
$resources_fields_data = json_decode(file_get_contents($cache_data[$n]), true);
}
if (is_null($resources_fields_data)) {
$resources_fields_data = array();
}
foreach ($resources_fields_data as $resource_id => $resource_fields) {
// First column will always be Resource ID
$csv_row = $resource_id . ',';
// Field values
foreach ($csv_field_headers as $column_header => $column_header_title) {
if (!array_key_exists($column_header, $resource_fields)) {
$csv_row .= '"",';
continue;
}
foreach ($resource_fields as $field_name => $field_value) {
if ($column_header == $field_name) {
$csv_row .= '"' . str_replace(array("\""), array("\"\""), i18n_get_translated($field_value)) . '",';
}
}
}
$csv_row = rtrim($csv_row, ',');
$csv_row .= "\n";
$filedata .= $csv_row;
}
// Add this data to the file and delete disk copy of array
file_put_contents($tempcsv, $filedata, FILE_APPEND);
if (file_exists($cache_data[$n])) {
unlink($cache_data[$n]);
}
}
if ($outputfile != "") {
// Data has been saved to file, just return
return true;
}
// Echo the data for immediate download
echo file_get_contents($tempcsv);
}
/**
* Generates the file content when exporting nodes
*
* @param array $field Array containing field information (as retrieved by get_field)
* @param boolean $send_headers If true, function sends headers used for downloading content. Default is set to false
*
* @return mixed
*/
function generateNodesExport(array $field, $parent = null, $send_headers = false)
{
global $lang, $FIXED_LIST_FIELD_TYPES;
if (0 === count($field) || !isset($field['ref']) || !isset($field['type'])) {
trigger_error('Field array cannot be empty. generateNodesExport() requires at least "ref" and "type" indexes!');
}
if (!in_array($field['type'], $FIXED_LIST_FIELD_TYPES)) {
return false;
}
$return = '';
$nodes = get_nodes($field['ref'], $parent);
foreach ($nodes as $node) {
$return .= "{$node['name']}\r\n";
}
log_activity("{$lang['export']} metadata field options - field {$field['ref']}", LOG_CODE_DOWNLOADED);
if ($send_headers) {
header('Content-type: application/octet-stream');
header("Content-disposition: attachment; filename=field{$field['ref']}_nodes_export.txt");
echo $return;
ob_flush();
exit();
}
return $return;
}

2299
include/dash_functions.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,487 @@
<?php
/*
* Dash Tile Generation Functions - Montala Ltd, Jethro Dew
* These control the content for the different variations of tile type and tile style.
*
*/
/*
* Tile serving
*
*/
function tile_select($tile_type, $tile_style, $tile, $tile_id, $tile_width, $tile_height)
{
/*
* Preconfigured and the legacy tiles controlled by config.
*/
if ($tile_type == "conf") {
switch ($tile_style) {
case "thmsl":
tile_config_themeselector($tile, $tile_id, $tile_width, $tile_height);
exit;
case "custm":
tile_config_custom($tile, $tile_id, $tile_width, $tile_height);
exit;
case "pend":
tile_config_pending($tile, $tile_id, $tile_width, $tile_height);
exit;
}
}
/*
* Free Text Tile
*/
if ($tile_type == "ftxt") {
tile_freetext($tile, $tile_id, $tile_width, $tile_height);
exit;
}
/*
* Search Type tiles
*/
if ($tile_type == "srch") {
switch ($tile_style) {
case "thmbs":
$promoted_image = getval("promimg", false);
tile_search_thumbs($tile, $tile_id, $tile_width, $tile_height, $promoted_image);
exit;
case "multi":
case "blank":
tile_search_multi_or_blank($tile, $tile_id, $tile_width, $tile_height);
exit;
}
}
// Featured collection - themes specific tiles
if ('fcthm' == $tile_type) {
switch ($tile_style) {
case 'thmbs':
tile_featured_collection_thumbs($tile, $tile_id, $tile_width, $tile_height, getval('promimg', 0));
break;
case 'multi':
tile_featured_collection_multi($tile, $tile_id, $tile_width, $tile_height, getval('promimg', 0));
break;
case 'blank':
default:
tile_featured_collection_blank($tile, $tile_id);
break;
}
exit();
}
}
/*
* Config controlled panels
*
*/
function tile_config_themeselector($tile, $tile_id, $tile_width, $tile_height)
{
global $lang,$pagename,$baseurl_short, $theme_direct_jump;
$url = "{$baseurl_short}pages/collections_featured.php";
$fc_categories = get_featured_collection_categories(0, []);
if ($pagename !== 'dash_tile_preview') {
?>
<div class="featuredcollectionselector HomePanel DashTile DashTileDraggable allUsers"
tile="<?php echo escape($tile["ref"])?>"
id="<?php echo str_replace("contents_", "", escape($tile_id));?>" >
<div id="<?php echo $tile_id?>" class="HomePanelThemes HomePanelDynamicDash HomePanelIN">
<?php
} ?>
<span class="theme-icon"></span>
<a onClick="return CentralSpaceLoad(this,true);" href="<?php echo $baseurl_short?>pages/collections_featured.php">
<h2><?php echo escape($lang["themes"]); ?></h2>
</a>
<p>
<?php if (!empty($fc_categories)) { ?>
<select id="themeselect" onChange="CentralSpaceLoad(this.value,true);">
<option value=""><?php echo escape($lang["select"]); ?></option>
<?php foreach ($fc_categories as $header) { ?>
<option value="<?php echo generateURL($url, array("parent" => $header["ref"])); ?>">
<?php echo escape(i18n_get_translated($header["name"])); ?>
</option>
<?php
}
?>
</select>
<?php
}
if (!$theme_direct_jump) { ?>
<a id="themeviewall" onClick="return CentralSpaceLoad(this,true);" href="<?php echo $url; ?>">
<?php echo LINK_CARET; ?><?php echo escape($lang["viewall"]); ?>
</a>
<?php
}
?>
</p>
<?php
if ($pagename !== 'dash_tile_preview') { ?>
</div>
</div>
<?php
} ?>
<script>
jQuery("a#<?php echo str_replace("contents_", "", $tile_id);?>").replaceWith(jQuery(".featuredcollectionselector"));
</script>
<?php
}
function tile_config_custom($tile, $tile_id, $tile_width, $tile_height)
{
global $lang;
?>
<span class='search-icon'></span>
<h2><?php echo escape(i18n_get_translated($tile["title"])); ?></h2>
<p><?php echo escape(i18n_get_translated($tile["txt"])); ?></p>
<?php
}
function tile_config_pending($tile, $tile_id, $tile_width, $tile_height)
{
global $lang, $search_all_workflow_states;
$linkstring = explode('?', $tile["link"]);
parse_str(str_replace("&amp;", "&", $linkstring[1]), $linkstring);
$search = "";
$count = 1;
$restypes = "";
$order_by = "relevance";
$archive = $linkstring["archive"];
$sort = "";
$search_all_workflow_states = false;
$tile_search = do_search($search, $restypes, $order_by, $archive, $count, $sort, false, 0, false, false, "", false, false, false, true);
if (!is_array($tile_search)) {
$found_resources = false;
$count = 0;
} else {
$found_resources = true;
$count = count($tile_search);
}
// Hide if no results
if (!$found_resources || $count == 0) {
global $usertile;
$tile_element_id = isset($usertile) ? "user_tile{$usertile['ref']}" : "tile{$tile['ref']}";
?>
<style>
#<?php echo escape($tile_element_id); ?> {
display: none;
}
</style>
<?php
return;
}
?>
<span class='collection-icon'></span>
<?php if (!empty($tile['title'])) { ?>
<h2 class="title"><?php echo escape(i18n_get_translated($tile['title'])); ?></h2>
<?php } elseif (!empty($tile['txt']) && isset($lang[strtolower($tile['txt'])])) { ?>
<h2 class="title notitle"><?php echo escape($lang[strtolower($tile['txt'])]); ?></h2>
<?php } elseif (!empty($tile['txt']) && !isset($lang[strtolower($tile['txt'])])) { ?>
<h2 class="title notitle"><?php echo escape($tile['txt']); ?></h2>
<?php }
if (!empty($tile['title']) && !empty($tile['txt'])) {
if (isset($lang[strtolower($tile['txt'])])) {
?>
<p><?php echo escape($lang[strtolower($tile['txt'])]); ?></p>
<?php
} else {
?>
<p><?php echo escape(i18n_get_translated($tile['txt'])); ?></p>
<?php
}
}
?>
<p class="tile_corner_box">
<span aria-hidden="true" class="fa fa-clone"></span>
<?php echo $count; ?>
</p>
<?php
}
/*
* Freetext tile
*
*/
function tile_freetext($tile, $tile_id, $tile_width, $tile_height)
{
global $lang;
?>
<span class='help-icon'></span>
<h2><?php echo escape(i18n_get_translated($tile["title"])); ?></h2>
<p><?php echo escape(i18n_get_translated($tile["txt"])); ?></p>
<?php
generate_dash_tile_toolbar($tile, $tile_id);
}
/*
* Search linked tiles
*
*/
function tile_search_thumbs($tile, $tile_id, $tile_width, $tile_height, $promoted_image = false)
{
$search_string = explode('?', $tile["link"]);
parse_str(str_replace("&amp;", "&", $search_string[1]), $search_string);
$search = isset($search_string["search"]) ? $search_string["search"] : "";
$icon = "";
if (substr($search, 0, 11) == "!collection") {
$icon = "cube";
} elseif (substr($search, 0, 7) == "!recent" || substr($search, 0, 5) == "!last") {
$icon = "clock-o";
} else {
$icon = "search";
}
if (!empty($tile["title"])) { ?>
<h2>
<span class='fa fa-<?php echo $icon ?>'></span>
<?php echo escape(i18n_get_translated($tile["title"]));?>
</h2>
<?php
} elseif (!empty($tile["txt"])) { ?>
<h2><?php echo escape(i18n_get_translated($tile["txt"]));?></h2>
<?php
}
if (!empty($tile["title"]) && !empty($tile["txt"])) { ?>
<p><?php echo escape(i18n_get_translated($tile["txt"]));?></p>
<?php
}
tltype_srch_generate_js_for_background_and_count($tile, $tile_id, (int) $tile_width, (int) $tile_height, (int) $promoted_image);
generate_dash_tile_toolbar($tile, $tile_id);
}
function tile_search_multi_or_blank($tile, $tile_id, $tile_width, $tile_height)
{
$search_string = explode('?', $tile["link"]);
parse_str(str_replace("&amp;", "&", $search_string[1]), $search_string);
$search = isset($search_string["search"]) ? $search_string["search"] : "";
$icon = "";
if (substr($search, 0, 11) == "!collection") {
$icon = "cube";
} elseif (substr($search, 0, 7) == "!recent" || substr($search, 0, 5) == "!last") {
$icon = "clock-o";
} else {
$icon = "search";
}
if (!empty($tile["title"])) { ?>
<h2>
<span class='fa fa-<?php echo $icon ?>'></span>
<?php echo escape(i18n_get_translated($tile["title"]));?>
</h2>
<?php
} elseif (!empty($tile["txt"])) { ?>
<h2>
<span class='fa fa-<?php echo $icon ?>'></span>
<?php echo escape(i18n_get_translated($tile["txt"]));?>
</h2>
<?php
}
if (!empty($tile["title"]) && !empty($tile["txt"])) { ?>
<p><?php echo escape(i18n_get_translated($tile["txt"]));?></p>
<?php
}
tltype_srch_generate_js_for_background_and_count($tile, $tile_id, (int) $tile_width, (int) $tile_height, 0);
generate_dash_tile_toolbar($tile, $tile_id);
}
function tile_featured_collection_thumbs($tile, $tile_id, $tile_width, $tile_height, $promoted_image)
{
global $baseurl_short, $lang, $view_title_field;
if ($promoted_image > 0) {
$promoted_image_data = get_resource_data($promoted_image);
if ($promoted_image_data !== false) {
$preview_resource = $promoted_image_data;
} else {
return false; // Promoted image could not be found.
}
$preview_resource_mod = hook('modify_promoted_image_preview_resource_data', '', array($promoted_image));
if ($preview_resource_mod !== false) {
$preview_resource = $preview_resource_mod;
}
$no_preview = false;
if (
!resource_has_access_denied_by_RT_size($preview_resource['resource_type'], 'pre')
&& file_exists(get_resource_path($preview_resource['ref'], true, 'pre', false, 'jpg', -1, 1, false))
) {
$preview_path = get_resource_path($preview_resource['ref'], false, 'pre', false, 'jpg', -1, 1, false);
} else {
$preview_path = "{$baseurl_short}gfx/no_preview/default.png";
$no_preview = true;
}
?>
<img alt="<?php echo escape(i18n_get_translated(($promoted_image_data["field" . $view_title_field] ?? ""))); ?>"
src="<?php echo $preview_path; ?>"
<?php
if ($no_preview) {
?>
style="position:absolute; top:<?php echo ($tile_height - 128) / 2; ?>px;left:<?php echo ($tile_width - 128) / 2; ?>px;"
<?php
} else {
// fit image to tile size
if (($preview_resource['thumb_width'] * 0.7) >= $preview_resource['thumb_height']) {
$ratio = $preview_resource['thumb_height'] / $tile_height;
$width = $preview_resource['thumb_width'] / $ratio;
if ($width < $tile_width) {
echo 'width="100%" ';
} else {
echo 'height="100%" ';
}
} else {
$ratio = $preview_resource['thumb_width'] / $tile_width;
$height = $preview_resource['thumb_height'] / $ratio;
if ($height < $tile_height) {
echo 'height="100%" ';
} else {
echo 'width="100%" ';
}
}
?>
style="position:absolute;top:0;left:0;"
<?php
} ?>
class="thmbs_tile_img"
/>
<?php
}
?>
<h2>
<span class='fa fa-folder'></span>
<?php
if ('' != $tile['title']) {
echo escape(i18n_get_translated($tile['title']));
} elseif ('' != $tile['txt']) {
echo escape(i18n_get_translated($tile['txt']));
}
?>
</h2>
<?php
if ('' != $tile['title'] && '' != $tile['txt']) {
?>
<p><?php echo escape(i18n_get_translated($tile['txt'])); ?></p>
<?php
}
generate_dash_tile_toolbar($tile, $tile_id);
}
function tile_featured_collection_multi($tile, $tile_id, $tile_width, $tile_height, $promoted_image)
{
global $baseurl_short, $lang, $view_title_field;
$link_parts = explode('?', $tile['link']);
parse_str(str_replace('&amp;', '&', $link_parts[1]), $link_parts);
$parent = (isset($link_parts["parent"]) ? (int) validate_collection_parent(array("parent" => (int) $link_parts["parent"])) : 0);
$resources = dash_tile_featured_collection_get_resources($parent, array("limit" => 4));
if (count($resources) > 0) {
if (count($resources) == 1) {
return tile_featured_collection_thumbs($tile, $tile_id, $tile_width, $tile_height, $resources[0]['ref']);
}
$i = 0;
foreach (array_rand($resources, min(count($resources), 4)) as $random_picked_resource_key) {
$resource = $resources[$random_picked_resource_key];
$shadow = true;
if (
!resource_has_access_denied_by_RT_size($resource['resource_type'], 'pre')
&& file_exists(get_resource_path($resource['ref'], true, 'pre', false, 'jpg', -1, 1, false))
) {
$preview_path = get_resource_path($resource['ref'], false, 'pre', false, 'jpg', -1, 1, false);
} else {
$preview_path = "{$baseurl_short}gfx/no_preview/default.png";
$shadow = false;
}
$tile_working_space = ('' == $tile['tlsize'] ? 140 : 280);
$gap = $tile_working_space / min(count($resources), 4);
$space = $i * $gap;
?>
<img
alt="<?php echo escape(i18n_get_translated(($resource["field" . $view_title_field] ?? ""))); ?>"
style="
position: absolute;
top: 10px;
left: <?php echo $space * 1.5; ?>px;
height: 100%;<?php echo ($shadow) ? "box-shadow: 0 0 25px #000;" : ''; ?>;
transform: rotate(<?php echo 20 - ($i * 12); ?>deg);"
src="<?php echo $preview_path; ?>">
<?php
$i++;
}
}
?>
<h2>
<span class='fa fa-folder'></span>
<?php
if ('' != $tile['title']) {
echo escape(i18n_get_translated($tile['title']));
} elseif ('' != $tile['txt']) {
echo escape(i18n_get_translated($tile['txt']));
}
?>
</h2>
<?php
if ('' != $tile['title'] && '' != $tile['txt']) {
?>
<p><?php echo escape(i18n_get_translated($tile['txt'])); ?></p>
<?php
}
generate_dash_tile_toolbar($tile, $tile_id);
}
function tile_featured_collection_blank($tile, $tile_id)
{
global $baseurl_short, $lang;
?>
<h2>
<span class='fa fa-folder'></span>
<?php
if ('' != $tile['title']) {
echo escape(i18n_get_translated($tile['title']));
} elseif ('' != $tile['txt']) {
echo escape(i18n_get_translated($tile['txt']));
}
?>
</h2>
<?php
if ('' != $tile['title'] && '' != $tile['txt']) {
?>
<p><?php echo escape(i18n_get_translated($tile['txt'])); ?></p>
<?php
}
generate_dash_tile_toolbar($tile, $tile_id);
}

1388
include/database_functions.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
<!-- Period select -->
<div class="Question" id="date_period">
<label for="period"><?php echo escape($lang["period"]); ?></label>
<select id="period" name="period" class="stdwidth" onChange="
if (this.value==-1) {document.getElementById('DateRange').style.display='block';} else {document.getElementById('DateRange').style.display='none';}
if (this.value==0) {document.getElementById('SpecificDays').style.display='block';} else {document.getElementById('SpecificDays').style.display='none';}
if (this.value!=-1) {document.getElementById('EmailMe').style.display='block';} else {document.getElementById('EmailMe').style.display='none';}
// Copy reporting period to e-mail period
if (document.getElementById('period').value==0) {
// Copy from specific day box
document.getElementById('email_days').value=document.getElementById('period_days').value;
} else {
document.getElementById('email_days').value=document.getElementById('period').value;
}
">
<?php
foreach ($reporting_periods_default as $period_default) {
if (is_int($period_default)) {
printf(
'<option value="%s"%s>%s</option>',
$period_default,
$period_init == $period_default ? ' selected' : '',
escape(str_replace('?', $period_default, $lang['lastndays']))
);
}
}
?>
<option value="0" <?php echo ($period_init == 0) ? "selected" : ''; ?>>
<?php echo escape($lang["specificdays"]); ?>
</option>
<option value="-1" <?php echo ($period_init == -1) ? "selected" : ''; ?>>
<?php echo escape($lang["specificdaterange"]); ?>
</option>
</select>
<div class="clearerleft"></div>
</div>
<!-- Specific Days Selector -->
<div id="SpecificDays" <?php echo ($period_init != 0) ? 'style="display:none;"' : ''; ?>>
<div class="Question">
<label for="period_days"><?php echo escape($lang["specificdays"]); ?></label>
<?php
$textbox = "<input type=\"text\" id=\"period_days\" name=\"period_days\" size=\"4\" value=\"" . escape(getval("period_days", "")) . "\">";
echo str_replace("?", $textbox, escape($lang["lastndays"]));
?>
<div class="clearerleft"></div>
</div>
</div>
<!-- Specific Date Range Selector -->
<div id="DateRange" <?php echo ($period_init != -1) ? 'style="display:none;"' : ''; ?>>
<div class="Question">
<label><?php echo escape($lang["fromdate"]); ?><br/><?php echo escape($lang["inclusive"]); ?></label>
<?php
$name = "from";
$dy = getval($name . "-y", 2000);
$dm = getval($name . "-m", 1);
$dd = getval($name . "-d", 1);
?>
<select name="<?php echo $name?>-d">
<?php for ($m = 1; $m <= 31; $m++) { ?>
<option <?php echo ($m == $dd) ? "selected" : ''; ?>>
<?php echo sprintf("%02d", $m)?>
</option>
<?php } ?>
</select>
<select name="<?php echo $name?>-m">
<?php for ($m = 1; $m <= 12; $m++) { ?>
<option <?php echo ($m == $dm) ? "selected" : ''; ?> value="<?php echo sprintf("%02d", $m)?>">
<?php echo escape($lang["months"][$m - 1]); ?>
</option>
<?php } ?>
</select>
<input type=text size=5 name="<?php echo $name?>-y" value="<?php echo escape($dy); ?>">
<div class="clearerleft"></div>
</div>
<div class="Question">
<label><?php echo escape($lang["todate"]); ?><br/><?php echo escape($lang["inclusive"]); ?></label>
<?php
$name = "to";
$dy = getval($name . "-y", date("Y"));
$dm = getval($name . "-m", date("m"));
$dd = getval($name . "-d", date("d"));
?>
<select name="<?php echo $name?>-d">
<?php for ($m = 1; $m <= 31; $m++) { ?>
<option <?php echo ($m == $dd) ? "selected" : ''; ?>>
<?php echo sprintf("%02d", $m)?>
</option>
<?php } ?>
</select>
<select name="<?php echo $name?>-m">
<?php for ($m = 1; $m <= 12; $m++) { ?>
<option <?php echo ($m == $dm) ? "selected" : ''; ?> value="<?php echo sprintf("%02d", $m)?>">
<?php echo escape($lang["months"][$m - 1]); ?>
</option>
<?php } ?>
</select>
<input type=text size=5 name="<?php echo $name?>-y" value="<?php echo escape($dy); ?>">
<div class="clearerleft"> </div>
</div>
</div>
<!-- end of Date Range Selector -->

6
include/db.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
// This file exists to support legacy/external plugins that still use include/db.php
// This was added for release 10.4 and it's suggested this file is removed after a few more releases, to allow a window of
// time for third party plugin updates.
include_once __DIR__ . '/boot.php';

123
include/debug_functions.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
/**
* Check and set the debug log override status for the current user.
*
* This function determines if debug logging should be enabled based on system variables
* and the user's ID. If a debug override is set for a specific user or globally, and the
* override has not expired, debug logging will be activated. Expired overrides are removed.
*
* @return void
*/
function check_debug_log_override()
{
global $debug_log_override, $userref;
if (isset($debug_log_override) || !isset($userref)) {
return;
}
$debug_log_override = false;
$debug_user = get_sysvar('debug_override_user', '');
$debug_expires = get_sysvar('debug_override_expires', '');
if ($debug_user == "" || $debug_expires == "") {
return;
}
if ($debug_expires < time()) {
ps_query("DELETE FROM sysvars WHERE name='debug_override_user' OR name='debug_override_expires'", array());
return;
}
if ($debug_user == -1 || $debug_user == $userref) {
$debug_log_override = true;
}
}
/**
* Create a debug log override for a specified user or globally.
*
* This function sets a debug override that enables debug logging for a specified user
* or all users if `$debug_user` is -1. The override is set to expire after a specified
* duration in seconds. Any existing override settings are removed before the new values
* are inserted.
*
* @param int $debug_user The user ID for whom to enable debug logging (-1 for all users). Default is -1.
* @param int $debug_expires The time in seconds until the debug override expires, starting from the current time. Default is 60 seconds.
* @return void
*/
function create_debug_log_override($debug_user = -1, $debug_expires = 60)
{
ps_query("DELETE FROM sysvars WHERE name='debug_override_user' OR name='debug_override_expires'", array());
$debug_expires += time();
ps_query(
"INSERT INTO sysvars VALUES ('debug_override_user',?), ('debug_override_expires',?)",
array("s",$debug_user,"s",$debug_expires)
);
clear_query_cache("sysvars");
}
/**
* Debug called function and its arguments
*
* The best way to use this function is to call it on the first line of a function definition:
*
* function some_test($required, $num, $optional_bool = false)
* {
* debug_function_call(__FUNCTION__, func_get_args());
*
* echo "called some_test" . PHP_EOL;
*
* return;
* }
*
* @param string $name The function name
* @param array $args The "runtime" args
*
* @return boolean|void @see debug()
*/
function debug_function_call($name, array $args)
{
global $debug_log, $debug_log_override;
if (!$debug_log && !$debug_log_override) {
return false;
}
$args_str = "";
$fct = new ReflectionFunction($name);
foreach ($fct->getParameters() as $param) {
$value = null;
if (!$param->isOptional() && isset($args[$param->getPosition()])) {
$value = $args[$param->getPosition()];
} elseif ($param->isOptional() && isset($args[$param->getPosition()])) {
$value = $args[$param->getPosition()];
} elseif ($param->isOptional() && $param->isDefaultValueAvailable()) {
$value = $param->getDefaultValue();
}
$args_str .= sprintf("\$%s = %s, ", $param->getName(), debug_stringify($value));
}
$args_str = rtrim($args_str, ", ");
return debug("{$name}( {$args_str} );");
}
/**
* Stringify variables for use in the debug log. This is used more as fallback to json_encode() failing to maintain quick
* readability of the logs.
*
* @param mixed $value Any value that needs stringified
*
* @return string
*/
function debug_stringify($value)
{
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
return trim(preg_replace('/\s+/m', ' ', print_r($value, true)));
}

1094
include/definitions.php Normal file

File diff suppressed because it is too large Load Diff

503
include/do_search.php Normal file
View File

@@ -0,0 +1,503 @@
<?php
/**
* Takes a search string $search, as provided by the user, and returns a results set of matching resources. If there are
* no matches, instead returns an array of suggested searches
*
* @uses debug()
* @uses hook()
* @uses ps_value()
* @uses ps_query()
* @uses split_keywords()
* @uses add_verbatim_keywords()
* @uses search_filter()
* @uses search_special()
*
* @param string $search Search string
* @param string $restypes Optionally used to specify which resource types to search for
* @param string $order_by
* @param string $archive Allows searching in more than one archive state
* @param int|array $fetchrows - If passing an integer, retrieve the specified number of rows (a limit with no offset).
* The returned array of resources will be padded with '0' elements up to the total without the limit.
* - If passing an array, the first element must be the offset (int) and the second the limit (int)
* (the number of rows to return). See setup_search_chunks() for more detail.
* IMPORTANT: When passing an array, the returned array will be in structured form - as returned by
* sql_limit_with_total_count() i.e. the array will have 'total' (the count) and 'data' (the resources)
* named array elements, and the data will not be padded.
* @param string $sort
* @param boolean $access_override Used by smart collections, so that all all applicable resources can be judged
* regardless of the final access-based results
* @param integer $starsearch DEPRECATED_STARSEARCH passed in for backwards compatibility
* @param boolean $ignore_filters
* @param boolean $return_disk_usage
* @param string $recent_search_daylimit
* @param string|bool $go Paging direction (prev|next)
* @param boolean $stats_logging Log keyword usage
* @param boolean $return_refs_only
* @param boolean $editable_only
* @param boolean $returnsql Returns the query as a PreparedStatementQuery instance
* @param integer $access Search for resources with this access
*
* @return null|string|array|PreparedStatementQuery
*/
function do_search(
$search,
$restypes = '',
$order_by = 'relevance',
$archive = '0',
$fetchrows = -1,
$sort = 'desc',
$access_override = false,
$starsearch = DEPRECATED_STARSEARCH, # Parameter retained for backwards compatibility
$ignore_filters = false,
$return_disk_usage = false,
$recent_search_daylimit = '',
$go = false,
$stats_logging = true,
$return_refs_only = false,
$editable_only = false,
$returnsql = false,
$access = null,
$smartsearch = false
) {
debug_function_call("do_search", func_get_args());
global $sql, $order, $select, $sql_join, $sql_filter, $orig_order, $usergroup,
$userref,$k, $DATE_FIELD_TYPES,$stemming, $usersearchfilter, $userpermissions, $usereditfilter, $userdata,
$lang, $baseurl, $internal_share_access, $config_separators, $date_field, $noadd, $wildcard_always_applied,
$index_contributed_by, $max_results, $config_search_for_number,
$category_tree_search_use_and_logic, $date_field, $FIXED_LIST_FIELD_TYPES, $userderestrictfilter;
if (!is_a($select, "PreparedStatementQuery")) {
$select = new PreparedStatementQuery($select ?? '', array());
}
if ($editable_only && !$returnsql && trim((string) $k) != "" && !$internal_share_access) {
return array();
}
$alternativeresults = hook("alternativeresults", "", array($go));
if ($alternativeresults) {
return $alternativeresults;
}
if (!is_not_wildcard_only($search)) {
$search = '';
debug('do_search(): User searched only for "*". Converting this into an empty search instead.');
}
$modifyfetchrows = hook("modifyfetchrows", "", array($fetchrows));
if ($modifyfetchrows) {
$fetchrows = $modifyfetchrows;
}
if (!validate_sort_value($sort)) {
$sort = 'asc';
}
// Check the requested order_by is valid for this search. Function also allows plugin hooks to change this
$orig_order = $order_by;
$order_by = set_search_order_by($search, $order_by, $sort);
$archive = explode(",", $archive); // Allows for searching in more than one archive state
// IMPORTANT!
// add to this array in the format [AND group]=array(<list of nodes> to OR)
$node_bucket = array();
// add to this normal array to exclude nodes from entire search
$node_bucket_not = array();
// Take the current search URL and extract any nodes (putting into buckets) removing terms from $search
resolve_given_nodes($search, $node_bucket, $node_bucket_not);
$searchidmatch = false;
// Used to check if ok to skip keyword due to a match with resource type/resource ID
if (is_int_loose($search)) {
// Resource ID is no longer indexed, if search is just for a single integer then include this
$searchidmatch = ps_value("SELECT COUNT(*) AS value FROM resource WHERE ref = ?", ["i",$search], 0) != 0;
}
// Resource type is no longer indexed
$restypenames = get_resource_types();
# Extract search parameters and split to keywords.
$search_params = $search;
if (substr($search, 0, 1) == "!" && substr($search, 0, 6) != "!empty") {
# Special search, discard the special search identifier when splitting keywords and extract the search parameters
$s = strpos($search, " ");
if ($s === false) {
$search_params = ""; # No search specified
} else {
$search_params = substr($search, $s + 1); # Extract search params
}
}
if ($search_params != "") {
if (preg_match('/^[^\\s]+$/',$search) && ($wildcard_always_applied || strpos($search,"*") !== false)) {
$keywords = [$search_params];
} else {
$keywords = split_keywords($search_params, false, false, false, false, true);
}
} else {
$keywords = array();
}
$search = trim($search);
$keywords = array_values(array_filter(array_unique($keywords), 'is_not_wildcard_only'));
$modified_keywords = hook('dosearchmodifykeywords', '', array($keywords, $search));
if ($modified_keywords) {
$keywords = $modified_keywords;
}
# -- Build up filter SQL that will be used for all queries
$sql_filter = new PreparedStatementQuery();
$sql_filter = search_filter($search, $archive, $restypes, $recent_search_daylimit, $access_override, $return_disk_usage, $editable_only, $access, $smartsearch);
debug("do_search(): \$sql_filter = '" . $sql_filter->sql . "', parameters = ['" . implode("','", $sql_filter->parameters) . "']");
# Initialise variables.
$sql = "";
$sql_keyword_union = array(); // An array of all the unions - at least one for each keyword
//$sql_keyword_union_params = array();
$sql_keyword_union_aggregation = array(); // This is added to the SELECT statement. Normally 'BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`', where '[union_index]' will be replaced
$sql_keyword_union_criteria = array(); // Criteria for the union to be true - normally '`h`.`keyword_[union_index]_found`', where '[union_index]' will be replaced
// For each union sql_keyword_union_or must be set
// This will normally will be false to ensure that all keywords are found
// Needs to be set to false when keywords are expanded and an extra $sql_keyword_union element is added (e.g. for wildcards) so that a match on any is ok
$sql_keyword_union_or = array();
$sql_join = new PreparedStatementQuery();
$sql_join->sql = "";
$sql_join->parameters = [];
# If returning disk used by the resources in the search results ($return_disk_usage=true) then wrap the returned SQL in an outer query that sums disk usage.
$sql_prefix = "";
$sql_suffix = "";
if ($return_disk_usage) {
$sql_prefix = "SELECT sum(disk_usage) total_disk_usage, count(*) total_resources, resourcelist.ref, resourcelist.score, resourcelist.user_rating, resourcelist.total_hit_count FROM (";
$sql_suffix = ") resourcelist";
}
# ------ Advanced 'custom' permissions, need to join to access table.
if ((!checkperm("v")) && !$access_override) {
# one extra join (rca2) is required for user specific permissions (enabling more intelligent watermarks in search view)
# the original join is used to gather group access into the search query as well.
$sql_join->sql = " LEFT OUTER JOIN resource_custom_access rca2 ON r.ref=rca2.resource AND rca2.user = ? AND (rca2.user_expires IS null or rca2.user_expires>now()) AND rca2.access<>2 ";
array_push($sql_join->parameters, "i", $userref);
$sql_join->sql .= " LEFT OUTER JOIN resource_custom_access rca ON r.ref=rca.resource AND rca.usergroup = ? AND rca.access<>2 ";
array_push($sql_join->parameters, "i", $usergroup);
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
# If rca.resource is null, then no matching custom access record was found
# If r.access is also 3 (custom) then the user is not allowed access to this resource.
# Note that it's normal for null to be returned if this is a resource with non custom permissions (r.access<>3).
$sql_filter->sql .= " NOT (rca.resource IS null AND r.access=3)";
}
# Join thumbs_display_fields to resource table
$select->sql = "r.ref, r.resource_type, r.has_image, r.is_transcoding, r.creation_date, r.rating, r.user_rating, r.user_rating_count, r.user_rating_total, r.file_extension, r.preview_extension, r.image_red, r.image_green, r.image_blue, r.thumb_width, r.thumb_height, r.archive, r.access, r.colour_key, r.created_by, r.file_modified, r.file_checksum, r.request_count, r.new_hit_count, r.expiry_notification_sent, r.preview_tweaks, r.file_path, r.modified, r.file_size ";
$select->parameters = array();
$sql_hitcount_select = "r.hit_count";
$modified_select = hook('modifyselect');
$select->sql .= $modified_select ? $modified_select : ''; // modify select hook 1
$modified_select2 = hook('modifyselect2');
$select->sql .= $modified_select2 ? $modified_select2 : ''; // modify select hook 2
$select->sql .= $return_disk_usage ? ',r.disk_usage' : ''; // disk usage
// Get custom group and user access rights if available to generate resultant_access,
// otherwise select null values so columns can still be used regardless
// This can then be passed through to access checking functions in order to eliminate many single queries.
if (!checkperm("v") && !$access_override) {
// Get custom access
$select->sql .= ",rca.access group_access,rca2.access user_access ";
if (!checkperm("g") && !$internal_share_access) {
// Restrict all resources by default
if (is_int_loose($userderestrictfilter) && $userderestrictfilter > 0) {
// Add exemption for custom access
$derestrict_filter_sql = get_filter_sql($userderestrictfilter);
if (is_a($derestrict_filter_sql, "PreparedStatementQuery")) {
$access_sql = "CASE WHEN " . $derestrict_filter_sql->sql . " THEN 0 ELSE 1 END";
$select->sql .= ", LEAST(IFNULL(rca.access, $access_sql), IFNULL(rca2.access, $access_sql)) resultant_access ";
// Add node parameters to the start
$select->parameters = array_merge(
$select->parameters,
$derestrict_filter_sql->parameters,
$derestrict_filter_sql->parameters,
);
}
} else {
// Set access to restricted unless custom access is set
$select->sql .= ", LEAST(IFNULL(rca.access, 1), IFNULL(rca2.access, 1)) resultant_access ";
}
} else {
// Consider X permission else apply standard resultant_access for this user based on resource access and custom access
$sql_restricted_types = array();
foreach ($restypenames as $res_check_type) {
if (checkperm('X' . $res_check_type['ref'])) {
$sql_restricted_types[] = $res_check_type['ref'];
}
}
// Custom access can override X permission. Note: rows not returned for custom access "Confidential" so X can't increase access here.
if (count($sql_restricted_types) > 0) {
$select->sql .= ", CASE ";
foreach ($sql_restricted_types as $sql_restricted_type) {
$select->sql .= "WHEN resource_type = ? THEN LEAST(IFNULL(rca.access, 1), IFNULL(rca2.access, 1)) ";
$select->parameters[] = 'i';
$select->parameters[] = $sql_restricted_type;
}
$select->sql .= "ELSE LEAST(IFNULL(rca.access, r.access), IFNULL(rca2.access, r.access)) END resultant_access ";
} else {
$select->sql .= ", LEAST(IFNULL(rca.access, r.access), IFNULL(rca2.access, r.access)) resultant_access ";
}
}
} else {
$select->sql .= ", null group_access, null user_access ";
$select->sql .= ", r.access resultant_access ";
}
# add 'joins' to select (only add fields if not returning the refs only)
$joins = $return_refs_only === false || $GLOBALS["include_fieldx"] === true ? get_resource_table_joins() : array();
foreach ($joins as $datajoin) {
if (metadata_field_view_access($datajoin) || $datajoin == $GLOBALS["view_title_field"]) {
$select->sql .= ", r.field{$datajoin} ";
}
}
# Prepare SQL to add join table for all provided keywords
$suggested = $keywords; # a suggested search
$fullmatch = true;
$c = 0;
$t = new PreparedStatementQuery();
$t2 = new PreparedStatementQuery();
$score = "";
# Do not process if a numeric search is provided (resource ID)
global $keysearch;
$keysearch = !($config_search_for_number && is_numeric($search));
# Fetch a list of fields that are not available to the user - these must be omitted from the search.
$hidden_indexed_fields = get_hidden_indexed_fields();
// *******************************************************************************
// order by RESOURCE TYPE
// *******************************************************************************
$sql_join->sql .= " JOIN resource_type AS rty ON r.resource_type = rty.ref ";
$select->sql .= ", rty.order_by ";
hook("search_pipeline_setup", "", array($search, $select, $sql_join , $sql_filter, $sql));
// Search pipeline - each step handles an aspect of search and adds to the assembled SQL.
$return = include "do_search_keywords.php";
if ($return !== 1) {
return $return;
} // Forward any return from this include.
$return = include "do_search_nodes.php";
if ($return !== 1) {
return $return;
} // Handle returns from this include.
$return = include "do_search_suggest.php";
if ($return !== 1) {
return $return;
} // Handle returns from this include.
$return = include "do_search_filtering.php";
if ($return !== 1) {
return $return;
} // Handle returns from this include.
$return = include "do_search_union_assembly.php";
if ($return !== 1) {
return $return;
} // Handle returns from this include.
# Handle numeric searches when $config_search_for_number=false, i.e. perform a normal search but include matches for resource ID first
global $config_search_for_number;
if (!$config_search_for_number && is_int_loose($search)) {
# Always show exact resource matches first.
$order_by = "(r.ref='" . $search . "') desc," . $order_by;
}
# Can only search for resources that belong to featured collections. Doesn't apply to user's special upload collection to allow for upload then edit mode.
$upload_collection = '!collection' . (0 - $userref);
if (checkperm("J") && $search != $upload_collection) {
$collection_join = " JOIN collection_resource AS jcr ON jcr.resource = r.ref JOIN collection AS jc ON jcr.collection = jc.ref";
$collection_join .= featured_collections_permissions_filter_sql("AND", "jc.ref", true);
$sql_join->sql = $collection_join . $sql_join->sql;
}
# --------------------------------------------------------------------------------
# Special Searches (start with an exclamation mark)
# --------------------------------------------------------------------------------
$sql_select = clone $select;
$special_results = search_special($search, $sql_join, $fetchrows, $sql_prefix, $sql_suffix, $order_by, $orig_order, $sql_select, $sql_filter, $archive, $return_disk_usage, $return_refs_only, $returnsql);
if ($special_results !== false) {
log_keyword_usage($keywords_used, $special_results);
return $special_results;
}
# -------------------------------------------------------------------------------------
# Standard Searches
# -------------------------------------------------------------------------------------
# We've reached this far without returning.
# This must be a standard (non-special) search.
# Construct and perform the standard search query.
$sql = new PreparedStatementQuery();
if ($sql_filter->sql != "") {
if ($sql->sql != "") {
$sql->sql .= " AND ";
}
$sql->sql .= $sql_filter->sql;
$sql->parameters = array_merge($sql->parameters, $sql_filter->parameters);
}
# Append custom permissions
$t->sql .= $sql_join->sql;
$t->parameters = array_merge($t->parameters, $sql_join->parameters);
if ($score == "") {
$score = $sql_hitcount_select;
} # In case score hasn't been set (i.e. empty search)
if (($t2->sql != "") && ($sql->sql != "")) {
$sql->sql = " AND " . $sql->sql;
}
# Compile final SQL
$results_sql = new PreparedStatementQuery();
$results_sql->sql = $sql_prefix . "SELECT DISTINCT $score score, $select->sql FROM resource r" . $t->sql . " WHERE " . $t2->sql . $sql->sql . " GROUP BY r.ref, user_access, group_access ORDER BY " . $order_by . $sql_suffix;
$results_sql->parameters = array_merge($select->parameters, $t->parameters, $t2->parameters, $sql->parameters);
# Debug
debug('$results_sql=' . $results_sql->sql . ", parameters: " . implode(",", $results_sql->parameters));
setup_search_chunks($fetchrows, $chunk_offset, $search_chunk_size);
if ($return_refs_only) {
# Execute query but only ask for ref columns back from ps_query();
# We force verbatim query mode on (and restore it afterwards) as there is no point trying to strip slashes etc. just for a ref column
if ($returnsql) {
return $results_sql;
}
$count_sql = clone $results_sql;
$count_sql->sql = str_replace("ORDER BY " . $order_by, "", $count_sql->sql);
$result = sql_limit_with_total_count($results_sql, $search_chunk_size, $chunk_offset, true, $count_sql);
if (is_array($fetchrows)) {
// Return without converting into the legacy padded array
log_keyword_usage($keywords_used, $result);
return $result;
}
$resultcount = $result["total"] ?? 0;
if ($resultcount > 0 && count($result["data"]) > 0) {
$result = array_map(function ($val) {
return ["ref" => $val["ref"]];
}, $result["data"]);
} elseif (!is_array($fetchrows)) {
$result = [];
}
log_keyword_usage($keywords_used, $result);
return $result;
} else {
# Execute query as normal
if ($returnsql) {
return $results_sql;
}
$count_sql = clone $results_sql;
$count_sql->sql = str_replace("ORDER BY " . $order_by, "", $count_sql->sql);
$result = sql_limit_with_total_count($results_sql, $search_chunk_size, $chunk_offset, true, $count_sql);
}
if (is_array($fetchrows)) {
// Return without converting into the legacy padded array
log_keyword_usage($keywords_used, $result);
return $result;
}
$resultcount = $result["total"] ?? 0;
if ($resultcount > 0 & count($result["data"]) > 0) {
$result = $result['data'];
if ($search_chunk_size !== -1) {
// Only perform legacy padding of results if not all rows have been requested or total may be incorrect
$diff = $resultcount - count($result);
while ($diff > 0) {
$result = array_merge($result, array_fill(0, ($diff < 1000000 ? $diff : 1000000), 0));
$diff -= 1000000;
}
}
hook("beforereturnresults", "", array($result, $archive));
log_keyword_usage($keywords_used, $result);
return $result;
} else {
$result = [];
}
hook('zero_search_results');
// No suggestions for field-specific searching or if just one keyword
if (strpos($search, ":") !== false || count($keywords) === 1) {
return "";
}
# All keywords resolved OK, but there were no matches
# Remove keywords, least used first, until we get results.
$lsql = new PreparedStatementQuery();
$omitmatch = false;
for ($n = 0; $n < count($keywords); $n++) {
if (substr($keywords[$n], 0, 1) == "-") {
$omitmatch = true;
$omit = $keywords[$n];
}
if ($lsql->sql != "") {
$lsql->sql .= " OR ";
}
$lsql->sql .= "keyword = ?";
$lsql->parameters = array_merge($lsql->parameters, ["i", $keywords[$n]]);
}
if ($omitmatch) {
return trim_spaces(str_replace(" " . $omit . " ", " ", " " . join(" ", $keywords) . " "));
}
if ($lsql->sql != "") {
$least = ps_value("SELECT keyword value FROM keyword WHERE " . $lsql->sql . " ORDER BY hit_count ASC LIMIT 1", $lsql->parameters, "");
return trim_spaces(str_replace(" " . $least . " ", " ", " " . join(" ", $keywords) . " "));
} else {
return array();
}
}
// Take the current search URL and extract any nodes (putting into buckets) removing terms from $search
//
// Currently supports:
// @@!<node id> (NOT)
// @@<node id>@@<node id> (OR)
function resolve_given_nodes(&$search, &$node_bucket, &$node_bucket_not)
{
// extract all of the words, a word being a bunch of tokens with optional NOTs
if (preg_match_all('/(' . NODE_TOKEN_PREFIX . NODE_TOKEN_NOT . '*\d+)+/', $search, $words) === false || count($words[0]) == 0) {
return;
}
// spin through each of the words and process tokens
foreach ($words[0] as $word) {
$search = str_replace($word, '', $search); // remove the entire word from the search string
$search = trim(trim($search), ',');
preg_match_all('/' . NODE_TOKEN_PREFIX . '(' . NODE_TOKEN_NOT . '*)(\d+)/', $word, $tokens);
if (count($tokens[1]) == 1 && $tokens[1][0] == NODE_TOKEN_NOT) { // you are currently only allowed NOT condition for a single token within a single word
$node_bucket_not[] = $tokens[2][0]; // add the node number to the node_bucket_not
continue;
}
$node_bucket[] = $tokens[2];
}
}

View File

@@ -0,0 +1,102 @@
<?php
// *******************************************************************************
//
// Legacy filtering - to be moved to migration script (q13255)
//
// *******************************************************************************
if (
strlen(trim((string) $usersearchfilter)) > 0
&& !is_numeric($usersearchfilter)
&& (
(isset($userdata[0]["search_filter_override"]) && trim($userdata[0]["search_filter_override"]) != ""
&& isset($userdata[0]["search_filter_o_id"]) && $userdata[0]["search_filter_o_id"] != -1)
||
(isset($userdata[0]["search_filter"]) && trim($userdata[0]["search_filter"]) != ""
&& isset($userdata[0]["search_filter_id"]) && $userdata[0]["search_filter_id"] != -1)
)
) {
// Migrate old style filter unless previously failed attempt
$migrateresult = migrate_filter($usersearchfilter);
$notification_users = get_notification_users();
if (is_numeric($migrateresult)) {
message_add(array_column($notification_users, "ref"), $lang["filter_migrate_success"] . ": '" . $usersearchfilter . "'", generateURL($baseurl . "/pages/admin/admin_group_management_edit.php", array("ref" => $usergroup)));
// Successfully migrated - now use the new filter
if (isset($userdata[0]["search_filter_override"]) && $userdata[0]["search_filter_override"] != '') {
// This was a user override filter - update the user record
ps_query("UPDATE user SET search_filter_o_id = ? WHERE ref = ?", ["i",$migrateresult,"i",$userref]);
} else {
save_usergroup($usergroup, array('search_filter_id' => $migrateresult));
}
$usersearchfilter = $migrateresult;
debug("FILTER MIGRATION: Migrated filter - new filter id#" . $usersearchfilter);
} elseif (is_array($migrateresult)) {
debug("FILTER MIGRATION: Error migrating filter: '" . $usersearchfilter . "' - " . implode('\n', $migrateresult));
// Error - set flag so as not to reattempt migration and notify admins of failure
if (isset($userdata[0]["search_filter_override"]) && $userdata[0]["search_filter_override"] != '') {
ps_query("UPDATE user SET search_filter_o_id='-1' WHERE ref = ?", ["i",$userref]);
} else {
save_usergroup($usergroup, array('search_filter_id' => -1));
}
message_add(array_column($notification_users, "ref"), $lang["filter_migration"] . " - " . $lang["filter_migrate_error"] . ": <br />" . implode('\n', $migrateresult), generateURL($baseurl . "/pages/admin/admin_group_management_edit.php", array("ref" => $usergroup)));
}
}
// Old text filters are no longer supported
if (is_int_loose($usersearchfilter) && $usersearchfilter > 0) {
$search_filter_sql = get_filter_sql($usersearchfilter);
if (!$search_filter_sql) {
exit($lang["error_search_filter_invalid"]);
}
if (is_a($search_filter_sql, "PreparedStatementQuery")) {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= $search_filter_sql->sql;
$sql_filter->parameters = array_merge($sql_filter->parameters, $search_filter_sql->parameters);
}
}
if ($editable_only) {
if (
strlen(trim($usereditfilter ?? "")) > 0
&& !is_numeric($usereditfilter)
&& trim($userdata[0]["edit_filter"]) != ""
&& $userdata[0]["edit_filter_id"] != -1
) {
// Migrate unless marked not to due to failure
$usereditfilter = edit_filter_to_restype_permission($usereditfilter, $usergroup, $userpermissions);
if (trim($usereditfilter) !== "") {
$migrateresult = migrate_filter($usereditfilter);
} else {
$migrateresult = 0; // filter was only for resource type, hasn't failed but no need to migrate again
save_usergroup($usergroup, array('edit_filter' => ''));
}
if (is_numeric($migrateresult)) {
debug("Migrated . " . $migrateresult);
// Successfully migrated - now use the new filter
save_usergroup($usergroup, array('edit_filter_id' => $migrateresult));
debug("FILTER MIGRATION: Migrated edit filter - '" . $usereditfilter . "' filter id#" . $migrateresult);
$usereditfilter = $migrateresult;
} elseif (is_array($migrateresult)) {
debug("FILTER MIGRATION: Error migrating filter: '" . $usersearchfilter . "' - " . implode('\n', $migrateresult));
// Error - set flag so as not to reattempt migration and notify admins of failure
save_usergroup($usergroup, array('edit_filter_id' => -1));
$notification_users = get_notification_users();
message_add(array_column($notification_users, "ref"), $lang["filter_migration"] . " - " . $lang["filter_migrate_error"] . ": <br />" . implode('\n', $migrateresult), generateURL($baseurl . "/pages/admin/admin_group_management_edit.php", array("ref" => $usergroup)));
}
}
if (is_numeric($usereditfilter) && $usereditfilter > 0) {
$edit_filter_sql = get_filter_sql($usereditfilter);
if (is_a($edit_filter_sql, "PreparedStatementQuery")) {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= $edit_filter_sql->sql;
$sql_filter->parameters = array_merge($sql_filter->parameters, $edit_filter_sql->parameters);
}
}
}

View File

@@ -0,0 +1,777 @@
<?php
// *******************************************************************************
//
// Included within do_search() for keywords processing and assembly into SQL
//
// *******************************************************************************
// Candidate for replacement function including the variables needed.
// function search_process_keyword($keyword, &$n, &$c, &$keywords, &$hidden_indexed_fields, &$search, &$searchidmatch, &$restypenames, &$ignore_filters, &$node_bucket, &$stats_logging, &$go, &$sql_keyword_union, &$sql_keyword_union_aggregation, &$sql_keyword_union_criteria, &$sql_keyword_union_or, &$omit, &$fullmatch, &$suggested, &$sql_join, &$sql_filter)
$keywords_used = [];
if ($keysearch) {
$date_parts = [];
for ($n = 0; $n < count($keywords); $n++) {
$canskip = false;
$search_field_restrict = "";
$keyword = $keywords[$n];
debug("do_search(): \$keyword = {$keyword}");
$quoted_string = (substr($keyword, 0, 1) == "\"" || substr($keyword, 0, 2) == "-\"" ) && substr($keyword, -1, 1) == "\"";
$quoted_field_match = false;
$field_short_name_specified = false;
// Extra sql to search non-field data that used to be stored in resource_keyword e.g. resource type/resource contributor
$non_field_keyword_sql = new PreparedStatementQuery();
if ($quoted_string && substr($keyword, 1, strlen(FULLTEXT_SEARCH_PREFIX)) == FULLTEXT_SEARCH_PREFIX) {
// Full text search
$fulltext_string = str_replace(FULLTEXT_SEARCH_QUOTES_PLACEHOLDER, "\"", substr($keyword, strlen(FULLTEXT_SEARCH_PREFIX) + 2, -1));
if (strpos($fulltext_string, "@") !== false) {
// There's an @ character in the fulltext search which InnoDB does not permit, so quote-wrap the search string
$fulltext_string = "'\"" . $fulltext_string . "\"'";
}
$freetextunion = new PreparedStatementQuery();
$freetextunion->sql = " SELECT resource, [bit_or_condition] 1 AS score FROM resource_node rn LEFT JOIN node n ON n.ref=rn.node WHERE MATCH(name) AGAINST (? IN BOOLEAN MODE)";
$freetextunion->parameters = ["s",$fulltext_string];
if (count($hidden_indexed_fields) > 0) {
$freetextunion->sql .= " AND n.resource_type_field NOT IN (" . ps_param_insert(count($hidden_indexed_fields)) . ")";
$freetextunion->parameters = array_merge($freetextunion->parameters, ps_param_fill($hidden_indexed_fields, "i"));
}
$sql_keyword_union[] = $freetextunion;
$sql_keyword_union_aggregation[] = "BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`";
$sql_keyword_union_or[] = false;
$sql_keyword_union_criteria[] = "`h`.`keyword_[union_index]_found`";
continue;
}
if ($keyword == $search && is_int_loose($keyword) && $searchidmatch) {
// Resource ID is no longer indexed, if search is just for a single integer then include this
$non_field_keyword_sql->sql .= " UNION (SELECT " . (int)$keyword . " AS resource, [bit_or_condition] 1 AS score)";
$canskip = true;
} elseif (in_array(mb_strtolower($keyword), array_map("mb_strtolower", array_column($restypenames, "name")))) {
// Resource type is no longer actually indexed but this will still honour the config by including in search
$non_field_keyword_sql->sql .= " UNION (SELECT r.ref AS resource, [bit_or_condition] 1 AS score FROM resource r LEFT JOIN resource_type rt ON r.resource_type=rt.ref WHERE r.ref > 0 AND rt.name LIKE ?)";
array_push($non_field_keyword_sql->parameters, "s", $keyword);
$canskip = true;
}
if ($index_contributed_by) {
// Resource type is no longer actually indexed but this will still honour the config by including in search
$matchusers = get_users(0, $keyword, "u.username", true);
if (count($matchusers) > 0) {
$user_sql = get_users(0, $keyword, "u.username", true, -1, "", true);
$non_field_keyword_sql->sql .= " UNION (
SELECT r.ref AS resource, [bit_or_condition] 1 AS score
FROM resource r
WHERE
r.created_by IN (SELECT u.ref FROM (" . $user_sql->sql . " ) as u)
AND r.ref >0
)";
$non_field_keyword_sql->parameters = array_merge(
$non_field_keyword_sql->parameters,
$user_sql->parameters
);
$canskip = true;
}
}
if (
(
!$quoted_string
|| ($quoted_string // If quoted string with a field specified we first need to try and resolve it to a node instead of working out keyword positions etc.
&& strpos($keyword, ":") !== false)
)
&& (substr($keyword, 0, 1) != "!"
|| substr($keyword, 0, 6) == "!empty")
) {
$keywordprocessed = false;
if (strpos($keyword, ":") !== false) {
$field_short_name_specified = true;
$kw = explode(":", ($quoted_string ? substr($keyword, 1, -1) : $keyword), 2);
# Fetch field info
global $fieldinfo_cache;
$fieldname = $kw[0];
$keystring = $kw[1];
debug("do_search(): \$fieldname = {$fieldname}");
debug("do_search(): \$keystring = {$keystring}");
if (isset($fieldinfo_cache[$fieldname])) {
$fieldinfo = $fieldinfo_cache[$fieldname];
} else {
$fieldinfo = ps_query("SELECT ref, `type` FROM resource_type_field WHERE name = ?", ["s",$fieldname], "schema");
// Checking for date from Simple Search will result with a fieldname like 'year' which obviously does not exist
if (0 === count($fieldinfo) && ('basicyear' == $kw[0] || 'basicmonth' == $kw[0] || 'basicday' == $kw[0])) {
$fieldinfo = ps_query("SELECT ref, `type` FROM resource_type_field WHERE ref = ?", ["i",$date_field], "schema");
}
if (0 === count($fieldinfo)) {
// Search may just happen to include a colon - treat the colon as a space and add to $keywords array to process separately
$addedkeywords = explode(":", $keyword);
$keywords = array_merge($keywords, $addedkeywords);
continue;
} else {
$fieldinfo = $fieldinfo[0];
$fieldinfo_cache[$fieldname] = $fieldinfo;
}
}
if (!metadata_field_view_access($fieldinfo['ref'])) {
// User can't search against a metadata field they don't have access to
return false;
}
}
//First try and process special keyword types
if ($field_short_name_specified && !$quoted_string && !$ignore_filters && isset($fieldinfo['type']) && in_array($fieldinfo['type'], $DATE_FIELD_TYPES)) {
// ********************************************************************************
// Date field keyword
// ********************************************************************************
global $datefieldinfo_cache;
if (isset($datefieldinfo_cache[$fieldname])) {
$datefieldinfo = $datefieldinfo_cache[$fieldname];
} else {
$datefieldinfo = ps_query("SELECT ref FROM resource_type_field WHERE name = ? AND type IN (" . FIELD_TYPE_DATE_AND_OPTIONAL_TIME . "," . FIELD_TYPE_EXPIRY_DATE . "," . FIELD_TYPE_DATE . "," . FIELD_TYPE_DATE_RANGE . ")", ["s",$fieldname], "schema");
$datefieldinfo_cache[$fieldname] = $datefieldinfo;
}
if (count($datefieldinfo) && substr($keystring, 0, 5) != "range") {
$c++;
$datefieldinfo = $datefieldinfo[0];
$datefield = $datefieldinfo["ref"];
$val = str_replace("n", "_", $keystring);
$val = str_replace("|", "-", $val);
if ($fieldinfo['type'] == FIELD_TYPE_DATE_RANGE) {
// Find where the searched value is between the range values
$sql_join->sql .= " JOIN resource_node drrn" . $c . "s ON drrn" . $c . "s.resource=r.ref JOIN node drn" . $c . "s ON drn" . $c . "s.ref=drrn" . $c . "s.node AND drn" . $c . "s.resource_type_field = ? AND drn" . $c . "s.name >= ? JOIN resource_node drrn" . $c . "e ON drrn" . $c . "e.resource=r.ref JOIN node drn" . $c . "e ON drn" . $c . "e.ref=drrn" . $c . "e.node AND drn" . $c . "e.resource_type_field = ? AND DATE(drn" . $c . "e.name) <= ?";
array_push($sql_join->parameters, "i", $datefield, "i", $datefield, "s", $val, "s", $val);
} else {
$sql_join->sql .= " JOIN resource_node rnd" . $c . " ON rnd" . $c . ".resource=r.ref JOIN node dn" . $c . " ON dn" . $c . ".ref=rnd" . $c . ".node AND dn" . $c . ".resource_type_field = ?";
array_push($sql_join->parameters, "i", $datefield);
$sql_filter->sql .= ($sql_filter->sql != "" ? " AND " : "") . "dn" . $c . ".name like ?";
array_push($sql_filter->parameters, "s", $val . "%");
}
// Find where the searched value is LIKE the range values
} elseif (in_array($kw[0], array("basicday","basicmonth","basicyear"))) {
$c++;
if (!isset($datefieldjoin)) {
// We only want to join once to the date_field
$sql_join->sql .= " JOIN resource_node rdnf" . $c . " ON rdnf" . $c . ".resource=r.ref JOIN node rdn" . $c . " ON rdnf" . $c . ".node=rdn" . $c . ".ref AND rdn" . $c . ".resource_type_field = ?";
$datefieldjoin = $c;
array_push($sql_join->parameters, "i", $date_field);
}
if ('basicday' == $kw[0]) {
$date_parts['day'] = $keystring;
} elseif ('basicmonth' == $kw[0]) {
$date_parts['month'] = $keystring;
} elseif ('basicyear' == $kw[0]) {
$date_parts['year'] = $keystring;
}
} elseif (count($datefieldinfo) && substr($keystring, 0, 5) == "range") {
# Additional date range filtering
$c++;
$rangestring = substr($keystring, 5);
if (strpos($rangestring, "start") !== false) {
$rangestartpos = strpos($rangestring, "start") + 5;
$rangestart = str_replace(" ", "-", substr($rangestring, $rangestartpos, strpos($rangestring, "end") ? strpos($rangestring, "end") - $rangestartpos : 10));
}
if (strpos($keystring, "end") !== false) {
$rangeend = str_replace(" ", "-", $rangestring);
$rangeend = substr($rangeend, strpos($rangeend, "end") + 3, 10) . " 23:59:59";
}
// Find where the start value or the end value is between the range values
if (isset($rangestart)) {
// Need to check for a date greater than the start date
$sql_join->sql .= " JOIN resource_node drrn" . $c . "s ON drrn" . $c . "s.resource=r.ref JOIN node drn" . $c . "s ON drn" . $c . "s.ref=drrn" . $c . "s.node AND drn" . $c . "s.resource_type_field = ? AND drn" . $c . "s.name>= ? ";
array_push($sql_join->parameters, "i", $fieldinfo['ref'], "s", $rangestart);
}
if (isset($rangeend)) {
// Need to check for a date earlier than the end date
$sql_join->sql .= " JOIN resource_node drrn" . $c . "e ON drrn" . $c . "e.resource=r.ref JOIN node drn" . $c . "e ON drn" . $c . "e.ref=drrn" . $c . "e.node AND drn" . $c . "e.resource_type_field = ? AND drn" . $c . "e.name <= ? ";
array_push($sql_join->parameters, "i", $fieldinfo['ref'], "s", $rangeend);
}
}
$keywordprocessed = true;
} elseif ($field_short_name_specified && substr($keystring, 0, 8) == "numrange" && !$quoted_string && !$ignore_filters && isset($fieldinfo['type']) && $fieldinfo['type'] == 0) {
// Text field numrange search ie mynumberfield:numrange1|1234 indicates that mynumberfield needs a numrange search for 1 to 1234.
$c++;
$rangefieldinfo = ps_query("SELECT ref FROM resource_type_field WHERE name = ? AND type IN (0)", ["s",$fieldname], "schema");
$rangefieldinfo = $rangefieldinfo[0];
$rangefield = $rangefieldinfo["ref"];
$rangestring = substr($keystring, 8);
$minmax = explode("|", $rangestring);
$min = str_replace("neg", "-", $minmax[0]);
if (isset($minmax[1])) {
$max = str_replace("neg", "-", $minmax[1]);
} else {
$max = '';
}
if ($max != '' || $min != '') {
// At least the min or max should be set
if ($max == '' || $min == '') {
// if only one number is entered, do a direct search
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "rnn" . $c . ".name = ? ";
array_push($sql_filter->parameters, "d", max($min, $max));
} else {
// else use min and max values as a range search
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "rnn" . $c . ".name >= ? ";
array_push($sql_filter->parameters, "d", $min);
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "rnn" . $c . ".name <= ? ";
array_push($sql_filter->parameters, "d", $max);
}
}
$sql_join->sql .= " JOIN resource_node rrn" . $c . " ON rrn" . $c . ".resource=r.ref LEFT JOIN node rnn" . $c . " ON rnn" . $c . ".ref=rrn" . $c . ".node AND rnn" . $c . ".resource_type_field = ?";
array_push($sql_join->parameters, "i", $rangefield);
$keywordprocessed = true;
} elseif (
$field_short_name_specified
&& !$ignore_filters
&& isset($fieldinfo['type'])
&& in_array($fieldinfo['type'], $FIXED_LIST_FIELD_TYPES)
) {
// Convert legacy fixed list field search to new format for nodes (@@NodeID)
// We've searched using a legacy format (ie. fieldShortName:keyword), try and convert it to @@NodeID
$field_nodes = get_nodes($fieldinfo['ref'], null, false, true);
// Check if multiple nodes have been specified for an OR search
$keywords_expanded = explode(';', $keystring);
$nodeorcount = count($node_bucket);
foreach ($keywords_expanded as $keyword_expanded) {
debug("keyword_expanded: " . $keyword_expanded);
$field_node_index = array_search(mb_strtolower(i18n_get_translated($keyword_expanded)), array_map('i18n_get_translated', array_map('mb_strtolower', array_column($field_nodes, 'name'))));
// Take the ref of the node and add it to the node_bucket as an OR
if (false !== $field_node_index) {
$node_bucket[$nodeorcount][] = $field_nodes[$field_node_index]['ref'];
$quoted_field_match = true; // String has been resolved to a node so even if it is quoted we don't need to process it as a quoted string now
$keywordprocessed = true;
}
}
} elseif (
$field_short_name_specified
&& !$quoted_string
&& preg_match(
'/^(?=[^ \\t\\n]*[!@#$%^&()_+\\-=\\[\\]{}\'\"\\\\,.<>\\/?])[^ \\t\\n]*$/',
$keyword
) === 1
) {
// Quote field specific keywords that contain special characters and no spaces
$keyword = "\"$keyword\"";
$quoted_string = true;
}
if ($field_short_name_specified) { // Need this also for string matching in a named text field
$keyword = $keystring;
$search_field_restrict = $fieldinfo['ref'];
}
if (!$quoted_string && !$keywordprocessed && !($field_short_name_specified && hook('customsearchkeywordfilter', null, array($kw)))) { // Need this also for string matching in a named text field
// Normal keyword
//
// Searches all fields that the user has access to
// If ignoring field specifications then remove them.
$keywords_expanded = explode(';', $keyword);
$keywords_expanded_or = count($keywords_expanded) > 1;
# Omit resources containing this keyword?
$omit = false;
if (substr($keyword, 0, 1) == "-") {
$omit = true;
$keyword = substr($keyword, 1);
}
# Search for resources with an empty field, ex: !empty18 or !emptycaption
$empty = false;
if (substr($keyword, 0, 6) == "!empty") {
$nodatafield = str_replace("!empty", "", $keyword);
if (!is_numeric($nodatafield)) {
$nodatafield = ps_value("SELECT ref value FROM resource_type_field WHERE name = ?", ["i",$nodatafield], "", "schema");
}
if ($nodatafield == "" || !is_numeric($nodatafield)) {
exit('invalid !empty search');
}
$empty = true;
}
if (in_array($keyword, $noadd)) { # skip common words that are excluded from indexing
debug("do_search(): skipped common word: {$keyword}");
} else {
// ********************************************************************************
// Handle wildcards
// ********************************************************************************
$wildcards = false;
if (strpos($keyword, "*") !== false || $wildcard_always_applied) {
if ($wildcard_always_applied && strpos($keyword, "*") === false) {
# Suffix asterisk if none supplied and using $wildcard_always_applied mode.
$keyword = $keyword . "*";
}
$wildcards = true;
}
$keyref = resolve_keyword(str_replace('*', '', $keyword), false, true, !$quoted_string); # Resolve keyword. Ignore any wildcards when resolving. We need wildcards to be present later but not here.
if ($keyref === false) {
if ($stemming) {
// Attempt to find match for original keyword
$keyref = resolve_keyword(str_replace('*', '', $keyword), false, true, false);
}
if ($keyref === false) {
if ($keywords_expanded_or) {
$alternative_keywords = array();
foreach ($keywords_expanded as $keyword_expanded) {
$alternative_keyword_keyref = resolve_keyword($keyword_expanded, false, true, true);
if ($alternative_keyword_keyref === false) {
continue;
}
$alternative_keywords[] = $alternative_keyword_keyref;
}
if (count($alternative_keywords) > 0) {
// Multiple alternative keywords
$alternative_keywords_sql = new PreparedStatementQuery();
$alternative_keywords_sql->sql = " OR nk[union_index].keyword IN (" . ps_param_insert(count($alternative_keywords)) . ")";
$alternative_keywords_sql->parameters = ps_param_fill($alternative_keywords, "i");
debug("do_search(): \$alternative_keywords_sql = {$alternative_keywords_sql->sql}, parameters = " . implode(",", $alternative_keywords_sql->parameters));
}
} elseif (strpos($keyword, "*") === false && preg_match('/\\s/', $search) !== 1) {
// Check keyword for defined separators and if found each part of the value is added as a keyword for checking. Not for wildcards with no spaces
$contains_separators = false;
foreach ($config_separators as $separator) {
if (strpos($keyword, $separator) !== false) {
$contains_separators = true;
}
}
if ($contains_separators === true) {
$keyword_split = split_keywords($keyword);
if ($field_short_name_specified) {
$keyword_split = array_map(prefix_value($fieldname . ":"), $keyword_split);
}
$keywords = array_merge($keywords, $keyword_split);
continue;
}
}
}
}
if ($keyref === false && !$omit && !$empty && !$wildcards && !$field_short_name_specified && !$canskip) {
// ********************************************************************************
// No wildcards
// ********************************************************************************
$fullmatch = false;
$soundex = resolve_soundex($keyword);
if ($soundex === false) {
# No keyword match, and no keywords sound like this word. Suggest dropping this word.
$suggested[$n] = "";
} else {
# No keyword match, but there's a word that sounds like this word. Suggest this word instead.
$suggested[$n] = "<i>" . $soundex . "</i>";
}
} else {
// ********************************************************************************
// Found wildcards
// ********************************************************************************
// Multiple alternative keywords
$alternative_keywords_sql = new PreparedStatementQuery();
$alternative_keywords = array();
if ($keywords_expanded_or) {
foreach ($keywords_expanded as $keyword_expanded) {
$alternative_keyword_keyref = resolve_keyword($keyword_expanded, false, true, true);
if ($alternative_keyword_keyref === false) {
continue;
}
$alternative_keywords[] = $alternative_keyword_keyref;
}
if (count($alternative_keywords) > 0) {
$alternative_keywords_sql->sql = " OR nk[union_index].keyword IN (" . ps_param_insert(count($alternative_keywords)) . ")";
$alternative_keywords_sql->parameters = ps_param_fill($alternative_keywords, "i");
debug("do_search(): \$alternative_keywords_sql = {$alternative_keywords_sql->sql}, parameters = " . implode(",", $alternative_keywords_sql->parameters));
}
}
if ($keyref === false) {
# make a new keyword
$keyref = resolve_keyword(str_replace('*', '', $keyword), true, true, false);
}
# Key match, add to query.
$c++;
$relatedsql = new PreparedStatementQuery();
# Add related keywords
$related = get_related_keywords($keyref);
if ($stemming) {
# Need to ensure we include related keywords for original string
$original_keyref = resolve_keyword(str_replace('*', '', $keyword), false, true, false);
if ($original_keyref && $original_keyref !== $keyref) {
$original_related = get_related_keywords($original_keyref);
if (count($original_related) > 0) {
$original_related_kws = ps_array("SELECT keyword AS `value` FROM keyword WHERE ref IN (" . ps_param_insert(count($original_related)) . ")", ps_param_fill($original_related, "i"));
$extra_related = array();
foreach ($original_related_kws as $orig_related_kw) {
$extrakeyword = GetStem(trim($orig_related_kw));
$extra_related[] = resolve_keyword($extrakeyword, true, false, false);
}
$related = array_merge($related, $extra_related);
}
}
}
if (count($related) > 0) {
$relatedsql->sql .= " OR (nk[union_index].keyword IN (" . ps_param_insert(count($related)) . ")";
$relatedsql->parameters = array_merge($relatedsql->parameters, ps_param_fill($related, "i"));
if ($field_short_name_specified && isset($fieldinfo['ref'])) {
$relatedsql->sql .= " AND nk[union_index].node IN (SELECT ref FROM node WHERE resource_type_field = ? )";
$relatedsql->parameters[] = "i";
$relatedsql->parameters[] = $fieldinfo['ref'];
}
$relatedsql->sql .= ")";
}
# Form join
$sql_exclude_fields = hook("excludefieldsfromkeywordsearch");
if ($omit) {
# Exclude matching resources from query (omit feature)
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
// ----- check that keyword does not exist via resource_node->node_keyword relationship -----
$sql_filter->sql .= "`r`.`ref` NOT IN (SELECT `resource` FROM `resource_node` JOIN `node_keyword` ON `resource_node`.`node`=`node_keyword`.`node`" .
" WHERE `resource_node`.`resource`=`r`.`ref` AND `node_keyword`.`keyword` = ?)";
array_push($sql_filter->parameters, "i", $keyref);
} else # Include in query
{
// --------------------------------------------------------------------------------
// Start of normal union for resource keywords
// --------------------------------------------------------------------------------
// // these restrictions apply to both !empty searches as well as normal keyword searches (i.e. both branches of next if statement)
$union_restriction_clause = new PreparedStatementQuery();
$skipfields = array();
if (!empty($sql_exclude_fields)) {
if ($wildcards) {
$union_restriction_clause->sql = " AND resource_type_field NOT IN (" . ps_param_insert(count($sql_exclude_fields)) . ")";
} else {
$union_restriction_clause->sql .= " AND nk[union_index].node NOT IN (SELECT ref FROM node WHERE resource_type_field IN (" . ps_param_insert(count($sql_exclude_fields)) . "))";
}
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ps_param_fill($sql_exclude_fields, "i"));
$skipfields = explode(",", str_replace(array("'","\""), "", $sql_exclude_fields));
}
if (count($hidden_indexed_fields) > 0) {
if ($wildcards) {
$union_restriction_clause->sql = " AND resource_type_field NOT IN (" . ps_param_insert(count($hidden_indexed_fields)) . ")";
} else {
$union_restriction_clause->sql .= " AND nk[union_index].node NOT IN (SELECT ref FROM node WHERE node.resource_type_field IN (" . ps_param_insert(count($hidden_indexed_fields)) . "))";
}
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ps_param_fill($hidden_indexed_fields, "i"));
$skipfields = array_merge($skipfields, $hidden_indexed_fields);
}
if (isset($search_field_restrict) && $search_field_restrict != "") {
// Search is looking for a keyword in a specified field
if ($wildcards) {
$union_restriction_clause->sql .= " AND resource_type_field = ?";
} else {
$union_restriction_clause->sql .= " AND nk[union_index].node IN (SELECT ref FROM node WHERE node.resource_type_field = ?)";
}
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ["i",$search_field_restrict]);
}
if ($empty) { // we are dealing with a special search checking if a field is empty
// First check user can see this field
if (in_array($nodatafield, $skipfields)) {
// Not permitted to check this field, return false
return false;
}
$selectedrestypes = [];
$restypes = trim((string)($restypes));
if ($restypes != "") {
$selectedrestypes = explode(",", $restypes);
}
$restypesql = new PreparedStatementQuery();
$nodatafieldinfo = get_resource_type_field($nodatafield);
$nodatarestypes = trim((string)$nodatafieldinfo["resource_types"]);
if ($nodatarestypes != "") {
$nodatarestypes = explode(",", $nodatarestypes);
} else {
$nodatarestypes = [];
}
if ($nodatafieldinfo["global"] === 1) {
// Global field empty search
// Candidate resources are those which exist in selected resource types
if (count($selectedrestypes) > 0) {
$restypesql->sql = " AND r[union_index].resource_type IN (" . ps_param_insert(count($selectedrestypes)) . ") ";
$restypesql->parameters = ps_param_fill($selectedrestypes, "i");
}
} else {
// Non-global field empty search
// Candidate resources are those whose resource type is linked to the field and which exists in selected resource types
if (count($selectedrestypes) > 0) {
$candidaterestypes = array_intersect($nodatarestypes, $selectedrestypes);
} else {
$candidaterestypes = $nodatarestypes;
}
$restypesql->sql = " AND r[union_index].resource_type IN (" . ps_param_insert(count($candidaterestypes)) . ") ";
$restypesql->parameters = ps_param_fill($candidaterestypes, "i");
}
// Check that nodes are empty
$union = new PreparedStatementQuery();
$union->sql = "SELECT ref AS resource, [bit_or_condition] 1 AS score FROM resource r[union_index] WHERE r[union_index].ref NOT IN
(
SELECT rn.resource FROM
node n
RIGHT JOIN resource_node rn ON rn.node=n.ref
WHERE n.resource_type_field = ? $restypesql->sql
GROUP BY rn.resource
)";
$union->parameters = array_merge(["i",$nodatafield], $restypesql->parameters);
$sql_keyword_union[] = $union;
$sql_keyword_union_criteria[] = "`h`.`keyword_[union_index]_found`";
$sql_keyword_union_aggregation[] = "BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`";
$sql_keyword_union_or[] = false;
} elseif ($wildcards) {
$union = new PreparedStatementQuery();
if (
substr($keyword, 0, 1) != "*"
&& strlen(trim($keyword, '*')) >= 3
&& preg_match('/[-+@<>().]/', $keyword) !== 1
) {
// Use fulltext search as a preference, but not if
// there is a leading wildcard
// or the search is too short
// or the search contains any of +,-,@,<,>,(,),.
$union->sql = "
SELECT resource, [bit_or_condition] hit_count AS score
FROM resource_node rn[union_index]
WHERE rn[union_index].node IN
(SELECT ref FROM `node` WHERE MATCH(name) AGAINST (? IN BOOLEAN MODE) "
. $union_restriction_clause->sql . ")
GROUP BY resource " .
($non_field_keyword_sql->sql != "" ? $non_field_keyword_sql->sql : "");
$keyword = "+" . $keyword;
} elseif (
preg_match('/\W/', str_replace("*", "", $keyword)) !== 1
) {
// Use a LIKE search, unless there is any whitespace
$keyword = str_replace("*", "%", $keyword);
$union->sql = "
SELECT resource, [bit_or_condition] hit_count AS score
FROM resource_node rn[union_index]
WHERE rn[union_index].node IN
(SELECT n.ref
FROM keyword k
JOIN node_keyword nk ON nk.keyword=k.ref
JOIN `node` n ON n.ref=nk.node
WHERE k.keyword LIKE ? "
. $union_restriction_clause->sql . ")
GROUP BY resource ";
} else {
// Use RLIKE to search between word boundaries in the node names
$keyword = preg_quote($keyword);
$keyword = str_replace('\*', '.*?', $keyword);
if (preg_match('/\w/', $keyword) === 0) {
// Use lookaheads/lookbehinds for boundary-like behavior when the keyword consists of non-word characters (e.g. ,, &, -)
// because \b only works for transitions between word characters (letters, digits, _) and non-word characters.
$keyword = "(?<!\w)" . $keyword . "(?!\w)";
} else {
$keyword = '\\b' . $keyword . '\\b';
}
$union->sql = "
SELECT resource, [bit_or_condition] hit_count AS score
FROM resource_node rn[union_index]
WHERE rn[union_index].node IN
(SELECT ref FROM `node` WHERE name RLIKE ? "
. $union_restriction_clause->sql . ")
GROUP BY resource " .
($non_field_keyword_sql->sql != "" ? $non_field_keyword_sql->sql : "");
}
$union->parameters = array_merge(["s",$keyword], $union_restriction_clause->parameters);
if ($non_field_keyword_sql->sql != "") {
$union->parameters = array_merge($union->parameters, $non_field_keyword_sql->parameters);
}
$sql_keyword_union[] = $union;
$sql_keyword_union_criteria[] = "`h`.`keyword_[union_index]_found`";
$sql_keyword_union_or[] = "";
$sql_keyword_union_aggregation[] = "BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`";
} else // we are dealing with a standard keyword match
{
// ----- resource_node -> node_keyword sub query -----
$union = new PreparedStatementQuery();
$union->sql = " SELECT resource, [bit_or_condition] hit_count AS score
FROM resource_node rn[union_index]
WHERE rn[union_index].node IN
(SELECT node
FROM `node_keyword` nk[union_index]
WHERE ((nk[union_index].keyword = ? " . $relatedsql->sql . ") " . $union_restriction_clause->sql . ")" .
($alternative_keywords_sql->sql != "" ? ($alternative_keywords_sql->sql . $union_restriction_clause->sql) : "" ) .
") GROUP BY resource " .
($non_field_keyword_sql->sql != "" ? $non_field_keyword_sql->sql : "") ;
$union->parameters = array_merge(["i",$keyref], $relatedsql->parameters, $union_restriction_clause->parameters);
if ($alternative_keywords_sql->sql != "") {
$union->parameters = array_merge($union->parameters, $alternative_keywords_sql->parameters, $union_restriction_clause->parameters);
}
if ($non_field_keyword_sql->sql != "") {
$union->parameters = array_merge($union->parameters, $non_field_keyword_sql->parameters);
}
$sql_keyword_union[] = $union;
// ---- end of resource_node -> node_keyword sub query -----
$sql_keyword_union_criteria[] = "`h`.`keyword_[union_index]_found`";
$sql_keyword_union_aggregation[] = "BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`";
$sql_keyword_union_or[] = $keywords_expanded_or;
// Log this
if ($stats_logging && !$go) {
$keywords_used[] = $keyref;
}
} // End of standard keyword match
} // end if not omit
} // end found wildcards
} // end handle wildcards
} // end normal keyword
} // End of if not quoted string and end of check if special search
if ($quoted_string && !$quoted_field_match) {
$quotedfieldid = "";
// This keyword is a quoted string, split into keywords but don't preserve quotes this time
if ($field_short_name_specified && isset($fieldinfo['ref'])) {
// We have already parsed the keyword when looking for a node, get string and then filter on this field
$quotedkeywords = split_keywords($keystring);
$quotedfieldid = $fieldinfo['ref'];
} else {
$quotedkeywords = split_keywords(substr($keyword, 1, -1));
}
$omit = false;
if (substr($keyword, 0, 1) == "-") {
$omit = true;
$keyword = substr($keyword, 1);
}
$qk = 1; // Set the counter to the first keyword
$last_key_offset = 1;
$fixedunion = new PreparedStatementQuery();
$fixedunioncondition = new PreparedStatementQuery();
foreach ($quotedkeywords as $quotedkeyword) {
global $noadd, $wildcard_always_applied;
if (in_array($quotedkeyword, $noadd)) { # skip common words that are excluded from indexing
# Support skipped keywords - if the last keyword was skipped (listed in $noadd), increase the allowed position from the previous keyword. Useful for quoted searches that contain $noadd words, e.g. "black and white" where "and" is a skipped keyword.
++$last_key_offset;
} else {
$keyref = resolve_keyword($quotedkeyword, false, true, false); # Resolve keyword.
if ($keyref === false) {
# make a new keyword
$keyref = resolve_keyword($quotedkeyword, true, true, false);
}
$union_restriction_clause = new PreparedStatementQuery();
if (!empty($sql_exclude_fields)) {
$union_restriction_clause->sql .= " AND nk_[union_index]_" . $qk . ".node NOT IN (SELECT ref FROM node WHERE resource_type_field IN (" . ps_param_insert(count($sql_exclude_fields)) . "))";
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ps_param_fill($sql_exclude_fields, "i"));
}
if (count($hidden_indexed_fields) > 0) {
$union_restriction_clause->sql .= " AND nk_[union_index]_" . $qk . ".node NOT IN (SELECT ref FROM node WHERE resource_type_field IN (" . ps_param_insert(count($hidden_indexed_fields)) . "))";
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ps_param_fill($hidden_indexed_fields, "i"));
}
if ($quotedfieldid != "") {
$union_restriction_clause->sql .= " AND nk_[union_index]_" . $qk . ".node IN (SELECT ref FROM node WHERE resource_type_field = ? )";
$union_restriction_clause->parameters = array_merge($union_restriction_clause->parameters, ["i",$quotedfieldid]);
}
if ($qk == 1) {
// Add code to find matching nodes in resource_node
$fixedunion->sql = " SELECT rn_[union_index]_" . $qk . ".resource, [bit_or_condition] rn_[union_index]_" . $qk . ".hit_count AS score FROM resource_node rn_[union_index]_" . $qk .
" LEFT OUTER JOIN `node_keyword` nk_[union_index]_" . $qk . " ON rn_[union_index]_" . $qk . ".node=nk_[union_index]_" . $qk . ".node AND (nk_[union_index]_" . $qk . ".keyword = ? " . ")";
$fixedunion->parameters = array_merge($fixedunion->parameters, ["i",$keyref]);
$fixedunioncondition->sql = "nk_[union_index]_" . $qk . ".keyword = ? " . $union_restriction_clause->sql;
$fixedunioncondition->parameters = array_merge(["i",$keyref], $union_restriction_clause->parameters);
} else {
# For keywords other than the first one, check the position is next to the previous keyword.
# Also check these occurances are within the same field.
$fixedunion->sql .= " JOIN `node_keyword` nk_[union_index]_" . $qk . " ON nk_[union_index]_" . $qk . ".node = nk_[union_index]_" . ($qk - 1) . ".node AND nk_[union_index]_" . $qk . ".keyword = ? AND nk_[union_index]_" . $qk . ".position=nk_[union_index]_" . ($qk - 1) . ".position+" . $last_key_offset ;
array_push($fixedunion->parameters, "i", $keyref);
}
$last_key_offset = 1;
$qk++;
} // End of if keyword not excluded (not in $noadd array)
} // End of each keyword in quoted string
if (trim($fixedunioncondition->sql) != "") {
if ($omit) {# Exclude matching resources from query (omit feature)
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= str_replace("[bit_or_condition]", "", " r.ref NOT IN (SELECT resource FROM (" . $fixedunion->sql . " WHERE " . $fixedunioncondition->sql . ") qfilter[union_index]) "); # Instead of adding to the union, filter out resources that do contain the quoted string.
$sql_filter->parameters = array_merge($sql_filter->parameters, $fixedunion->parameters, $fixedunioncondition->parameters);
} elseif (is_a($fixedunion, "PreparedStatementQuery")) {
$addunion = new PreparedStatementQuery();
$addunion->sql = $fixedunion->sql . " WHERE " . $fixedunioncondition->sql . " GROUP BY resource ";
$addunion->parameters = array_merge($fixedunion->parameters, $fixedunioncondition->parameters);
$sql_keyword_union[] = $addunion;
$sql_keyword_union_aggregation[] = "BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found` ";
$sql_keyword_union_or[] = false;
$sql_keyword_union_criteria[] = "`h`.`keyword_[union_index]_found`";
}
}
$c++;
}
} // end keywords expanded loop
if (isset($datefieldjoin)) {
$date_string = sprintf(
"%s-%s-%s",
$date_parts['year'] ?? '____',
$date_parts['month'] ?? '__',
$date_parts['day'] ?? '__'
) . '%';
$sql_filter->sql .= ($sql_filter->sql != "" ? " AND " : "") . "rdn" . $datefieldjoin . ".name like ? ";
array_push($sql_filter->parameters, "s", $date_string);
}
} // end keysearch if

View File

@@ -0,0 +1,35 @@
<?php
// Node filtering for main search
$node_bucket_sql = "";
$rn = 0;
$node_hitcount = "";
foreach ($node_bucket as $node_bucket_or) {
if ($category_tree_search_use_and_logic) {
foreach ($node_bucket_or as $node_bucket_and) {
$sql_join->sql .= ' JOIN `resource_node` rn' . $rn . ' ON r.`ref`=rn' . $rn . '.`resource` AND rn' . $rn . '.`node` = ?';
array_push($sql_join->parameters, "i", $node_bucket_and);
$node_hitcount .= (($node_hitcount != "") ? " +" : "") . "rn" . $rn . ".hit_count";
$rn++;
}
} else {
$sql_join->sql .= ' JOIN `resource_node` rn' . $rn . ' ON r.`ref`=rn' . $rn . '.`resource` AND rn' . $rn . '.`node` IN (' . ps_param_insert(count($node_bucket_or)) . ')';
$sql_join->parameters = array_merge($sql_join->parameters, ps_param_fill($node_bucket_or, "i"));
$node_hitcount .= (($node_hitcount != "") ? " +" : "") . "rn" . $rn . ".hit_count";
$rn++;
}
}
if ($node_hitcount != "") {
$sql_hitcount_select = "(SUM(" . $sql_hitcount_select . ") + SUM(" . $node_hitcount . ")) ";
}
$select->sql .= ", " . $sql_hitcount_select . " total_hit_count";
$sql_filter->sql = $node_bucket_sql . $sql_filter->sql;
if (count($node_bucket_not) > 0) {
$sql_filter->sql = 'NOT EXISTS (SELECT `resource`, node FROM `resource_node` WHERE r.ref=`resource` AND `node` IN (' .
ps_param_insert(count($node_bucket_not)) . ')) AND ' . $sql_filter->sql;
$sql_filter->parameters = array_merge(ps_param_fill($node_bucket_not, "i"), $sql_filter->parameters);
}

View File

@@ -0,0 +1,29 @@
<?php
# Could not match on provided keywords? Attempt to return some suggestions.
if (!$fullmatch) {
if ($suggested == $keywords) {
# Nothing different to suggest.
debug("No alternative keywords to suggest.");
return "";
} else {
# Suggest alternative spellings/sound-a-likes
$suggest = "";
if (strpos($search, ",") === false) {
$suggestjoin = " ";
} else {
$suggestjoin = ", ";
}
foreach ($suggested as $suggestion) {
if ($suggestion != "") {
if ($suggest != "") {
$suggest .= $suggestjoin;
}
$suggest .= $suggestion;
}
}
debug("Suggesting $suggest");
return $suggest;
}
}

View File

@@ -0,0 +1,80 @@
<?php
# ---------------------------------------------------------------
# Keyword union assembly.
# Use UNIONs for keyword matching instead of the older JOIN technique - much faster
# Assemble the new join from the stored unions
# ---------------------------------------------------------------
if (count($sql_keyword_union) > 0) {
$union_sql_arr = [];
$union_sql_params = [];
for ($i = 1; $i <= count($sql_keyword_union); $i++) {
$bit_or_condition = "";
for ($y = 1; $y <= count($sql_keyword_union); $y++) {
if ($i == $y) {
$bit_or_condition .= " TRUE AS `keyword_{$y}_found`, ";
} else {
$bit_or_condition .= " FALSE AS `keyword_{$y}_found`,";
}
}
$sql_keyword_union[($i - 1)]->sql = str_replace('[bit_or_condition]', $bit_or_condition, $sql_keyword_union[($i - 1)]->sql);
$sql_keyword_union[($i - 1)]->sql = str_replace('[union_index]', $i, $sql_keyword_union[($i - 1)]->sql);
$sql_keyword_union[($i - 1)]->sql = str_replace('[union_index_minus_one]', ($i - 1), $sql_keyword_union[($i - 1)]->sql);
$union_sql_arr[] = $sql_keyword_union[$i - 1]->sql;
$union_sql_params = array_merge($union_sql_params, $sql_keyword_union[$i - 1]->parameters);
}
for ($i = 1; $i <= count($sql_keyword_union_criteria); $i++) {
$sql_keyword_union_criteria[($i - 1)] = str_replace('[union_index]', $i, $sql_keyword_union_criteria[($i - 1)]);
$sql_keyword_union_criteria[($i - 1)] = str_replace('[union_index_minus_one]', ($i - 1), $sql_keyword_union_criteria[($i - 1)]);
}
for ($i = 1; $i <= count($sql_keyword_union_aggregation); $i++) {
$sql_keyword_union_aggregation[($i - 1)] = str_replace('[union_index]', $i, $sql_keyword_union_aggregation[($i - 1)]);
}
$sql_join->sql .= " JOIN (
SELECT resource,sum(score) AS score,
" . join(", ", $sql_keyword_union_aggregation) . " from
(" . join(" union ", $union_sql_arr) . ") AS hits GROUP BY resource) AS h ON h.resource=r.ref ";
$sql_join->parameters = array_merge($sql_join->parameters, $union_sql_params);
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
if (count($sql_keyword_union_or) != count($sql_keyword_union_criteria)) {
debug("Search error - union criteria mismatch");
return "ERROR";
}
$sql_filter->sql .= "(";
for ($i = 0; $i < count($sql_keyword_union_or); $i++) {
// Builds up the string of conditions that will be added when joining to the $sql_keyword_union_criteria
if ($i == 0) {
$sql_filter->sql .= $sql_keyword_union_criteria[$i];
continue;
}
if ($sql_keyword_union_or[$i] != $sql_keyword_union_or[$i - 1]) {
$sql_filter->sql .= ') AND (' . $sql_keyword_union_criteria[$i];
continue;
}
if ($sql_keyword_union_or[$i]) {
$sql_filter->sql .= ' OR ';
} else {
$sql_filter->sql .= ' AND ';
}
$sql_filter->sql .= $sql_keyword_union_criteria[$i];
}
$sql_filter->sql .= ")";
# Use amalgamated resource_keyword hitcounts for scoring (relevance matching based on previous user activity)
$score = "h.score";
}

View File

@@ -0,0 +1,63 @@
<?php
$download_summary = download_summary($ref);
$total = 0;
foreach ($download_summary as $usage) {
if (array_key_exists($usage["usageoption"], $download_usage_options)) {
$total += $usage["c"];
} elseif ($usage['usageoption'] == '-1') {
$total += $usage['c'];
}
}
$rl_url = "{$baseurl}/pages/log.php";
$rl_params = array(
"ref" => $ref,
"search" => $search,
"order_by" => $order_by,
"sort" => $sort,
"archive" => $archive,
"filter_by_type" => "d",
);
$rl_params_override = array(
"filter_by_usageoption" => null,
);
?>
<div class="RecordDownloadSpace" id="RecordDownloadSummary" style="margin-right:10px; display: none;">
<table cellpadding="0" cellspacing="0">
<tr>
<td colspan=2>
<a href="<?php echo generateURL($rl_url, $rl_params, $rl_params_override); ?>" onclick="return ModalLoad(this, true);">
<?php echo LINK_CARET . escape($lang["usagetotal"]); ?>
</a>
</td>
</tr>
<tr class="DownloadDBlend" >
<td><?php echo escape($lang["usagetotalno"]); ?></td>
<td width="20%"><?php echo $total ?></td>
</tr>
</table>
<?php if ($total > 0 && $download_usage && $usage['usageoption'] != '-1') { ?>
<table cellpadding="0" cellspacing="0">
<tr>
<td colspan=2><?php echo escape($lang["usagebreakdown"]); ?></td>
</tr>
<?php
foreach ($download_summary as $usage) {
if (array_key_exists($usage["usageoption"], $download_usage_options)) {
$rl_params_override["filter_by_usageoption"] = $usage["usageoption"];
?>
<tr>
<td>
<a href="<?php echo generateURL($rl_url, $rl_params, $rl_params_override); ?>" onclick="return ModalLoad(this, true);">
<?php echo LINK_CARET . escape(i18n_get_translated($download_usage_options[$usage["usageoption"]])); ?>
</a>
</td>
<td width="20%"><?php echo $usage["c"]; ?></td>
</tr>
<?php
}
}
?>
</table>
<?php } ?>
</div>

View File

@@ -0,0 +1,230 @@
<?php
$on_upload = ($pagename == "upload_batch");
if ($on_upload || (isset($ref) && $ref < 0)) {
if ($show_status_and_access_on_upload && !$on_upload) {
?>
</div><!-- end of previous collapsing section --> <?php
}
if ($tabs_on_edit && !$on_upload) {
?>
<h1><?php echo escape($lang["upload-options"]); ?></h1>
<div id="UploadOptionsSection">
<?php
} elseif (!$on_upload) {
?>
<h2 class="CollapsibleSectionHead"><?php echo escape($lang["upload-options"]); ?></h2>
<div class="CollapsibleSection" id="UploadOptionsSection">
<?php
}
if ($on_upload || !$embedded_data_user_select) {
if ($metadata_read) {
// Show option to prevent extraction of exif data
?>
<div class="Question" id="question_noexif">
<label for="no_exif"><?php echo escape($lang["no_exif"]) ?></label>
<input
type=checkbox
id="no_exif"
name="no_exif"
value="yes"
<?php if (getval("no_exif", ($metadata_read_default) ? "" : "no") != "") { ?>
checked
<?php } ?>>
<div class="clearerleft"></div>
</div>
<?php
} elseif ($no_metadata_read_default) {
// Confusing, set the value to null so that metadata will be extracted
?>
<input type=hidden id="no_exif" name="no_exif" value="">
<?php
} else {
// Set the value to no so that metadata will be extracted
?>
<input type=hidden id="no_exif" name="no_exif" value="no">
<?php
}
}
if ($enable_related_resources && $relate_on_upload && ($on_upload || ($ref < 0 && !$multiple)) && !$external_upload) { # When uploading
?>
<div class="Question" id="question_related">
<label for="relateonupload"><?php echo escape($lang["relatedresources_onupload"]) ?></label>
<input name="relateonupload" id="relateonupload" type="checkbox" value="1" style="margin-top:7px;" <?php if ($relate_on_upload_default) {
echo " checked ";
} ?>/>
<div class="clearerleft"> </div>
</div><?php
}
if (getval("single", "") == "" || $on_upload) {
$non_col_options = 0;
# Add Resource Batch: specify default content - also ask which collection to add the resource to.
if ($enable_add_collection_on_upload && !(isset($external_upload) && $external_upload)) {
$collection_add = getval("collection_add", "");
?>
<div
class="Question<?php
if (!$on_upload && isset($save_errors) && is_array($save_errors) && array_key_exists('collectionname', $save_errors)) {
echo " FieldSaveError";
} ?>"
id="question_collectionadd">
<label for="collection_add"><?php echo escape($lang["addtocollection"]) ?></label>
<select name="collection_add" id="collection_add" class="stdwidth">
<?php if (can_create_collections() && $upload_add_to_new_collection_opt) { ?>
<option value="new" <?php echo ($upload_add_to_new_collection) ? "selected" : ''; ?>>
(<?php echo escape($lang["createnewcollection"]) ?>)
</option>
<?php
}
if ($upload_do_not_add_to_new_collection_opt) { ?>
<option
value="false"
<?php if (
!$upload_add_to_new_collection
|| $do_not_add_to_new_collection_default
|| $collection_add == 'false'
) { ?>
selected
<?php
} ?>>
<?php echo escape($lang["batchdonotaddcollection"]) ?>
</option>
<?php
}
// If the user is attached to a collection that is not allowed to add resources to,
// then we hide this collection from the drop down menu of the upload page
$temp_list = get_user_collections($userref);
$list = array();
$hide_non_editable = array();
for ($n = 0; $n < count($temp_list); $n++) {
if (!($temp_list[$n]['user'] != $userref && $temp_list[$n]['allow_changes'] == 0)) {
array_push($list, $temp_list[$n]);
}
}
$currentfound = false;
// Make sure it's possible to set the collection with collection_add (compact style "upload to this collection"
if (is_numeric($collection_add) && getval("resetform", "") == "" && (!isset($save_errors) || !$save_errors)) {
# Switch to the selected collection (existing or newly created) and refresh the frame.
set_user_collection($userref, $collection_add);
refresh_collection_frame($collection_add);
}
for ($n = 0; $n < count($list); $n++) {
# Remove smart collections as they cannot be uploaded to.
if (
(
!isset($list[$n]['savedsearch'])
||
(
isset($list[$n]['savedsearch'])
&& $list[$n]['savedsearch'] == null
)
)
#show only active collections if a start date is set for $active_collections
&&
(
strtotime($list[$n]['created']) > ((isset($active_collections)) ? strtotime($active_collections) : 1)
||
(
$list[$n]['name'] == "Default Collection"
&& $list[$n]['user'] == $userref
)
)
) {
if ($list[$n]["ref"] == $usercollection) {
$currentfound = true;
}
?>
<option value="<?php echo $list[$n]["ref"]; ?>" <?php echo ($list[$n]['ref'] == $collection_add) ? "selected" : ''; ?>>
<?php echo i18n_get_collection_name($list[$n]) ?>
</option>
<?php
}
}
if (!$currentfound) {
# The user's current collection has not been found in their list of collections (perhaps they have selected a theme to edit). Display this as a separate item.
$cc = get_collection($usercollection);
//Check if the current collection is editable as well by checking $cc['allow_changes']
if (false !== $cc && collection_writeable($usercollection)) {
$currentfound = true;
?>
<option
value="<?php echo escape($usercollection) ?>"
<?php if (is_numeric($collection_add) && $usercollection == $collection_add) { ?>
selected
<?php } ?>>
<?php echo i18n_get_collection_name($cc)?>
</option>
<?php
}
}
?>
</select>
<div class="clearerleft"></div>
<div name="collectioninfo" id="collectioninfo" style="display:none;">
<div
name="collectionname"
id="collectionname"
style="display: <?php echo $upload_add_to_new_collection_opt ? 'block' : 'none'; ?>">
<label for="entercolname">
<?php
echo escape($lang["collectionname"]);
echo $upload_collection_name_required ? "<sup>*</sup>" : '';
?>
</label>
<input type=text id="entercolname" name="entercolname" class="stdwidth" value='<?php echo htmlentities(stripslashes(getval("entercolname", "")), ENT_QUOTES);?>'>
</div>
</div> <!-- end collectioninfo -->
</div> <!-- end question_collectionadd -->
<script>
jQuery(document).ready(function() {
jQuery('#collection_add').change(function () {
if (jQuery('#collection_add').val() == 'new') {
jQuery('#collectioninfo').fadeIn();
} else {
jQuery('#collectioninfo').fadeOut();
}
});
jQuery('#collection_add').change();
});
</script>
<?php
} // end enable_add_collection_on_upload
}
}
if ($on_upload) {
?>
</div> <!-- End of Upload options -->
<div class="BasicsBox">
<script>
// Add code to change URL if options change
jQuery(document).ready(function() {
jQuery('#relateonupload').on('change', function () {
if (jQuery(this).is(':checked')) {
relate_on_upload = true;
} else {
relate_on_upload = false;
}
});
});
</script>
<?php
} elseif ($edit_upload_options_at_top) {
?>
<!-- End of Upload options -->
<?php
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* Encrypts data
*
* @uses generateSecureKey()
*
* @param string $data Data to be encypted
* @param string $key Key to use
* @param string $keylength Optional key length
* @todo Add a fourth parameter to use with custom metadata (NOT ResourceSpace metadata) for generating MAC. this should
* add extra security by making MAC harder to be forged
*
* @return string Encrypted data
*/
function rsEncrypt($data, $key, $keylength = 128)
{
global $scramble_key;
/*
Encrypt-then-MAC (EtM)
======================
PlainText
|
Encryption <-- Key
|_________ |
| | |
| HashFunction
| |
--------------------
| Ciphertext | MAC |
--------------------
The plaintext is first encrypted, then a MAC is produced based on the resulting ciphertext. The ciphertext and its
MAC are sent together.
*/
$method = "AES-128-CTR";
$options = OPENSSL_RAW_DATA;
$nonce = generateSecureKey($keylength);
// Get 2 derived subkeys, one for message authentication code (MAC) and the other one for encryption/ decryption.
$mac_key = hash_hmac("sha256", "mac_key", $scramble_key, true);
$enc_key = hash_hmac("sha256", "enc_key", $scramble_key, true);
// Synthetic Initialization Vector (SIV)
$siv = substr(hash_hmac("sha256", "{$nonce}{$scramble_key}{$key}", $mac_key, true), 0, 16);
$cyphertext = bin2hex(openssl_encrypt($data, $method, $enc_key, $options, $siv));
$mac = hash_hmac("sha256", "{$cyphertext}{$nonce}{$scramble_key}", $mac_key);
return "{$nonce}@@{$cyphertext}@@{$mac}";
}
/**
* Decrypts data
*
* @param string $data Data to be decrypted
* @param string $key
* @todo Add a third parameter to use with custom metadata (NOT ResourceSpace metadata) for generating MAC. this should
* add extra security by making MAC harder to be forged
*
* @return false|string Returns FALSE if MAC check failed, plaintext otherwise
*/
function rsDecrypt($data, $key)
{
global $scramble_key;
$method = "AES-128-CTR";
$options = OPENSSL_RAW_DATA;
// Get 2 derived subkeys, one for message authentication code (MAC) and the other one for encryption/ decryption.
$mac_key = hash_hmac("sha256", "mac_key", $scramble_key, true);
$enc_key = hash_hmac("sha256", "enc_key", $scramble_key, true);
if (count(explode("@@", $data)) < 3) {
return false;
}
list($nonce, $cyphertext, $mac) = explode("@@", $data);
// Check MAC
if ($mac !== hash_hmac("sha256", "{$cyphertext}{$nonce}{$scramble_key}", $mac_key)) {
debug("rsCrypt: MAC did not match!");
return false;
}
// Synthetic Initialization Vector (SIV)
$siv = substr(hash_hmac("sha256", "{$nonce}{$scramble_key}{$key}", $mac_key, true), 0, 16);
return openssl_decrypt(hex2bin($cyphertext), $method, $enc_key, $options, $siv);
}
/**
* Prior to eval() checks to make sure the code has been signed first, by the offline script / migration script.
*
* @param string $code The code to check
*
* @return string The code, if correctly signed, or an empty string if not.
*/
function eval_check_signed($code)
{
// No need to sign empty string.
if (trim($code) == "") {
return "";
}
// Extract the signature from the code.
$code_split = explode("\n", $code);
if (count($code_split) < 2) {
set_sysvar("code_sign_required", "YES");
return "";
} // Not enough lines to include a key, exit
$signature = str_replace("//SIG", "", trim($code_split[0])); // Extract signature
$code = trim(substr($code, strpos($code, "\n") + 1));
// Code not signed correctly? Exit early.
if ($signature != sign_code($code)) {
set_sysvar("code_sign_required", "YES");
return "";
}
// All is as expected, return the code ready for execution.
return $code;
}
/**
* Returns a signature for a given block of code.
*
* @param string $code The code to sign
*
* @return string The signature
*/
function sign_code($code)
{
global $scramble_key;
return hash_hmac("sha256", trim($code), $scramble_key);
}
/**
* Returns a signature for a given block of code.
*
* @param bool $confirm Require user to approve code changes when resigning from the server side.
* @param bool $output Display output. $confirm will override this option to provide detail if approval needed.
* @param bool $output_changes_only Output changes only - do not sign code.
*
* @return void
*/
function resign_all_code($confirm = true, $output = true, $output_changes_only = false)
{
if ($confirm) {
$output = true;
}
$todo = array
(
array("resource_type_field", "value_filter"),
array("resource_type_field", "onchange_macro"),
array("resource_type_field", "autocomplete_macro"),
array("resource_type_field", "exiftool_filter"),
array("resource_type", "config_options"),
array("usergroup", "config_options")
);
foreach ($todo as [$table, $column]) {
$rows = ps_query("select ref,`$column` from `$table`");
foreach ($rows as $row) {
$code = $row[$column];
$ref = $row["ref"];
if (trim((string)$code) == "") {
$code = "";
}
if ($output && !$output_changes_only) {
echo $table . " -> " . $column . " -> " . $ref;
}
// Extract signature if already one present
$purecode = $code;
if (substr($code, 0, 5) == "//SIG") {
$purecode = trim(substr($code, strpos($code, "\n") + 1));
}
if (trim(eval_check_signed($code)) !== trim($purecode)) {
// Code is not signed.
// Needs signing. Confirm it's safe.
if ($confirm) {
if (!$output_changes_only) {
echo " needs signing\n-----------------------------\n";
echo $purecode;
echo "\n-----------------------------\nIs this code safe? (y/n)";
ob_flush();
$line = fgets(STDIN);
if (trim($line) != "y") {
exit();
}
} else {
echo $table . " -> " . $column . " -> " . $ref . "\n" . $code . "\n\n";
}
}
$code = trim($code);
$code = "//SIG" . sign_code($code) . "\n" . $code;
if (!$output_changes_only) {
ps_query("update `$table` set `$column`=? where ref=?", array("s",$code,"i",$ref));
}
} else {
if ($output && !$output_changes_only) {
echo " is OK\n";
}
}
}
}
// Clear the cache so the code uses the updated signed code.
if (!$output_changes_only) {
clear_query_cache("schema");
set_sysvar("code_sign_required", "");
}
}
/**
* Used to compare the user's provided token with the expected value derived from the given identifier
*
* Used by isValidCSRFToken()
* Also used on upload_batch to validate an upload session when user cookie not available (i.e. companion uploads)
*
* @uses rsDecrypt()
*
* @param string $token_data Encrypted token data
* @param string $id Identifier
*
* @return bool
*
*/
function rs_validate_token($token_data, $id)
{
if (trim($token_data) === "") {
debug("rs_validate_token(): INVALID - no token data");
return false;
}
$plaintext = rsDecrypt($token_data, $id);
if ($plaintext === false) {
debug("rs_validate_token(): INVALID - unable to decrypt token data");
return false;
}
$csrf_data = json_decode($plaintext, true);
if (is_null($csrf_data)) {
debug("rs_validate_token(): INVALID - unable to decode token data");
return false;
}
if ($csrf_data["session"] === $id) {
return true;
}
debug("rs_validate_token(): INVALID - decoded value does not match");
return false;
}

View File

@@ -0,0 +1,185 @@
<?php
use Montala\ResourceSpace\CommandPlaceholderArg;
/**
* Initialize facial recognition functionality.
*
* IMPORTANT: only one field can be setup for the annotation side and it also MUST be a dynamic keywords list
*
* @uses ps_value()
*
* @return boolean
*/
function initFacialRecognition()
{
global $facial_recognition_tag_field, $facial_recognition_face_recognizer_models_location, $annotate_enabled,
$annotate_fields;
if (!is_numeric($facial_recognition_tag_field) || 0 >= $facial_recognition_tag_field) {
return false;
}
if (!is_dir($facial_recognition_face_recognizer_models_location)) {
if (strpos($facial_recognition_face_recognizer_models_location, $GLOBALS["storagedir"]) === 0) {
// Create the directory as it is in the configured filestore
mkdir($facial_recognition_face_recognizer_models_location, 0777, true);
} else {
// Folder needs to be created by server admin
$error = str_replace(
["%variable","%path"],
["\$facial_recognition_face_recognizer_models_location", $facial_recognition_face_recognizer_models_location],
$GLOBALS["lang"]["error_invalid_path"]
);
if (PHP_SAPI == "cli") {
echo $error . PHP_EOL;
} else {
debug($error);
}
return false;
}
}
$facial_recognition_rtf_type = ps_value(
"SELECT `type` AS `value`
FROM resource_type_field
WHERE ref = ?
",
array("i",$facial_recognition_tag_field),
null,
"schema"
);
if (FIELD_TYPE_DYNAMIC_KEYWORDS_LIST != $facial_recognition_rtf_type) {
$error = str_replace(
["%variable","%type"],
["\$facial_recognition_tag_field", $GLOBALS["lang"]["fieldtype-dynamic_keywords_list"]],
$GLOBALS["lang"]["error_invalid_field_type"]
);
if (PHP_SAPI == "cli") {
echo $error . PHP_EOL;
} else {
debug($error);
}
return false;
}
$annotate_enabled = true;
$annotate_fields[] = $facial_recognition_tag_field;
return true;
}
/**
* Crops out a selected area of an image and makes it ready to be used by FaceRecognizer.
*
* Note: The selected area should follow the normalized coordinate system.
*
* @uses get_utility_path()
* @uses debug()
*
* @param string $image_path Path of the source image
* @param string $prepared_image_path Path of the prepared image
* @param float $x X position
* @param float $y Y position
* @param float $width Width
* @param float $height Height
* @param boolean $overwrite_existing Set to TRUE to overwrite existing prepared image (if any exists)
*
* @return boolean
*/
function prepareFaceImage($image_path, $prepared_image_path, $x, $y, $width, $height, $overwrite_existing = false)
{
if (!file_exists($image_path)) {
debug("FACIAL_RECOGNITION: Could not find image at '{$image_path}'");
return false;
}
// Use existing prepared image if one is found
if (!$overwrite_existing && file_exists($prepared_image_path)) {
return true;
}
// X, Y, width and height MUST be numeric
if (!is_numeric($x) || !is_numeric($y) || !is_numeric($width) || !is_numeric($height)) {
return false;
}
$convert_fullpath = get_utility_path('im-convert');
if (false === $convert_fullpath) {
debug('FACIAL_RECOGNITION: Could not find ImageMagick "convert" utility!');
return false;
}
list($image_width, $image_height) = getimagesize($image_path);
$image_path_escaped = escapeshellarg($image_path);
$prepared_image_path_escaped = escapeshellarg($prepared_image_path);
$x = escapeshellarg(round($x * $image_width, 0));
$y = escapeshellarg(round($y * $image_height, 0));
$width = escapeshellarg(round($width * $image_width, 0));
$height = escapeshellarg(round($height * $image_height, 0));
$cmd = $convert_fullpath;
$cmd .= " {$image_path_escaped} -colorspace gray -depth 8";
$cmd .= " -crop {$width}x{$height}+{$x}+{$y}";
$cmd .= " -resize 90x90\>";
$cmd .= " +repage {$prepared_image_path_escaped}";
if ('' !== run_command($cmd)) {
return false;
}
return true;
}
/**
* Use FaceRecognizer to predict the association between a face and a label (i.e person name)
*
* @param string $model_file_path Path to the FaceRecognizer model state file
* @param string $test_image_path Path to the prepared image we are testing
*
* @return boolean|array Return the label ID and probability on successful prediction or FALSE on error
*/
function faceRecognizerPredict($model_file_path, $test_image_path)
{
if (!file_exists($model_file_path)) {
debug("FACIAL_RECOGNITION: Could not find model at '{$model_file_path}'");
return false;
}
if (!file_exists($test_image_path)) {
debug("FACIAL_RECOGNITION: Could not find the test image at '{$test_image_path}'");
return false;
}
$python_fullpath = get_utility_path('python');
if (false === $python_fullpath) {
debug('FACIAL_RECOGNITION: Could not find Python!');
return false;
}
$faceRecognizer_path = __DIR__ . '/../lib/facial_recognition/faceRecognizer.py';
$cmdparams = [
'[MODEL_PATH]' => new CommandPlaceholderArg(
$model_file_path,
fn($p) => is_valid_rs_path($p,
[
$GLOBALS['storagedir'],
$GLOBALS['facial_recognition_face_recognizer_models_location'],
])
),
'[IMAGE_PATH]' => new CommandPlaceholderArg($test_image_path, 'is_valid_rs_path'),
];
$command = "{$python_fullpath} {$faceRecognizer_path} [MODEL_PATH] [IMAGE_PATH]";
$prediction = run_command($command, false, $cmdparams, 300);
$prediction = json_decode($prediction);
if (null === $prediction || 2 > count($prediction)) {
return false;
}
return $prediction;
}

View File

@@ -0,0 +1,247 @@
<?php
// Included from preview_preprocessing.php
use Montala\ResourceSpace\CommandPlaceholderArg;
# Increase time limit
set_time_limit(0);
$ffmpeg_fullpath = get_utility_path("ffmpeg");
if ($generateall) {
# Create a preview video
$targetfile = get_resource_path($ref, true, "pre", false, $ffmpeg_preview_extension, -1, 1, false, "", $alternative);
if (PHP_SAPI !== "cli") {
set_processing_message(str_replace("[resource]", $ref, $lang["processing_preview_video"]));
}
if (file_exists($target)) {
$snapshotsize = getimagesize($target);
$width = $snapshotsize[0];
$height = $snapshotsize[1];
} else {
# Get preview sizes from DB
$preview_sizes = get_all_image_sizes(true);
$pre_size = array_values(array_filter($preview_sizes, function ($var) {
return $var['id'] == 'pre';
}));
$width = $pre_size[0]['width'];
$height = $pre_size[0]['height'];
}
$sourcewidth = $width;
$sourceheight = $height;
global $config_windows, $ffmpeg_get_par;
if ($ffmpeg_get_par) {
$par = 1;
# Find out the Pixel Aspect Ratio
$shell_exec_cmd = $ffmpeg_fullpath . " -i %%FILE%% 2>&1";
$shell_exec_params = ["%%FILE%%" => new CommandPlaceholderArg($file, 'is_valid_rs_path')];
if (isset($ffmpeg_command_prefix)) {
$shell_exec_cmd = $ffmpeg_command_prefix . " " . $shell_exec_cmd;
}
$output = run_command($shell_exec_cmd, false, $shell_exec_params);
preg_match('/PAR ([0-9]+):([0-9]+)/m', $output, $matches);
if (intval($matches[1] ?? 0) > 0 && intval($matches[2] ?? 0) > 0) {
$par = $matches[1] / $matches[2];
if ($par < 1) {
$width = ceil($width * $par);
} elseif ($par > 1) {
$height = ceil($height / $par);
}
}
}
if ($height < $ffmpeg_preview_min_height) {
$height = $ffmpeg_preview_min_height;
}
if ($width < $ffmpeg_preview_min_width) {
$width = $ffmpeg_preview_min_width;
}
if ($height > $ffmpeg_preview_max_height) {
$width = ceil($width * ($ffmpeg_preview_max_height / $height));
$height = $ffmpeg_preview_max_height;
}
if ($width > $ffmpeg_preview_max_width) {
$height = ceil($height * ($ffmpeg_preview_max_width / $width));
$width = $ffmpeg_preview_max_width;
}
# Frame size must be a multiple of two
if ($width % 2) {
$width++;
}
if ($height % 2) {
$height++;
}
/* Plugin hook to modify the output W & H before running ffmpeg. Better way to return both W and H at the same is appreciated. */
$tmp = hook("ffmpegbeforeexec", "", array($ffmpeg_fullpath, $file));
if (is_array($tmp) && $tmp) {
list($width, $height) = $tmp;
}
if (hook("replacetranscode", "", array($file,$targetfile,$ffmpeg_global_options,$ffmpeg_preview_options,$width,$height))) {
exit(); // Do not proceed, replacetranscode hook intends to avoid everything below
}
if ($extension == 'gif') {
global $ffmpeg_preview_gif_options;
$ffmpeg_preview_options = $ffmpeg_preview_gif_options;
}
$shell_exec_cmd = $ffmpeg_fullpath . " $ffmpeg_global_options -y -loglevel error -i %%FILE%% " . $ffmpeg_preview_options . " -t %%SECONDS%% -s %%WIDTH%%x%%HEIGHT%% %%TARGETFILE%%";
$shell_exec_params = [
"%%FILE%%" => new CommandPlaceholderArg($file, 'is_valid_rs_path'),
"%%SECONDS%%" => (int) $ffmpeg_preview_seconds,
"%%WIDTH%%" => (int) $width,
"%%HEIGHT%%" => (int) $height,
"%%TARGETFILE%%" => new CommandPlaceholderArg($targetfile, 'is_safe_basename'),
];
if (isset($ffmpeg_command_prefix)) {
$shell_exec_cmd = $ffmpeg_command_prefix . " " . $shell_exec_cmd;
}
$tmp = hook("ffmpegmodpreparams", "", [$shell_exec_cmd, $ffmpeg_fullpath, $file, $shell_exec_params]);
if ($tmp) {
$shell_exec_cmd = $tmp;
}
run_command($shell_exec_cmd, false, $shell_exec_params);
if (
$ffmpeg_get_par
&& (
isset($snapshotcheck)
&& !$snapshotcheck
)
&& $par > 0
&& $par <> 1
) {
# recreate snapshot with correct PAR
$width = $sourcewidth;
$height = $sourceheight;
if ($par < 1) {
$width = ceil($sourcewidth * $par);
} elseif ($par > 1) {
$height = ceil($sourceheight / $par);
}
# Frame size must be a multiple of two
if ($width % 2) {
$width++;
}
if ($height % 2) {
$height++;
}
$shell_exec_cmd = "$ffmpeg_fullpath $ffmpeg_global_options -y -loglevel error -i %%FILE%% -s %%WIDTH%%x%%HEIGHT%% %%TARGETFILE%% -f image2 -vframes 1 -ss %%SNAPSHOTTIME%% %%TARGETFILE%%";
$shell_exec_params = [
"%%FILE%%" => new CommandPlaceholderArg($file, 'is_safe_basename'),
"%%WIDTH%%" => (int) $width,
"%%HEIGHT%%" => (int) $height,
"%%SNAPSHOTTIME%%" => (int) $snapshottime,
"%%TARGETFILE%%" => new CommandPlaceholderArg($targetfile, 'is_safe_basename'),
];
run_command($shell_exec_cmd, false, $shell_exec_params);
}
if (!file_exists($targetfile)) {
debug("FFmpeg failed: " . $shell_exec_cmd);
}
// Handle alternative files.
global $ffmpeg_alternatives;
if (isset($ffmpeg_alternatives) && $generateall) {
$ffmpeg_alt_previews = array();
for ($n = 0; $n < count($ffmpeg_alternatives); $n++) {
$generate = true;
if (
isset($ffmpeg_alternatives[$n]["lines_min"])
// If this alternative size is larger than the source, do not generate.
&& $ffmpeg_alternatives[$n]["lines_min"] >= $sourceheight
) {
$generate = false;
}
if (PHP_SAPI !== "cli") {
set_processing_message(str_replace(["[resource]","[name]"], [$ref,$ffmpeg_alternatives[$n]["name"]], $lang["processing_alternative_video"]));
}
$tmp = hook("preventgeneratealt", "", [$file]);
if ($tmp === true) {
$generate = false;
}
if ($generate) {
// Remove any existing alternative file(s) with this name.
$existing = ps_query("select ref from resource_alt_files where resource = ? and name = ?", array("i", $ref, "s", $ffmpeg_alternatives[$n]["name"]));
for ($m = 0; $m < count($existing); $m++) {
delete_alternative_file($ref, $existing[$m]["ref"]);
}
$alt_type = '';
if (isset($ffmpeg_alternatives[$n]['alt_type'])) {
$alt_type = $ffmpeg_alternatives[$n]["alt_type"];
}
# Create the alternative file.
$aref = add_alternative_file($ref, $ffmpeg_alternatives[$n]["name"], '', '', '', 0, $alt_type);
$apath = get_resource_path($ref, true, "", true, $ffmpeg_alternatives[$n]["extension"], -1, 1, false, "", $aref);
# Process the video
$shell_exec_cmd = "$ffmpeg_fullpath $ffmpeg_global_options -y -loglevel error -i %%FILE%% " . $ffmpeg_alternatives[$n]["params"] . " %%TARGETFILE%%";
$shell_exec_params = [
"%%FILE%%" => new CommandPlaceholderArg($file, 'is_safe_basename'),
"%%WIDTH%%" => (int) $width,
"%%HEIGHT%%" => (int) $height,
"%%SNAPSHOTTIME%%" => (int) $snapshottime,
"%%TARGETFILE%%" => new CommandPlaceholderArg($apath, 'is_safe_basename'),
];
$tmp = hook("ffmpegmodaltparams", "", array($shell_exec_cmd, $ffmpeg_fullpath, $file, $n, $aref, $shell_exec_params));
if ($tmp) {
$shell_exec_cmd = $tmp;
}
run_command($shell_exec_cmd, false, $shell_exec_params);
if (file_exists($apath)) {
# Update the database with the new file details.
$file_size = filesize_unlimited($apath);
ps_query(
"UPDATE resource_alt_files
SET file_name = ?,
file_extension = ?,
file_size = ?,
creation_date = NOW()
WHERE ref = ?",
[
"s", $ffmpeg_alternatives[$n]["filename"] . "." . $ffmpeg_alternatives[$n]["extension"],
"s", $ffmpeg_alternatives[$n]["extension"],
"i", $file_size, "i", $aref
]
);
// add this filename to be added to resource.ffmpeg_alt_previews
if (isset($ffmpeg_alternatives[$n]['alt_preview']) && $ffmpeg_alternatives[$n]['alt_preview']) {
$ffmpeg_alt_previews[] = basename($apath);
}
} else {
# Remove the alternative file entries with this name as ffmpeg has failed to create file.
$existing = ps_query("SELECT ref FROM resource_alt_files WHERE resource = ? AND name = ?", array("i", $ref, "s", $ffmpeg_alternatives[$n]["name"]));
for ($m = 0; $m < count($existing); $m++) {
delete_alternative_file($ref, $existing[$m]["ref"]);
}
}
}
}
}
}

763
include/file_functions.php Normal file
View File

@@ -0,0 +1,763 @@
<?php
/**
* Ensures the filename cannot leave the directory set.
* Only to be used for internal ResourceSpace paths as only a limited character set is supported
*
* @param string $name
* @return string
*/
function safe_file_name($name)
{
// Returns a file name stripped of all non alphanumeric values
// Spaces are replaced with underscores
$alphanum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
$name = str_replace(' ', '_', $name);
$newname = '';
for ($n = 0; $n < strlen($name); $n++) {
$c = substr($name, $n, 1);
if (strpos($alphanum, $c) !== false) {
$newname .= $c;
}
}
// Set to 250 to allow for total length to be below 255 limit including filename and extension
$newname = mb_substr($newname, 0, 250);
return $newname;
}
/**
* Generate a UID for filnames that can be different from user to user (e.g. contact sheets)
*
* @param integer $user_id
*
* @return string
*/
function generateUserFilenameUID($user_id)
{
if (!is_numeric($user_id) || 0 >= $user_id) {
trigger_error('Bad parameter for generateUserFilenameUID()!');
}
global $rs_session, $scramble_key;
$filename_uid = '';
if (isset($rs_session)) {
$filename_uid .= $rs_session;
}
$filename_uid .= $user_id;
return substr(hash('sha256', $filename_uid . $scramble_key), 0, 15);
}
/**
* Checks if a path is part of a whitelisted list of paths. This applies to both folders and files.
*
* Note: the function is not supposed to check/ validate the syntax of the path (ie. UNIX/ Windows)
*
* @param string $path Path which is going to be checked against whitelisted paths
* @param array $whitelisted_paths List of whitelisted paths
*
* @return boolean
*/
function isPathWhitelisted($path, array $whitelisted_paths)
{
foreach ($whitelisted_paths as $whitelisted_path) {
if (substr_compare($whitelisted_path, $path, 0, strlen($path)) === 0) {
return true;
}
}
return false;
}
/**
* Return a checksum for the given file path.
*
* @param string $path Path to file
* @param bool $forcefull Force use of whole file and ignore $file_checksums_50k setting
*
* @return string|false Return the checksum value, false otherwise.
*/
function get_checksum($path, $forcefull = false)
{
debug("get_checksum( \$path = {$path} );");
global $file_checksums_50k;
if (!is_readable($path)) {
return false;
}
# Generate the ID
if ($file_checksums_50k && !$forcefull) {
# Fetch the string used to generate the unique ID
$use = filesize_unlimited($path) . "_" . file_get_contents($path, false, null, 0, 50000);
$checksum = md5($use);
} else {
$checksum = md5_file($path);
}
return $checksum;
}
/**
* Download remote file to the temp filestore location.
*
* @param string $url Source URL
* @param string $key Optional key to use - to prevent conflicts when simultaneous calls use same file name
*
* @return string|bool Returns the new temp filestore location or false otherwise.
*/
function temp_local_download_remote_file(string $url, string $key = "")
{
$userref = $GLOBALS['userref'] ?? 0;
if ($userref === 0) {
return false;
}
if ($key != "" && preg_match('/\W+/', $key) !== 0) {
// Block potential path traversal - allow only word characters.
return false;
}
$url = trim($url);
$url_original = $url;
// Remove query string from URL
$url = explode('?', $url);
$url = reset($url);
$path_parts = pathinfo(basename($url));
$filename = safe_file_name($path_parts['filename'] ?? '');
$extension = $path_parts['extension'] ?? '';
$filename .= ($extension !== '' ? ".{$extension}" : '');
// When the filename isn't valid, try and get from the HTTP header
$check_in_header = strpos($filename, ".") === false && filter_var($url_original, FILTER_VALIDATE_URL);
foreach ($GLOBALS['valid_upload_remote_sources'] as $valid_upload_remote_src) {
// Support dynamic remote URL that may otherwise be mistaken with a file (e.g. pages/download.php)
if (url_starts_with($valid_upload_remote_src, $url_original)) {
$check_in_header = true;
break;
}
}
if ($check_in_header) {
$urlinfo = parse_url($url);
if (!isset($urlinfo["scheme"]) || !in_array($urlinfo["scheme"], ["http","https"])) {
return false;
}
$headers = get_headers($url_original, true);
foreach ($headers as $header => $headervalue) {
if (
strtolower($header) == "content-disposition"
// Check for double quotes first (e.g. attachment; filename="O'Malley's Bar.pdf")
// OR Check for single quotes (e.g. attachment; filename='Space Travel.jpg')
// OR Get file name up to first space
&&
(
preg_match('/.*filename=[\"]([^\"]+)/', $headervalue, $matches)
|| preg_match('/.*filename=[\']([^\']+)/', $headervalue, $matches)
|| preg_match("/.*filename=([^ ]+)/", $headervalue, $matches)
)
) {
$filename = $matches[1];
}
}
$extension = pathinfo(basename($filename), PATHINFO_EXTENSION);
$filename = safe_file_name(pathinfo(basename($filename), PATHINFO_FILENAME)) . ".{$extension}";
}
if (is_banned_extension($extension)) {
debug('[temp_local_download_remote_file] WARN: Banned extension for ' . $filename);
return false;
}
// Get temp location
$tmp_uniq_path_id = $userref . "_" . $key . generateUserFilenameUID($userref);
$tmp_dir = get_temp_dir(false) . "/remote_files/" . $tmp_uniq_path_id;
if (!is_dir($tmp_dir)) {
mkdir($tmp_dir, 0777, true);
}
$tmp_file_path = $tmp_dir . "/" . $filename;
if ($tmp_file_path == $url) {
// Already downloaded earlier by API call
return $tmp_file_path;
}
// Download the file
$GLOBALS['use_error_exception'] = true;
try {
if (copy($url_original, $tmp_file_path)) {
return $tmp_file_path;
}
} catch (Throwable $t) {
debug(sprintf(
'Failed to download remote file from "%s" to temp location "%s". Reason: %s',
$url_original,
$tmp_file_path,
$t->getMessage()
));
}
unset($GLOBALS['use_error_exception']);
return false;
}
/**
* Basic check of uploaded file against list of allowed extensions
*
* @param array{name: string} $uploadedfile An element from the $_FILES PHP reserved variable
* @param array $validextensions Array of valid extension strings
*/
function check_valid_file_extension(array $uploadedfile, array $validextensions): bool
{
$extension = parse_filename_extension($uploadedfile['name']);
return in_array(strtolower($extension), array_map("strtolower", $validextensions)) && !is_banned_extension($extension);
}
/**
* Is the given extension in the list of blocked extensions?
* Also ensures extension is no longer than 10 characters due to resource.file_extension database column limit
*
* @param string $extension - file extension to check
*/
function is_banned_extension(string $extension): bool
{
return !(
preg_match('/^[a-zA-Z0-9_-]{1,10}$/', $extension) === 1
&& !in_array(mb_strtolower($extension), array_map('mb_strtolower', $GLOBALS['banned_extensions']))
);
}
/**
* Remove empty folder from path to file. Helpful to remove a temp directory once the file it was created to hold no longer exists.
* This function should be called only once the directory to be removed is empty.
*
* @param string $path_to_file Full path to file in filestore.
*
* @return void
*/
function remove_empty_temp_directory(string $path_to_file = "")
{
if ($path_to_file != "" && !file_exists($path_to_file)) {
$tmp_path_parts = pathinfo($path_to_file);
$path_to_folder = str_replace(DIRECTORY_SEPARATOR . $tmp_path_parts['basename'], '', $path_to_file);
rmdir($path_to_folder);
}
}
/**
* Confirm upload path is one of valid paths.
*
* @param string $file_path Upload path.
* @param array $valid_upload_paths Array of valid upload paths to test against.
*
* @return bool true when path is valid else false
*/
function is_valid_upload_path(string $file_path, array $valid_upload_paths): bool
{
$orig_use_error_exception_val = $GLOBALS['use_error_exception'] ?? false;
$GLOBALS["use_error_exception"] = true;
try {
$file_path = realpath($file_path);
} catch (Exception $e) {
debug("Invalid file path specified" . $e->getMessage());
return false;
}
$GLOBALS['use_error_exception'] = $orig_use_error_exception_val;
foreach ($valid_upload_paths as $valid_upload_path) {
if (is_dir($valid_upload_path)) {
$checkpath = realpath($valid_upload_path);
if (strpos($file_path, $checkpath) === 0) {
return true;
}
}
}
return false;
}
/**
* Validate the files on disk that are associated with the given resources
*
* @param array $resources Array of resource IDs or
* array of resource data e.g, from search results
* @param array $criteria Array with an array of callables for each resource with the
* required return values in order to pass the check e.g.
* 'file_exists" =>true for a file presence only check
*
* @return array $results An array with resource ID as the index and the results of the check as the value (boolean)
* e.g. ["1234" => true, "1235" => false]
*/
function validate_resource_files(array $resources, array $criteria = []): array
{
$checkresources = isset($resources[0]["ref"]) ? $resources : get_resource_data_batch($resources);
$results = [];
foreach ($checkresources as $resource) {
if (!is_int_loose($resource["ref"])) {
$results[$resource["ref"]] = false;
continue;
}
$filepath = get_resource_path($resource["ref"], true, '', false, $resource["file_extension"] ?? "jpg");
$results[$resource["ref"]] = false;
foreach ($criteria as $criterion => $expected) {
if (!is_callable($criterion)) {
$results[$resource["ref"]] = false;
// Not a valid check
continue 2;
}
$cscheck = $expected === "%RESOURCE%file_checksum";
if (substr($expected, 0, 10) == "%RESOURCE%") {
// $expected is a resource table column
$expected = $resource[substr($expected, 10)];
}
$testresult = call_user_func($criterion, $filepath);
if ($cscheck && ($expected === null || $expected === "")) {
// No checksum is currently recorded. Update it now that it's been calculated
$results[$resource["ref"]] = true;
debug("Skipping checksum check for resource " . $resource["ref"] . " - no existing checksum");
ps_query("UPDATE resource SET file_checksum = ? WHERE ref = ?", ['s', $testresult, 'i', $resource["ref"]]);
continue;
}
$results[$resource["ref"]] = $testresult === $expected;
if ($results[$resource["ref"]] === false) {
debug($resource["ref"] . " failed integrity check. Expected: " . $criterion . "=" . $expected . ", got : " . $testresult);
// No need to continue with other $criteria as check has failed
continue 2;
}
}
}
return $results;
}
/**
* Check if a given file path is from a valid RS accessible location
*
* @param string $path
* @param array $override_paths Override checking of the default RS paths to check a specific location only.
*/
function is_valid_rs_path(string $path, array $override_paths = []): bool
{
debug_function_call(__FUNCTION__, func_get_args());
if ($GLOBALS["config_windows"]) {
$path = str_replace("\\", "/", $path);
}
$sourcerealpath = realpath($path);
$source_path_not_real = !$sourcerealpath || !file_exists($sourcerealpath);
debug('source_path_not_real = ' . json_encode($source_path_not_real));
$checkname = $path;
$pathinfo = pathinfo($path);
if (($pathinfo["extension"] ?? "") === "icc") {
// ResourceSpace generated .icc files have a double extension, need to strip extension again before checking
$checkname = $pathinfo["filename"] ?? "";
}
debug("checkname = {$checkname}");
if (
$source_path_not_real
&& !(preg_match('/^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9_\-[:space:]\/:.]+$/', ($pathinfo['dirname'] ?? ""))
&& is_safe_basename($checkname))
) {
debug('Invalid non-existing path');
return false;
}
// Check if path contains symlinks, if so don't use the value returned by realpath() as it is unlikely to match the expected paths
$symlink = false;
$path_parts = array_filter(explode("/", $path));
if ($GLOBALS["config_windows"]) {
$checkpath = "";
} else {
$checkpath = "/";
}
foreach ($path_parts as $path_part) {
$checkpath .= $path_part;
if (!file_exists($checkpath)) {
break;
}
if (check_symlink($checkpath)) {
debug("{$checkpath} is a symlink");
$symlink = true;
break;
}
$checkpath .= "/";
}
$path_to_validate = ($source_path_not_real || $symlink) ? $path : $sourcerealpath;
debug("path_to_validate = {$path_to_validate}");
if (count($override_paths) > 0) {
$default_paths = $override_paths;
} else {
$default_paths = [
dirname(__DIR__) . '/gfx',
$GLOBALS['storagedir'],
$GLOBALS['syncdir'],
$GLOBALS['fstemplate_alt_storagedir'],
];
if (isset($GLOBALS['tempdir'])) {
$default_paths[] = $GLOBALS['tempdir'];
}
}
$allowed_paths = array_filter(array_map('trim', array_unique($default_paths)));
debug('allowed_paths = ' . implode(', ', $allowed_paths));
foreach ($allowed_paths as $allowed_path) {
debug("Iter allowed path - {$allowed_path}");
$validpath = ($source_path_not_real || $symlink) ? $allowed_path : realpath($allowed_path);
if ($GLOBALS["config_windows"]) {
$allowed_path = str_replace("\\", "/", $allowed_path);
$validpath = str_replace("\\", "/", $validpath);
$path_to_validate = str_replace("\\", "/", $path_to_validate);
}
debug("validpath = {$validpath}");
debug("path_to_validate = {$path_to_validate}");
if ($validpath !== false && mb_strpos($path_to_validate, $validpath) === 0) {
debug('Path allowed');
return true;
}
}
debug('Default as an invalid path');
return false;
}
/**
* Validation helper function to determine if a path base name is unsafe (e.g OS command injection).
* Very strict, limited to specific characters only. Should only be used for filenames originating in ResourceSpace.
*/
function is_safe_basename(string $val): bool
{
$file_name = pathinfo($val, PATHINFO_FILENAME);
return
safe_file_name($file_name) === str_replace(' ', '_', $file_name)
&& !is_banned_extension(parse_filename_extension($val));
}
// phpcs:disable
enum ProcessFileUploadErrorCondition
{
case MissingSourceFile;
case EmptySourceFile;
case InvalidUploadPath;
case SpecialFile;
case InvalidExtension;
case MimeTypeMismatch;
case FileMoveFailure;
/**
* Translate error condition to users' language.
* @param array $lang Language string mapping (i.e. the global $lang var)
* @return string The translated version or the conditions' name if not found in the map
*/
public function i18n(array $lang): string
{
return $lang["error_file_upload_cond-{$this->name}"] ?? $this->name;
}
}
// phpcs:enable
/**
* High level function which can handle processing file uploads.
*
* @param SplFileInfo|array{name: string, full_path: string, type: string, tmp_name: string, error: int, size: int} $source
* @param array{
* allow_extensions?: list<string>,
* file_move?: 'move_uploaded_file'|'rename'|'copy',
* mime_file_based_detection?: bool,
* } $processor Processors which can override different parts of the main logic (e.g. allow specific extensions)
*
* @return array{success: bool, error?: ProcessFileUploadErrorCondition}
*/
function process_file_upload(SplFileInfo|array $source, SplFileInfo $destination, array $processor): array
{
if ($source instanceof SplFileInfo) {
$source_file_name = $source->getFilename();
$source_file_path = $source->getRealPath();
$source_is_file = $source->isFile();
$source_file_size = $source_is_file ? $source->getSize() : 0;
$file_move_processor = $processor['file_move'] ?? 'rename';
} else {
$source_file_name = $source['name'];
$source_file_path = $source['tmp_name'];
$source_is_file = file_exists($source_file_path);
$source_file_size = $source_is_file ? filesize($source_file_path) : 0;
$file_move_processor = 'move_uploaded_file';
if (!is_uploaded_file($source['tmp_name'])) {
trigger_error('Invalid $source input. For files not uploaded via HTTP POST, please use SplFileInfo');
exit();
}
}
$fail_due_to = static fn(ProcessFileUploadErrorCondition $cond): array => ['success' => false, 'error' => $cond];
// Source validation
if (!$source_is_file) {
debug("Missing source file - {$source_file_path}");
return $fail_due_to(ProcessFileUploadErrorCondition::MissingSourceFile);
} elseif ($source_file_size === 0) {
return $fail_due_to(ProcessFileUploadErrorCondition::EmptySourceFile);
} elseif (
!is_valid_upload_path(
$source_file_path,
[...$GLOBALS['valid_upload_paths'], ini_get('upload_tmp_dir'), sys_get_temp_dir()]
)
) {
debug("[WARN] Invalid upload path detected - {$source_file_path}");
return $fail_due_to(ProcessFileUploadErrorCondition::InvalidUploadPath);
}
// Check for "special" files
if (
array_intersect(
[
'crossdomain.xml',
'clientaccesspolicy.xml',
'.htaccess',
'.htpasswd',
],
[$source_file_name]
) !== []
) {
return $fail_due_to(ProcessFileUploadErrorCondition::SpecialFile);
}
// Extension validation
$source_file_ext = parse_filename_extension($source_file_name);
if (
(
isset($processor['allow_extensions'])
&& $processor['allow_extensions'] !== []
&& !check_valid_file_extension(['name' => $source_file_name], $processor['allow_extensions'])
)
|| is_banned_extension($source_file_ext)
) {
return $fail_due_to(ProcessFileUploadErrorCondition::InvalidExtension);
}
// Check content (MIME) type based on the file received (don't trust the header from the upload)
$mime_file_based_detection = $processor['mime_file_based_detection'] ?? true;
$mime_type_by_ext = get_mime_types_by_extension($source_file_ext);
if ($mime_type_by_ext === []) {
log_activity(
"Unknown MIME type for file extension '{$source_file_ext}'",
LOG_CODE_SYSTEM,
get_mime_type($source_file_path, $source_file_ext, true)[0]
);
/* todo: Drop this overriding once we have a better MIME type database (e.g. in 3 releases from now based on the
activity log entries). This was temporarily added for v10.6 to avoid multiple failed uploads due to this new
check. */
$mime_file_based_detection = false;
}
if (
$mime_file_based_detection
&& array_intersect($mime_type_by_ext, get_mime_type($source_file_path, $source_file_ext, true)) === []
) {
debug("MIME type mismatch for file '{$source_file_name}'");
return $fail_due_to(ProcessFileUploadErrorCondition::MimeTypeMismatch);
}
// Destination processing
$dest_path = $destination->isFile() ? $destination->getRealPath() : $destination->getPathname();
if ($destination->isDir()) {
trigger_error('Destination path must be for a file, not a directory!');
exit();
} elseif (!(is_valid_rs_path($dest_path) && is_safe_basename($destination->getBasename()))) {
debug("Destination path '{$dest_path}' not allowed!");
trigger_error('Destination path not allowed');
exit();
} elseif (array_intersect(['move_uploaded_file', 'rename', 'copy'], [$file_move_processor]) === []) {
trigger_error('Invalid upload (file move) processor');
exit();
} elseif ($file_move_processor($source_file_path, $dest_path)) {
return ['success' => true];
} else {
debug("Unable to move file uploaded FROM '{$source_file_path}' TO '$dest_path'");
return $fail_due_to(ProcessFileUploadErrorCondition::FileMoveFailure);
}
}
/**
* Parse file name (can include path, although it's unnecessary) to prevent known security bypasses associated with
* extensions, such as:
* - Double extensions, e.g. .jpg.php
* - Null bytes, e.g. .php%00.jpg, where .jpg gets truncated and .php becomes the new extension
* - Using Windows (DOS) 8.3 short path feature where it's possible to replace existing files by using their shortname
* (e.g. ".htaccess" can be replaced by "HTACCE~1")
*/
function parse_filename_extension(string $filename): string
{
$orig_use_error_exception_val = $GLOBALS['use_error_exception'] ?? false;
$GLOBALS["use_error_exception"] = true;
try {
$finfo = new SplFileInfo($filename);
} catch (Throwable $t) {
debug("[WARN] Bad file '{$filename}'. Reason: {$t->getMessage()}");
return '';
}
$GLOBALS['use_error_exception'] = $orig_use_error_exception_val;
/*
Windows (DOS) 8.3 short paths (e.g. "HTACCE~1" = ".htaccess"). Example file info in such scenario:
Filename is: HTACCE~1
Path is: (note, depends if input is a file name with path)
Path name is: HTACCE~1
Real path is: C:\path\to\.htaccess
*/
if (preg_match('/^[A-Z0-9]{1,6}~([A-Z0-9]?)(\.[A-Z0-9_]{1,3})?$/i', $finfo->getFilename()) === 1) {
if ($finfo->getRealPath() !== false && basename($finfo->getRealPath()) !== $finfo->getFilename()) {
return parse_filename_extension($finfo->getRealPath());
} else {
// Invalid if not a real file to avoid potential exploits
debug("[WARN] Windows (DOS) 8.3 short path for non-existent file '{$filename}' - considered invalid");
return '';
}
}
// Invalidate if it's a hidden file without an extension (e.g .htaccess or .htpasswd) which would be incorrectly
// picked up as an extension
if (trim($finfo->getBasename($finfo->getExtension())) === '.') {
debug("Hidden file '{$filename}' without an extension - considered invalid");
return '';
}
return $finfo->getExtension();
}
/**
* Delete old files and folders from tempo directory based on the configured $purge_temp_folder_age value
* Affects filestore/tmp, $storagedir/tmp or the configured $tempdir directory
*/
function delete_temp_files(): void
{
if ($GLOBALS["purge_temp_folder_age"] === 0) {
// Disabled
return;
}
// Set up array of folders to scan
$folderstoscan = [];
$folderstoscan[] = get_temp_dir();
$modified_folderstoscan = hook("add_folders_to_delete_from_temp", "", array($folderstoscan));
if (is_array($modified_folderstoscan) && !empty($modified_folderstoscan)) {
$folderstoscan = $modified_folderstoscan;
}
// Set up array of folders to exclude
$excludepaths = [];
if (isset($GLOBALS["geo_tile_cache_directory"])) {
$excludepaths[] = $GLOBALS["geo_tile_cache_directory"];
} else {
$excludepaths[] = get_temp_dir() . "tiles";
}
if (DOWNLOAD_FILE_LIFETIME > $GLOBALS["purge_temp_folder_age"]) {
$excludepaths[] = get_temp_dir(false, "user_downloads");
}
// Set up arrays to hold items to delete
$folderstodelete = [];
$filestodelete = [];
foreach ($folderstoscan as $foldertoscan) {
if (!file_exists($foldertoscan)) {
continue;
}
$foldercontents = new DirectoryIterator($foldertoscan);
foreach ($foldercontents as $object) {
if (time() - $object->getMTime() > $GLOBALS["purge_temp_folder_age"] * 24 * 60 * 60) {
$tmpfilename = $object->getFilename();
if ($object->isDot()) {
continue;
}
foreach ($excludepaths as $excludepath) {
if (
($tmpfilename == $excludepath)
|| strpos($object->getRealPath(), $excludepath) == 0
) {
continue 2;
}
}
if ($object->isDir()) {
$folderstodelete[] = $foldertoscan . DIRECTORY_SEPARATOR . $tmpfilename;
} elseif ($object->isFile()) {
$filestodelete[] = $foldertoscan . DIRECTORY_SEPARATOR . $tmpfilename;
}
}
}
}
foreach ($folderstodelete as $foldertodelete) {
// Extra check that folder is in an expected path
if (strpos($foldertodelete, $GLOBALS["storagedir"]) === false
&& strpos($foldertodelete, $GLOBALS["tempdir"]) === false
&& strpos($foldertodelete, 'filestore/tmp') === false
) {
continue;
}
$success = rcRmdir($foldertodelete);
if ('cli' == PHP_SAPI) {
echo " - deleting directory " . $foldertodelete . " - " . ($success ? "SUCCESS" : "FAILED") . PHP_EOL;
}
debug(" - deleting directory " . $foldertodelete . " - " . ($success ? "SUCCESS" : "FAILED"));
}
foreach ($filestodelete as $filetodelete) {
// Extra check that file is in an expected path
if (strpos($filetodelete, $GLOBALS["storagedir"]) === false
&& strpos($filetodelete, $GLOBALS["tempdir"]) === false
&& strpos($filetodelete, 'filestore/tmp') === false
) {
continue;
}
$success = try_unlink($filetodelete);
if ('cli' == PHP_SAPI) {
echo " - deleting file " . $filetodelete . " - " . ($success ? "SUCCESS" : "FAILED") . PHP_EOL;
}
debug(" - deleting file " . $filetodelete . " - " . ($success ? "SUCCESS" : "FAILED"));
}
}
/**
* Are the arguments set in $archiver_settings["arguments"] permitted?
* Allows word characters, '@', and '-' only
*
* @param string Argument string
*
*/
function permitted_archiver_arguments($string): bool
{
return preg_match('/[^\@\-\w]/', $string) === 0;
}
/**
* Check if a given path is absolute or contains a symlink or junction
* is_link() does not accurately detect junction links on Windows systems
* instead we check if the output from stat() and lstat() differ.
*
* @param string $checkpath
* @return bool
*/
function check_symlink(string $checkpath): bool
{
if ($GLOBALS["config_windows"]) {
return stat($checkpath) != lstat($checkpath);
} else {
return is_link($checkpath);
}
}

648
include/footer.php Executable file
View File

@@ -0,0 +1,648 @@
<?php
hook("before_footer_always");
if (getval("loginmodal", "")) {
$login_url = $baseurl . "/login.php?url=" . urlencode(getval("url", "")) . "&api=" . urlencode(getval("api", "")) . "&error=" . urlencode(getval("error", "")) . "&auto=" . urlencode(getval("auto", "")) . "&nocookies=" . urlencode(getval("nocookies", "")) . "&logout=" . urlencode(getval("logout", true));
?><script>
jQuery(document).ready(function(){
ModalLoad('<?php echo $login_url?>',true);
});
</script>
<?php
}
# Complete rendering of footer controlled elements and closure divs on full page load (ie. ajax is "")
# This rendering is bypassed when dynamically loading content into CentralSpace (ajax is "true")
if (getval("ajax", "") == "" && !hook("replace_footer")) {
hook("beforefooter");
if (!in_array($pagename, ["login","user_password"])) {
?>
</div><!--End CentralSpaceFC-->
</div><!--End CentralSpaceContainerFC-->
<?php
}
?>
<!-- Footer closures -->
<div class="clearer"></div>
<!-- Use aria-live assertive for high priority changes in the content: -->
<span role="status" aria-live="assertive" class="ui-helper-hidden-accessible"></span>
<div class="clearerleft"></div>
<div class="clearer"></div>
<?php hook("footertop");
if ($pagename == "login") {
?>
<!--Global Footer-->
<div id="Footer">
<div class="ResponsiveViewFullSite">
<a href="#" onClick="SetCookie('ui_view_full_site', true, 1, true); location.reload();"><?php echo escape($lang['responsive_view_full_site']); ?></a>
</div>
<?php
if (!hook("replace_footernavrightbottom")) {
?>
<div id="FooterNavRightBottom"><?php echo text("footer")?></div>
<?php
}
?>
<div class="clearer"></div>
</div>
<?php
}
echo $extrafooterhtml;
} // end ajax
/* always include the below as they are perpage */
if (($pagename != "login") && ($pagename != "user_password") && ($pagename != "user_request")) {?>
</div><!--End CentralSpacePP-->
</div><!--End CentralSpaceContainerPP-->
</div><!--End UICenterPP -->
<?php
}
hook("footerbottom");
draw_performance_footer();
//titlebar modifications
if ($show_resource_title_in_titlebar) {
$general_title_pages = array("admin_content","team_archive","team_resource","team_user","team_request","team_research","team_plugins","team_mail","team_export","team_stats","team_report","research_request","team_user_edit","admin_content_edit","team_request_edit","team_research_edit","requests","edit","themes","collection_public","collection_manage","team_home","help","home","tag","upload_java_popup","upload_java","contact","geo_search","search_advanced","about","contribute","user_preferences","view_shares","check","index");
$search_title_pages = array("contactsheet_settings","search","collection_edit","edit","collection_download","collection_share","collection_request");
$resource_title_pages = array("view","delete","log","alternative_file","alternative_files","resource_email","edit","preview");
$additional_title_pages = array(hook("additional_title_pages_array"));
$title = "";
// clear resource or search title for pages that don't apply:
if (!in_array($pagename, array_merge($general_title_pages, $search_title_pages, $resource_title_pages)) && (empty($additional_title_pages) || !in_array($pagename, $additional_title_pages))) {
echo "<script language='javascript'>\n";
echo "document.title = \"$applicationname\";\n";
echo "</script>";
}
// place resource titles
elseif (in_array($pagename, $resource_title_pages) && !isset($_GET['collection']) && !isset($_GET['java'])) { /* for edit page */
if (isset($ref)) {
$title = str_replace('"', "''", i18n_get_translated(get_data_by_field($ref, $view_title_field)));
}
echo "<script type=\"text/javascript\" language='javascript'>\n";
if ($pagename == "edit") {
$title = $lang['action-edit'] . " - " . $title;
}
echo "document.title = \"$applicationname - $title\";\n";
echo "</script>";
}
// place collection titles
elseif (in_array($pagename, $search_title_pages)) {
$collection = getval("ref", "");
if (isset($search_title)) {
$title = str_replace('"', "''", $lang["searchresults"] . " - " . html_entity_decode(strip_tags($search_title)));
} else {
$collectiondata = get_collection($collection);
$title = strip_tags(str_replace('"', "''", i18n_get_collection_name($collectiondata)));
}
// add a hyphen if title exists
if (strlen($title) != 0) {
$title = "- $title";
}
if ($pagename == "edit") {
$title = " - " . $lang['action-editall'] . " " . $title;
}
if ($pagename == "collection_share") {
$title = " - " . $lang['share'] . " " . $title;
}
if ($pagename == "collection_edit") {
$title = " - " . $lang['action-edit'] . " " . $title;
}
if ($pagename == "collection_download") {
$title = " - " . $lang['download'] . " " . $title;
}
echo "<script language='javascript'>\n";
echo "document.title = \"$applicationname $title\";\n";
echo "</script>";
}
// place page titles
elseif (in_array($pagename, $general_title_pages)) {
if (isset($lang[$pagename])) {
$pagetitle = $lang[$pagename];
} elseif (isset($lang['action-' . $pagename])) {
$pagetitle = $lang["action-" . $pagename];
if (getval("java", "") != "") {
$pagetitle = $lang['upload'] . " " . $pagetitle;
}
} elseif (isset($lang[str_replace("_", "", $pagename)])) {
$pagetitle = $lang[str_replace("_", "", $pagename)];
} elseif ($pagename == "admin_content") {
$pagetitle = $lang['managecontent'];
} elseif ($pagename == "collection_public") {
$pagetitle = $lang["publiccollections"];
} elseif ($pagename == "collection_manage") {
$pagetitle = $lang["mycollections"];
} elseif ($pagename == "team_home") {
$pagetitle = $lang["teamcentre"];
} elseif ($pagename == "help") {
$pagetitle = $lang["helpandadvice"];
} elseif (strpos($pagename, "upload") !== false) {
$pagetitle = $lang["upload"];
} elseif ($pagename == "contact") {
$pagetitle = $lang["contactus"];
} elseif ($pagename == "geo_search") {
$pagetitle = $lang["geographicsearch"];
} elseif ($pagename == "search_advanced") {
$pagetitle = $lang["advancedsearch"];
if (getval("archive", "") == 2) {
$pagetitle .= " - " . $lang['archiveonlysearch'];
}
} elseif ($pagename == "about") {
$pagetitle = $lang["aboutus"];
} elseif ($pagename == "contribute") {
$pagetitle = $lang["mycontributions"];
} elseif ($pagename == "user_preferences") {
$pagetitle = $lang["user-preferences"];
} elseif ($pagename == "requests") {
$pagetitle = $lang["myrequests"];
} elseif ($pagename == "team_resource") {
$pagetitle = $lang["manageresources"];
} elseif ($pagename == "team_archive") {
$pagetitle = $lang["managearchiveresources"];
} elseif ($pagename == "view_shares") {
$pagetitle = $lang["shared_collections"];
} elseif ($pagename == "team_user") {
$pagetitle = $lang["manageusers"];
} elseif ($pagename == "team_request") {
$pagetitle = $lang["managerequestsorders"];
} elseif ($pagename == "team_research") {
$pagetitle = $lang["manageresearchrequests"];
} elseif ($pagename == "team_plugins") {
$pagetitle = $lang["pluginmanager"];
} elseif ($pagename == "team_mail") {
$pagetitle = $lang["sendbulkmail"];
} elseif ($pagename == "team_export") {
$pagetitle = $lang["exportdata"];
} elseif ($pagename == "team_stats") {
$pagetitle = $lang["viewstatistics"];
} elseif ($pagename == "team_report") {
$pagetitle = $lang["viewreports"];
} elseif ($pagename == "check") {
$pagetitle = $lang["installationcheck"];
} elseif ($pagename == "index") {
$pagetitle = $lang["systemsetup"];
} elseif ($pagename == "team_user_edit") {
$pagetitle = $lang["edituser"];
} elseif ($pagename == "admin_content_edit") {
$pagetitle = $lang["editcontent"];
} elseif ($pagename == "team_request_edit") {
$pagetitle = $lang["editrequestorder"];
} elseif ($pagename == "team_research_edit") {
$pagetitle = $lang["editresearchrequest"];
} else {
$pagetitle = "";
}
if (strlen($pagetitle) != 0) {
$pagetitle = "- $pagetitle";
}
echo "<script language='javascript'>\n";
echo "document.title = \"$applicationname $pagetitle\";\n";
echo "</script>";
}
hook("additional_title_pages");
}
if (isset($onload_message["text"])) {?>
<script>
jQuery(document).ready(function()
{
styledalert(<?php echo (isset($onload_message["title"]) ? json_encode($onload_message["title"]) : "''") . "," . json_encode($onload_message["text"]) ;?>);
});
</script>
<?php
}
if (getval("ajax", "") == "") {
// don't show closing tags if we're in ajax mode
echo "<!--CollectionDiv-->";
$omit_collectiondiv_load_pages = array("login","user_request","user_password","index");
$more_omit_collectiondiv_load_pages = hook("more_omit_collectiondiv_load_pages");
if (is_array($more_omit_collectiondiv_load_pages)) {
$omit_collectiondiv_load_pages = array_merge($omit_collectiondiv_load_pages, $more_omit_collectiondiv_load_pages);
}
?></div>
<?php # Work out the current collection (if any) from the search string if external access
if (
isset($k)
&& $k != ""
&& isset($search)
&& !isset($usercollection)
&& substr($search, 0, 11) == "!collection"
) {
// Search may include extra terms after a space so need to make sure we extract only the ID
$searchparts = explode(" ", substr($search, 11));
$usercollection = trim($searchparts[0]);
}
?>
<script>
<?php
if (!isset($usercollection)) {?>
usercollection='';
<?php
} else {?>
usercollection='<?php echo escape($usercollection) ?>';
<?php
} ?>
</script><?php
if (!hook("replacecdivrender")) {
$col_on = !in_array($pagename, $omit_collectiondiv_load_pages) && !checkperm("b") && isset($usercollection);
if ($col_on) {
// Footer requires restypes as a string because it is urlencoding them
if (isset($restypes) && is_array($restypes)) {
$restypes = implode(',', $restypes);
}
?>
<div id="CollectionDiv" class="CollectBack AjaxCollect ui-layout-south"></div>
<script type="text/javascript">
var collection_frame_height=<?php echo COLLECTION_FRAME_HEIGHT ?>;
var thumbs="<?php echo escape($thumbs); ?>";
function ShowThumbs()
{
myLayout.sizePane("south", collection_frame_height);
jQuery('.ui-layout-south').animate({scrollTop:0}, 'fast');
jQuery('#CollectionMinDiv').hide();
jQuery('#CollectionMaxDiv').show();
SetCookie('thumbs',"show",1000);
ModalCentre();
if(typeof chosenCollection !== 'undefined' && chosenCollection)
{
jQuery('#CollectionMaxDiv select').chosen({disable_search_threshold:chosenCollectionThreshold});
}
}
function HideThumbs()
{
myLayout.sizePane("south", 40);
jQuery('.ui-layout-south').animate({scrollTop:0}, 'fast');
jQuery('#CollectionMinDiv').show();
jQuery('#CollectionMaxDiv').hide();
SetCookie('thumbs',"hide",1000);
ModalCentre();
if(typeof chosenCollection !== 'undefined' && chosenCollection)
{
jQuery('#CollectionMinDiv select').chosen({disable_search_threshold:chosenCollectionThreshold});
}
}
function ToggleThumbs()
{
thumbs = getCookie("thumbs");
if (thumbs=="show")
{
HideThumbs();
}
else
{
ShowThumbs();
}
}
function InitThumbs()
{
if(thumbs!="hide")
{
ShowThumbs();
}
else if(thumbs=="hide")
{
HideThumbs();
}
}
jQuery(document).ready(function()
{
CollectionDivLoad('<?php echo generateURL($baseurl_short . 'pages/collections.php', ['thumbs' => $thumbs, 'k' => $k ?? '', 'order_by' => $order_by ?? '', 'sort' => $sort ?? '', 'search' => $search ?? '', 'archive' => $archive ?? '', 'daylimit' => $daylimit ?? '', 'offset' => $offset ?? '', 'resource_count' => $resource_count ?? '']) ?>&collection='+usercollection);
InitThumbs();
});
</script>
<?php
} // end omit_collectiondiv_load_pages
else {
?>
<script>
jQuery(document).ready(function()
{
ModalCentre();
});
</script>
<?php
}
?>
<script type="text/javascript">
var resizeTimer;
myLayout=jQuery('body').layout(
{
livePaneResizing:true,
triggerEventsDuringLiveResize: false,
resizerTip: '<?php echo escape($lang["resize"]); ?>',
east__spacing_open:0,
east__spacing_closed:8,
east_resizable: true,
east__closable: false,
east__size: 295,
north_resizable: false,
north__closable:false,
north__spacing_closed: 0,
north__spacing_open: 0,
<?php
if ($col_on) {?>
south__resizable:true,
south__minSize:40,
south__spacing_open:8,
south__spacing_closed:8,
south__togglerLength_open:"200",
south__togglerTip_open: '<?php echo escape($lang["toggle"]); ?>',
south__onclose_start: function(pane)
{
if (pane=="south" && (typeof colbarresizeon === "undefined" || colbarresizeon==true))
{
if(jQuery('.ui-layout-south').height()>40 && thumbs!="hide")
{
HideThumbs();
}
else if(jQuery('.ui-layout-south').height()<=40 && thumbs=="hide")
{
ShowThumbs();
}
return false;
}
ModalCentre();
},
south__onresize: function(pane)
{
if (pane=="south" && (typeof colbarresizeon === "undefined" || colbarresizeon==true))
{
thumbs = getCookie("thumbs");
if(jQuery('.ui-layout-south').height() < collection_frame_height && thumbs!="hide")
{
HideThumbs();
}
else if(jQuery('.ui-layout-south').height()> 40 && thumbs=="hide")
{
ShowThumbs();
}
}
ModalCentre();
},
<?php
} else {?>
south__initHidden: true,
<?php
}
?>
});
</script>
<?php
}
if (!hook("responsive_footer")) {
?>
<!-- Responsive -->
<script src="<?php echo $baseurl_short; ?>js/responsive.js?css_reload_key=<?php echo $css_reload_key; ?>"></script>
<script>
function toggleSimpleSearch()
{
if(jQuery("#searchspace").hasClass("ResponsiveSimpleSearch"))
{
jQuery("#searchspace").removeClass("ResponsiveSimpleSearch");
jQuery("#SearchBarContainer").removeClass("FullSearch");
jQuery("#Rssearchexpand").val("<?php echo escape($lang["responsive_more"]);?>");
jQuery('#UICenter').show(0);
search_show = false;
}
else
{
jQuery("#searchspace").addClass("ResponsiveSimpleSearch");
jQuery("#SearchBarContainer").addClass("FullSearch");
jQuery("#Rssearchexpand").val(" <?php echo escape($lang["responsive_less"]);?> ");
jQuery('#UICenter').hide(0);
search_show = true;
}
}
/* Responsive Stylesheet inclusion based upon viewing device */
if(document.createStyleSheet)
{
document.createStyleSheet('<?php echo $baseurl ;?>/css/responsive/slim-style.css?rcsskey=<?php echo $css_reload_key; ?>');
}
else
{
jQuery("head").append("<link rel='stylesheet' href='<?php echo $baseurl ;?>/css/responsive/slim-style.css?rcsskey=<?php echo $css_reload_key; ?>' type='text/css' media='screen' />");
}
if(!is_touch_device() && jQuery(window).width() <= 1280)
{
if(document.createStyleSheet)
{
document.createStyleSheet('<?php echo $baseurl; ?>/css/responsive/slim-non-touch.css?rcsskey=<?php echo $css_reload_key; ?>');
}
else
{
jQuery("head").append("<link rel='stylesheet' href='<?php echo $baseurl; ?>/css/responsive/slim-non-touch.css?rcsskey=<?php echo $css_reload_key; ?>' type='text/css' media='screen' />");
}
}
var responsive_show = "<?php echo escape($lang['responsive_collectiontogglehide']);?>";
var responsive_hide;
var responsive_newpage = true;
if(jQuery(window).width() <= 1100)
{
jQuery('.ResponsiveViewFullSite').css('display', 'block');
SetCookie("selected_search_tab", "search");
}
else
{
jQuery('.ResponsiveViewFullSite').css('display', 'none');
}
if(jQuery(window).width()<=700)
{
touchScroll("UICenter");
}
var lastWindowWidth = jQuery(window).width();
jQuery(window).resize(function()
{
// Check if already resizing
if(typeof rsresize !== 'undefined')
{
return;
}
newwidth = jQuery( window ).width();
if(lastWindowWidth > 1100 && newwidth < 1100)
{
// Set flag to prevent recursive loop
rsresize = true;
selectSearchBarTab('search');
rsresize = undefined;
}
else if(lastWindowWidth > 900 && newwidth < 900)
{
rsresize = true;
console.log("hiding collections");
hideMyCollectionsCols();
responsiveCollectionBar();
jQuery('#CollectionDiv').hide(0);
rsresize = undefined;
}
else if(lastWindowWidth < 900 && newwidth > 900)
{
rsresize = true;
showResponsiveCollection();
rsresize = undefined;
}
lastWindowWidth = newwidth;
});
jQuery("#HeaderNav1Click").click(function(event)
{
event.preventDefault();
if(jQuery(this).hasClass("RSelectedButton"))
{
jQuery(this).removeClass("RSelectedButton");
jQuery("#HeaderNav1").slideUp(0);
jQuery("#Header").removeClass("HeaderMenu");
}
else
{
jQuery("#HeaderNav2Click").removeClass("RSelectedButton");
jQuery("#HeaderNav2").slideUp(80);
jQuery("#Header").addClass("HeaderMenu");
jQuery(this).addClass("RSelectedButton");
jQuery("#HeaderNav1").slideDown(80);
}
if(jQuery("#searchspace").hasClass("ResponsiveSimpleSearch"))
{
toggleSimpleSearch();
}
});
jQuery("#HeaderNav2Click").click(function(event)
{
event.preventDefault();
if(jQuery(this).hasClass("RSelectedButton"))
{
jQuery(this).removeClass("RSelectedButton");
jQuery("#HeaderNav2").slideUp(0);
jQuery("#Header").removeClass("HeaderMenu");
}
else
{
jQuery("#Header").addClass("HeaderMenu");
jQuery("#HeaderNav1Click").removeClass("RSelectedButton");
jQuery("#HeaderNav1").slideUp(80);
jQuery(this).addClass("RSelectedButton");
jQuery("#HeaderNav2").slideDown(80);
}
if(jQuery("#searchspace").hasClass("ResponsiveSimpleSearch"))
{
toggleSimpleSearch();
}
});
jQuery("#HeaderNav2").on("click","a",function()
{
if(jQuery(window).width() <= 1200)
{
jQuery("#HeaderNav2").slideUp(0);
jQuery("#HeaderNav2Click").removeClass("RSelectedButton");
}
});
jQuery("#HeaderNav1").on("click","a",function()
{
if(jQuery(window).width() <= 1200)
{
jQuery("#HeaderNav1").slideUp(00);
jQuery("#HeaderNav1Click").removeClass("RSelectedButton");
}
});
jQuery("#SearchBarContainer").on("click","#Rssearchexpand",toggleSimpleSearch);
if(jQuery(window).width() <= 700 && jQuery(".ListviewStyle").length && is_touch_device())
{
jQuery("td:last-child,th:last-child").hide();
}
</script>
<!-- end of Responsive -->
<?php
}
hook('afteruilayout');
?>
<!-- Start of modal support -->
<div id="modal_overlay" onClick="ModalClose();"></div>
<div id="modal_outer">
<div id="modal" tabindex="0">
</div>
</div>
<div id="modal_dialog" style="display:none;"></div>
<script type="text/javascript">
jQuery(window).bind('resize.modal', ModalCentre);
</script>
<!-- End of modal support -->
<script>
try
{
top.history.replaceState(document.title+'&&&'+jQuery('#CentralSpace').html(), applicationname);
}
catch(e){console.log(e);
}
</script>
<script>
/* Destroy tagEditor if below breakpoint window size (doesn't work in responsize mode */
window_width = jQuery(window).width();
window_width_breakpoint = 1100;
simple_search_pills_view = <?php echo $simple_search_pills_view ? "true" : "false"; ?>
/* Page load */
if(window_width <= window_width_breakpoint && simple_search_pills_view == true)
{
jQuery('#ssearchbox').tagEditor('destroy');
}
/* Page resized to below breakpoint */
jQuery(window).resize(function()
{
window_width = jQuery(window).width();
if(window_width <= window_width_breakpoint && simple_search_pills_view == true)
{
jQuery('#ssearchbox').tagEditor('destroy');
}
});
</script>
</body>
</html><?php
} // end if !ajax

5683
include/general_functions.php Executable file

File diff suppressed because it is too large Load Diff

227
include/geocoding_view.php Normal file
View File

@@ -0,0 +1,227 @@
<?php
// Resource View Leaflet Map Using Leaflet.js and Various Leaflet Plugins
// Setup initial Leaflet map variables.
global $lang, $baseurl, $baseurl_short, $view_mapheight, $map_default, $map_zoomslider, $map_zoomnavbar, $map_kml, $map, $map_kml_file, $map_retina, $map_polygon_field, $modal, $fields;
$zoomslider = 'false';
$zoomcontrol = 'true';
$polygon = '';
$modal = (getval("modal", "") == 'true');
// Set Leaflet map search view height and layer control container height based on $mapheight.
if (isset($view_mapheight)) {
$map1_height = $view_mapheight;
$layer_controlheight = $view_mapheight - 40;
} else // Default values.
{
$map1_height = 300;
$layer_controlheight = 250;
}
// Show zoom slider instead of default Leaflet zoom control?
if ($map_zoomslider) {
$zoomslider = 'true';
$zoomcontrol = 'false';
}
if ($hide_geolocation_panel && !isset($geolocation_panel_only)) { ?>
<script>
function ShowGeolocation()
{
if(!jQuery("#GeolocationData").length){
jQuery.ajax({
type:"GET",
url: '<?php echo $baseurl_short?>pages/ajax/geolocation_loader.php?ref=<?php echo urlencode($ref)?>&k=<?php echo urlencode($k)?>',
success: function(data){
jQuery("#GeolocationHideLink").after(data);
}
});
}
jQuery("#GeolocationData").slideDown();
jQuery("#GeolocationHideLink").show();
jQuery("#GeolocationShowLink").hide();
}
function HideGeolocation()
{
jQuery("#GeolocationData").slideUp();
jQuery("#GeolocationShowLink").show();
jQuery("#GeolocationHideLink").hide();
}
</script> <?php
}
// Begin geolocation section.
if (!isset($geolocation_panel_only)) { ?>
<div class="RecordBox">
<div class="RecordPanel"> <?php
if ($hide_geolocation_panel) { ?>
<div id="GeolocationShowLink" class="CollapsibleSection" ><?php echo "<a href=\"javascript: void(0)\" onClick=\"ShowGeolocation();\">&#x25B8;&nbsp;" . escape($lang['showgeolocationpanel']) . "</a>";?></div>
<div id="GeolocationHideLink" class="CollapsibleSection" style="display:none"><?php echo "<a href=\"javascript: void(0)\" onClick=\"HideGeolocation();return false;\">&#x25BE;&nbsp;" . escape($lang['hidegeolocationpanel']) . "</a>";?></div> <?php
}
}
if (!$hide_geolocation_panel || isset($geolocation_panel_only)) { ?>
<div id="GeolocationData">
<div class="Title"><?php echo escape($lang['location-title']); ?></div>
<?php
if ($resource['geo_lat'] != '' && $resource['geo_long'] != '') { ?>
<?php if ($edit_access) { ?>
<p>
<?php echo LINK_CARET ?><a href="<?php echo $baseurl_short?>pages/geo_edit.php?ref=<?php echo urlencode($ref); ?>" onClick="return CentralSpaceLoad(this,true);"><?php echo escape($lang['location-edit']); ?></a>
</p>
<?php
}
$zoom = leaflet_map_zoom($resource['mapzoom']);
// Check for modal view.
if (!$modal) {
$map_container = 'map_id';
$map_container_obj = "map_obj";
} else {
$map_container = 'map_id_modal';
$map_container_obj = "map_modal_obj";
}
?>
<!--Setup Leaflet map container with sizing-->
<div id="<?php echo $map_container; ?>" style="width: 99%; margin-top:0px; margin-bottom:0px; height: <?php echo $map1_height;?>px; display:block; border:1px solid black; float:none; overflow: hidden;">
</div>
<script type="text/javascript">
// Define available Leaflet basemaps groups and layers using leaflet.providers.js, L.TileLayer.PouchDBCached.js, and styledLayerControl.js based on ../include/map_functions.php.
<?php include __DIR__ . '/map_processing.php'; ?>
<!--Determine basemaps and map groups for user selection-->
<?php include __DIR__ . '/map_basemaps.php'; ?>
jQuery(document).ready(function ()
{
// Setup and define the Leaflet map with the initial view using leaflet.js and L.Control.Zoomslider.js
var <?php echo $map_container_obj; ?>_geo_lat = <?php echo $resource['geo_lat']; ?>;
var <?php echo $map_container_obj; ?>_geo_long = <?php echo $resource['geo_long']; ?>;
var <?php echo $map_container_obj; ?>_zoom = <?php echo $zoom; ?>;
if (typeof <?php echo $map_container_obj; ?> !== "undefined")
{
<?php echo $map_container_obj; ?>.remove();
}
var <?php echo $map_container_obj; ?> = new L.map(<?php echo $map_container; ?>, {
preferCanvas: true,
renderer: L.canvas(),
zoomsliderControl: <?php echo $zoomslider?>,
zoomControl: <?php echo $zoomcontrol?>
}).setView([<?php echo $map_container_obj; ?>_geo_lat, <?php echo $map_container_obj; ?>_geo_long], <?php echo $map_container_obj; ?>_zoom);
<?php echo $map_container_obj; ?>.invalidateSize();
// Define default Leaflet basemap layer using leaflet.js, leaflet.providers.js, and L.TileLayer.PouchDBCached.js
var defaultLayer = new L.tileLayer.provider('<?php echo $map_default;?>', {
useCache: '<?php echo $map_default_cache;?>', <!--Use browser caching of tiles (recommended)?-->
detectRetina: '<?php echo $map_retina;?>', <!--Use retina high resolution map tiles?-->
attribution: default_attribute
}).addTo(<?php echo $map_container_obj; ?>);
<?php echo $map_container_obj; ?>.invalidateSize(true);
// Set styled layer control options for basemaps and add to the Leaflet map using styledLayerControl.js
var options = {
container_maxHeight: "<?php echo $layer_controlheight?>px",
group_maxHeight: "180px",
exclusive: false
};
var control = L.Control.styledLayerControl(baseMaps,options);
<?php echo $map_container_obj; ?>.addControl(control);
// Show zoom history navigation bar and add to Leaflet map using Leaflet.NavBar.min.js
<?php if ($map_zoomnavbar && $view_mapheight >= 400) { ?>
L.control.navbar().addTo(<?php echo $map_container_obj; ?>); <?php
} ?>
// Add a scale bar to the Leaflet map using leaflet.min.js
new L.control.scale().addTo(<?php echo $map_container_obj; ?>);
// Add a KML overlay to the Leaflet map using leaflet-omnivore.min.js
<?php if ($map_kml) { ?>
omnivore.kml('<?php echo $baseurl?>/filestore/system/<?php echo $map_kml_file?>').addTo(<?php echo $map_container_obj; ?>); <?php
} ?>
// Limit geocoordinate values to six decimal places for display on marker hover
function georound(num) {
return +(Math.round(num + "e+6") + "e-6");
}
// Add a marker for the resource
L.marker([<?php echo $map_container_obj; ?>_geo_lat, <?php echo $map_container_obj; ?>_geo_long], {
<?php
$maprestype = get_resource_types($resource['resource_type']);
$markercolour = (isset($maprestype[0]) && isset($MARKER_COLORS[$maprestype[0]["colour"]])) ? (int)$maprestype[0]["colour"] : ($resource['resource_type'] % count($MARKER_COLORS));
echo "icon: " . strtolower($MARKER_COLORS[$markercolour]) . "Icon,\n";
?>
title: georound(<?php echo $map_container_obj; ?>_geo_lat) + ", " + georound(<?php echo $map_container_obj; ?>_geo_long) + " (WGS84)"
}).addTo(<?php echo $map_container_obj; ?>);
// Add the resource footprint polygon to the map and pan/zoom to the polygon
<?php if (is_numeric($map_polygon_field)) {
$polygon = leaflet_polygon_parsing($fields, false);
if (!is_null($polygon['values']) && $polygon['values'] != "" && $polygon['values'] != "[]") { ?>
var refPolygon = L.polygon([<?php echo $polygon['values']; ?>]).addTo(<?php echo $map_container_obj; ?>);
<?php echo $map_container_obj; ?>.fitBounds(refPolygon.getBounds(), {
padding: [25, 25]
}); <?php
}
} else // Pan to the marker location.
{ ?>
<?php echo $map_container_obj; ?>.setView([<?php echo $map_container_obj; ?>_geo_lat, <?php echo $map_container_obj; ?>_geo_long], <?php echo $map_container_obj; ?>_zoom); <?php
}
?>
// Fix for Microsoft Edge and Internet Explorer browsers
<?php echo $map_container_obj; ?>.invalidateSize(true);
});
</script>
<!--Show resource geolocation value-->
<div id="resource_coordinate" style="margin-top:0px; margin-bottom:0px; width: 99%;">
<p> <?php echo escape($lang['marker'] . ' ' . strtolower($lang['latlong'])) . ': ';
echo round($resource['geo_lat'], 6) . ', ';
echo round($resource['geo_long'], 6) . ' (WGS84)'; ?> </p>
</div>
<?php
} else { ?>
<a href="<?php echo $baseurl_short?>pages/geo_edit.php?ref=<?php echo urlencode($ref); ?>" onClick="return CentralSpaceLoad(this,true);"><?php echo LINK_PLUS ?><?php echo escape($lang['location-add']);?></a> <?php
}
if ($view_panels) { ?>
<script>
jQuery(document).ready(function ()
{
let parent_element = jQuery('#<?php echo $modal ? 'modal' : 'CentralSpace' ?>');
parent_element.find("#GeolocationData").children(".Title").attr("panel", "GeolocationData").appendTo(parent_element.find("#Titles1"));
removePanel = parent_element.find("#GeolocationData").parent().parent(".RecordBox");
parent_element.find("#GeolocationData").appendTo(parent_element.find("#Panel1")).addClass("TabPanel").hide();
removePanel.remove();
//Function to switch tab panels
jQuery('.ViewPanelTitles').children('.Title').click(function()
{
parent_element.find(this).parent().parent().children('.TabPanel').hide();
parent_element.find(this).parent().children('.Title').removeClass('Selected');
parent_element.find(this).addClass('Selected');
parent_element.find('#' + jQuery(this).attr('panel')).show();
<?php if (isset($map_container_obj)) {
echo $map_container_obj ?? "" . ".invalidateSize(true);";
} ?>
});
});
</script> <?php
} ?>
</div> <?php
}
if (!isset($geolocation_panel_only)) { ?>
</div> <!--End of RecordPanel-->
</div> <!--End of RecordBox--> <?php
}

645
include/header.php Executable file
View File

@@ -0,0 +1,645 @@
<?php
hook("preheaderoutput");
$k = getval("k", "");
if (!isset($internal_share_access)) {
// Set a flag for logged in users if $external_share_view_as_internal is set and logged on user is accessing an external share
$internal_share_access = internal_share_access();
}
$logout = getval("logout", "");
$loginas = getval("loginas", "");
# Do not display header / footer when dynamically loading CentralSpace contents.
$ajax = getval("ajax", "");
// Force full page reload if CSS or JS has been updated
$current_css_reload = getval("css_reload_key", 0, true);
if ($ajax != "" && $current_css_reload != 0 && $current_css_reload != $css_reload_key) {
http_response_code(205);
$return["error"] = array(
"status" => 205,
"title" => escape($lang["error-reload-required"]),
);
echo json_encode($return);
exit();
}
rs_setcookie("css_reload_key", $css_reload_key);
$noauth_page = in_array(
$pagename,
[
"login",
"user_change_password",
"user_request",
"done",
]
);
if ($ajax == "") {
if (!isset($thumbs) && ($pagename != "login") && ($pagename != "user_password") && ($pagename != "user_request")) {
$thumbs = getval("thumbs", "unset");
if ($thumbs == "unset") {
$thumbs = $thumbs_default;
rs_setcookie("thumbs", $thumbs, 1000, "", "", false, false);
}
}
?>
<!DOCTYPE html>
<html lang="<?php echo $language ?>">
<!--
ResourceSpace version <?php echo $productversion?>
For copyright and license information see /documentation/licenses/resourcespace.txt
https://www.resourcespace.com
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="CACHE-CONTROL" content="NO-CACHE">
<meta http-equiv="PRAGMA" content="NO-CACHE">
<?php if ($search_engine_noindex || (getval("k", "") != "" && $search_engine_noindex_external_shares)) { ?>
<meta name="robots" content="noindex,nofollow">
<?php } ?>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php hook('extra_meta'); ?>
<title><?php echo escape($applicationname); ?></title>
<link rel="icon" type="image/png" href="<?php echo get_favicon_url(); ?>" />
<!-- Load jQuery and jQueryUI -->
<script src="<?php echo $baseurl . $jquery_path; ?>?css_reload_key=<?php echo $css_reload_key; ?>"></script>
<script src="<?php echo $baseurl . $jquery_ui_path?>?css_reload_key=<?php echo $css_reload_key; ?>" type="text/javascript"></script>
<script src="<?php echo $baseurl; ?>/lib/js/jquery.layout.js?css_reload_key=<?php echo $css_reload_key?>"></script>
<link type="text/css" href="<?php echo $baseurl?>/css/smoothness/jquery-ui.min.css?css_reload_key=<?php echo $css_reload_key?>" rel="stylesheet" />
<script src="<?php echo $baseurl?>/lib/js/jquery.ui.touch-punch.min.js"></script>
<?php if ($pagename == "login") { ?>
<script type="text/javascript" src="<?php echo $baseurl?>/lib/js/jquery.capslockstate.js"></script>
<?php } ?>
<script type="text/javascript" src="<?php echo $baseurl?>/lib/js/jquery.tshift.min.js"></script>
<script type="text/javascript" src="<?php echo $baseurl?>/lib/js/jquery-periodical-updater.js"></script>
<script type="text/javascript">StaticSlideshowImage=<?php echo $static_slideshow_image ? "true" : "false";?>;</script>
<script type="text/javascript" src="<?php echo $baseurl?>/js/slideshow_big.js?css_reload_key=<?php echo $css_reload_key?>"></script>
<?php if ($contact_sheet) { ?>
<script type="text/javascript" src="<?php echo $baseurl?>/js/contactsheet.js"></script>
<script>contactsheet_previewimage_prefix = '<?php echo escape($storageurl)?>';</script>
<script type="text/javascript">jQuery.noConflict();</script>
<?php } ?>
<script type="text/javascript">
var ProcessingCSRF=<?php echo generate_csrf_js_object('processing'); ?>;
var ajaxLoadingTimer=<?php echo $ajax_loading_timer;?>;
</script>
<script src="<?php echo $baseurl;?>/js/ajax_collections.js?css_reload_key=<?php echo $css_reload_key?>" type="text/javascript"></script>
<script src="<?php echo $baseurl; ?>/lib/tinymce/tinymce.min.js" referrerpolicy="origin"></script>
<!-- UPPY -->
<script type="text/javascript" src="<?php echo $baseurl_short;?>lib/js/uppy.js?<?php echo $css_reload_key;?>"></script>
<link rel="stylesheet" href="<?php echo $baseurl?>/css/uppy.min.css?css_reload_key=<?php echo $css_reload_key?>">
<?php if ($keyboard_navigation_video_search || $keyboard_navigation_video_view || $keyboard_navigation_video_preview) { ?>
<script type="text/javascript" src="<?php echo $baseurl_short?>js/videojs-extras.js?<?php echo $css_reload_key?>"></script>
<?php
}
if ($simple_search_pills_view) { ?>
<script src="<?php echo $baseurl_short; ?>lib/jquery_tag_editor/jquery.caret.min.js"></script>
<script src="<?php echo $baseurl_short; ?>lib/jquery_tag_editor/jquery.tag-editor.min.js"></script>
<link type="text/css" rel="stylesheet" href="<?php echo $baseurl_short; ?>lib/jquery_tag_editor/jquery.tag-editor.css" />
<?php
}
?>
<!-- Chart.js for graphs -->
<script language="javascript" type="module" src="<?php echo $baseurl_short; ?>lib/js/chartjs-4-4-0.js"></script>
<script language="javascript" type="module" src="<?php echo $baseurl_short; ?>lib/js/date-fns.js"></script>
<script language="javascript" type="module" src="<?php echo $baseurl_short; ?>lib/js/chartjs-adapter-date-fns.js"></script>
<!-- jsTree -->
<link rel="stylesheet" href="<?php echo $baseurl_short; ?>lib/jstree/themes/default-dark/style.min.css">
<script src="<?php echo $baseurl_short; ?>lib/jstree/jstree.min.js"></script>
<script src="<?php echo $baseurl_short; ?>js/category_tree.js?css_reload_key=<?php echo $css_reload_key; ?>"></script>
<!-- DOMPurify -->
<script src="<?php echo $baseurl; ?>/lib/js/purify.min.js?reload_key=<?php echo (int) $css_reload_key; ?>"></script>
<?php
global $not_authenticated_pages;
$not_authenticated_pages = array('login', 'user_change_password','user_password','user_request');
if (isset($GLOBALS['modify_header_not_authenticated_pages']) && is_array($GLOBALS['modify_header_not_authenticated_pages'])) {
$not_authenticated_pages = array_filter($GLOBALS['modify_header_not_authenticated_pages']);
}
$browse_on = has_browsebar();
if ($browse_on) {
?>
<script src="<?php echo $baseurl_short ?>js/browsebar_js.php" type="text/javascript"></script>
<?php
}
$selected_search_tab = getval("selected_search_tab", "");
?>
<script type="text/javascript">
var baseurl_short="<?php echo $baseurl_short?>";
var baseurl="<?php echo $baseurl?>";
var pagename="<?php echo $pagename?>";
var errorpageload = "<h1><?php echo escape($lang["error"]) ?></h1><p><?php echo escape(str_replace(array("\r","\n"), '', nl2br($lang["error-pageload"]))) ?></p>";
var errortext = "<?php echo escape($lang["error"]) ?>";
var applicationname = "<?php echo $applicationname?>";
var branch_limit=false;
var branch_limit_field = new Array();
var global_trash_html = '<!-- Global Trash Bin (added through CentralSpaceLoad) -->';
var TileNav = <?php echo $tilenav ? "true" : "false" ?>;
var errornotloggedin = '<?php echo escape($lang["error_not_logged_in"]) ?>';
var login = '<?php echo escape($lang["login"]) ?>';
<?php echo "global_trash_html += '" . render_trash("trash", "", true) . "';\n"; ?>
oktext="<?php echo escape($lang["ok"]) ?>";
var scrolltopElementCentral='.ui-layout-center';
var scrolltopElementContainer='.ui-layout-container';
var scrolltopElementCollection='.ui-layout-south';
var scrolltopElementModal='#modal';
<?php
if ($browse_on) {
echo "browse_clicked = false;";
}
?>
</script>
<script src="<?php echo $baseurl_short?>js/global.js?css_reload_key=<?php echo $css_reload_key?>" type="text/javascript"></script>
<script src="<?php echo $baseurl_short?>lib/js/polyfills.js?css_reload_key=<?php echo $css_reload_key; ?>"></script>
<?php
if ($keyboard_navigation) {
include __DIR__ . "/keyboard_navigation.php";
}
hook("additionalheaderjs");
echo $headerinsert;
$extrafooterhtml = "";
?>
<!-- Structure Stylesheet -->
<link href="<?php echo $baseurl?>/css/global.css?css_reload_key=<?php echo $css_reload_key?>" rel="stylesheet" type="text/css" media="screen,projection,print" />
<!-- Colour stylesheet -->
<link href="<?php echo $baseurl?>/css/light.css?css_reload_key=<?php echo $css_reload_key?>" rel="stylesheet" type="text/css" media="screen,projection,print" />
<!-- Override stylesheet -->
<link href="<?php echo $baseurl?>/css/css_override.php?k=<?php echo escape($k); ?>&css_reload_key=<?php echo $css_reload_key?>&noauth=<?php echo $noauth_page; ?>" rel="stylesheet" type="text/css" media="screen,projection,print" />
<!--- FontAwesome for icons-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/fontawesome/css/all.min.css?css_reload_key=<?php echo $css_reload_key?>">
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/fontawesome/css/v4-shims.min.css?css_reload_key=<?php echo $css_reload_key?>">
<!-- Load specified font CSS -->
<?php if (!isset($custom_font) || $custom_font == '') { ?>
<link id="global_font_link" href="<?php echo $baseurl?>/css/fonts/<?php echo $global_font ?>.css?css_reload_key=<?php echo $css_reload_key?>" rel="stylesheet" type="text/css" />
<?php } ?>
<!-- Web app manifest -->
<link rel="manifest" href="<?php echo $baseurl . escape($web_app_manifest_location) ?>">
<?php
if (!$disable_geocoding) {
// Geocoding & leaflet maps
// Load Leaflet and plugin files.
?>
<!--Leaflet.js files-->
<link rel="stylesheet" href="<?php echo $baseurl; ?>/lib/leaflet/leaflet.css?css_reload_key=<?php echo $css_reload_key; ?>"/>
<script src="<?php echo $baseurl; ?>/lib/leaflet/leaflet.js?<?php echo $css_reload_key; ?>"></script>
<?php
if ($geo_leaflet_maps_sources) { ?>
<!--Leaflet Providers v1.10.2 plugin files-->
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-providers-1.10.2/leaflet-providers.js"></script>
<?php
} else {
header_add_map_providers();
}
?>
<!--Leaflet PouchDBCached v1.0.0 plugin file with PouchDB v7.1.1 file-->
<?php if ($map_default_cache || $map_layer_cache) { ?>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/pouchdb-7.1.1/pouchdb-7.1.1.min.js"></script>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-PouchDBCached-1.0.0/L.TileLayer.PouchDBCached.min.js"></script>
<?php } ?>
<!--Leaflet MarkerCluster v1.4.1 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-markercluster-1.4.1/dist/MarkerCluster.css"/>
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-markercluster-1.4.1/dist/MarkerCluster.Default.css"/>
<!--Leaflet ColorMarkers v1.0.0 plugin file-->
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-colormarkers-1.0.0/js/leaflet-color-markers.js"></script>
<!--Leaflet NavBar v1.0.1 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-NavBar-1.0.1/src/Leaflet.NavBar.css"/>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-NavBar-1.0.1/src/Leaflet.NavBar.min.js"></script>
<!--Leaflet Omnivore v0.3.1 plugin file-->
<?php if ($map_kml) { ?>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-omnivore-0.3.4/leaflet-omnivore.min.js"></script>
<?php } ?>
<!--Leaflet EasyPrint v2.1.9 plugin file-->
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-easyPrint-2.1.9/dist/bundle.min.js"></script>
<!--Leaflet StyledLayerControl v5/16/2019 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-StyledLayerControl-5-16-2019/css/styledLayerControl.css"/>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-StyledLayerControl-5-16-2019/src/styledLayerControl.min.js"></script>
<!--Leaflet Zoomslider v0.7.1 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-zoomslider-0.7.1/src/L.Control.Zoomslider.css"/>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-zoomslider-0.7.1/src/L.Control.Zoomslider.min.js"></script>
<!--Leaflet Shades v1.0.2 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-shades-1.0.2/src/css/leaflet-shades.css"/>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-shades-1.0.2/leaflet-shades.js"></script>
<?php
}
echo get_plugin_css();
// after loading these tags we change the class on them so a new set can be added before they are removed (preventing flickering of overridden theme)
?>
<script>jQuery('.plugincss').attr('class','plugincss0');</script>
</head>
<body lang="<?php echo escape($language); ?>">
<a href="#UICenter" class="skip-to-main-content"><?php echo escape($lang["skip-to-main-content"]); ?></a>
<!-- Processing graphic -->
<div id='ProcessingBox' style='display: none'>
<i aria-hidden="true" class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<p id="ProcessingStatus"></p>
</div>
<!--Global Header-->
<?php
if (($pagename == "terms") && (getval("url", "") == "index.php")) {
$loginterms = true;
} else {
$loginterms = false;
}
if ($pagename != "preview") {
// Standard header
$homepage_url = $baseurl . "/pages/home.php";
if ($use_theme_as_home) {
$homepage_url = $baseurl . "/pages/collections_featured.php";
}
if ($use_recent_as_home) {
$homepage_url = $baseurl . "/pages/search.php?search=" . urlencode('!last' . $recent_search_quantity);
}
if ($pagename == "login" || $pagename == "user_request" || $pagename == "user_password") {
$homepage_url = $baseurl . "/index.php";
}
# Calculate Header Image Display
if (isset($usergroup)) {
//Get group logo value
$curr_group = get_usergroup($usergroup);
if (!empty($curr_group["group_specific_logo"])) {
$linkedheaderimgsrc = (isset($storageurl) ? $storageurl : $baseurl . "/filestore") . "/admin/groupheaderimg/group" . $usergroup . "." . $curr_group["group_specific_logo"];
}
if (!empty($curr_group["group_specific_logo_dark"])) {
$linkedheaderimgsrc_dark = (isset($storageurl) ? $storageurl : $baseurl . "/filestore") . "/admin/groupheaderimg/group" . $usergroup . "_dark." . $curr_group["group_specific_logo_dark"];
}
}
$linkUrl = isset($header_link_url) ? $header_link_url : $homepage_url;
?>
<div id="Header" class="<?php
echo in_array($pagename, $not_authenticated_pages) ? ' LoginHeader ' : ' ui-layout-north ';
echo isset($slimheader_darken) && $slimheader_darken ? 'slimheader_darken' : ''; ?>"
>
<div id="HeaderResponsive">
<?php
$header_img_src = get_header_image(false, true);
if ($header_link && ($k == "" || $internal_share_access)) { ?>
<a href="<?php echo $linkUrl; ?>" onclick="return CentralSpaceLoad(this,true);" class="HeaderImgLink">
<img src="<?php echo $header_img_src; ?>" id="HeaderImg" alt="<?php echo $applicationname;?>">
</a>
<?php
} else {
?>
<div class="HeaderImgLink">
<img src="<?php echo $header_img_src; ?>" id="HeaderImg" alt="<?php echo $applicationname;?>">
</div>
<?php
}
$user_profile_image = get_profile_image($userref, false);
// Responsive
if (isset($username) && ($pagename != "login") && !$loginterms && getval("k", "") == "") {
?>
<div id="HeaderButtons" style="display:none;">
<div id="ButtonHolder">
<a href="#" id="HeaderNav2Click" class="ResponsiveHeaderButton ResourcePanel ResponsiveButton">
<span class="rbText"><?php echo escape($lang["responsive_main_menu"]); ?></span>
<span class="fa fa-fw fa-lg fa-bars"></span>
</a>
<a href="#" id="HeaderNav1Click" class="ResponsiveHeaderButton ResourcePanel ResponsiveButton">
<span class="rbText">
<?php if (!$allow_password_change) {
echo escape((!isset($userfullname) || $userfullname == "" ? $username : $userfullname));
} else {
echo escape($lang["responsive_settings_menu"]);
} ?>
</span>
<?php if ($user_profile_image != "") { ?>
<img src='<?php echo $user_profile_image; ?>' alt='Profile icon' class="ProfileImage" id='UserProfileImage'>
<?php } else { ?>
<span class="fa fa-fw fa-lg fa-user"></span>
<?php } ?>
</a>
</div>
</div>
<?php
}
?>
</div>
<?php
hook("headertop");
if (!isset($allow_password_change)) {
$allow_password_change = true;
}
if (isset($username) && !in_array($pagename, $not_authenticated_pages) && !$loginterms && '' == $k || $internal_share_access) {
?>
<div id="HeaderNav2" class="HorizontalNav HorizontalWhiteNav">
<?php
if (!($pagename == "terms" && isset($_SERVER["HTTP_REFERER"]) && strpos($_SERVER["HTTP_REFERER"], "login") !== false && $terms_login)) {
include __DIR__ . "/header_links.php";
}
?>
</div>
<div id="HeaderNav1" class="HorizontalNav">
<?php
if (checkPermission_anonymoususer()) {
if (!hook("replaceheadernav1anon")) {
?>
<ul>
<li>
<a href="<?php echo $baseurl?>/login.php"><?php echo escape($lang["login"]); ?></a>
</li>
<?php if ($contact_link) { ?>
<li>
<a href="<?php echo $baseurl?>/pages/contact.php" onclick="return CentralSpaceLoad(this,true);">
<?php echo escape($lang["contactus"]); ?>
</a>
</li>
<?php } ?>
</ul>
<?php
} /* end replaceheadernav1anon */
} else {
?>
<ul>
<?php
if (
(
($top_nav_upload && checkperm("c"))
|| ($top_nav_upload_user && checkperm("d"))
)
&& ($useracceptedterms == 1 || !$terms_login)
) {
$topuploadurl = get_upload_url("", $k);
?>
<li class="HeaderLink UploadButton">
<a href="<?php echo $topuploadurl ?>" onclick="return CentralSpaceLoad(this,true);">
<?php echo UPLOAD_ICON . escape($lang["upload"]); ?>
</a>
</li>
<?php
}
?>
<li>
<a href="<?php echo $baseurl; ?>/pages/user/user_home.php" onclick="ModalClose(); return ModalLoad(this, true, true, 'right');" alt="<?php echo escape($lang['myaccount']); ?>" title="<?php echo escape($lang['myaccount']); ?>">
<?php
if (isset($header_include_username) && $header_include_username) {
if ($user_profile_image != "") {
?>
<img src='<?php echo $user_profile_image; ?>' alt='Profile icon' class="ProfileImage" id='UserProfileImage'> &nbsp;<?php echo escape($userfullname == "" ? $username : $userfullname) ?>
<span class="MessageTotalCountPill Pill" style="display: none;"></span>
<?php
} else {
?>
<i aria-hidden="true" class="fa fa-user fa-fw"></i>&nbsp;<?php echo escape($userfullname == "" ? $username : $userfullname) ?>
<span class="MessageTotalCountPill Pill" style="display: none;"></span>
<?php
}
} else {
if ($user_profile_image != "") {
?>
<img src='<?php echo $user_profile_image; ?>' alt='Profile icon' class="ProfileImage" id='UserProfileImage'>
<span class="MessageTotalCountPill Pill" style="display: none;"></span>
<?php
} else {
?>
<i aria-hidden="true" class="fa fa-user fa-lg fa-fw"></i>
<span class="MessageTotalCountPill Pill" style="display: none;"></span>
<?php
}
}
?>
</a>
<div id="MessageContainer" style="position:absolute; "></div>
</li>
<!-- Admin menu link -->
<?php if (checkperm("t") && ($useracceptedterms == 1 || !$terms_login)) { ?>
<li>
<a href="<?php echo $baseurl?>/pages/team/team_home.php" onclick="ModalClose();return ModalLoad(this,true,true,'right');" alt="<?php echo escape($lang['teamcentre']); ?>" title="<?php echo escape($lang['teamcentre']); ?>">
<i aria-hidden="true" class="fa fa-lg fa-bars fa-fw"></i>
<?php
if (!$actions_on && (checkperm("R") || checkperm("r"))) {
# Show pill count if there are any pending requests
$pending = ps_value("select sum(thecount) value from (select count(*) thecount from request where status = 0 union select count(*) thecount from research_request where status = 0) as theunion", array(), 0);
?>
<span id="TeamMessages" class="Pill" <?php echo $pending > 0 ? 'data-value="' . $pending . '"' : 'style="display:none"'?>>
<?php echo $pending > 0 ? $pending : '' ?>
</span>
<?php
} else {
?>
<span id="TeamMessages" class="Pill" style="display:none"></span>
<?php
}
?>
</a>
</li>
<?php
} ?>
<!-- End of Admin link -->
</ul>
<?php
}
include_once __DIR__ . '/../pages/ajax/message.php';
?>
</div>
<?php
} else {
# Empty Header
?>
<div id="HeaderNav1" class="HorizontalNav ">&nbsp;</div>
<div id="HeaderNav2" class="HorizontalNav HorizontalWhiteNav">&nbsp;</div>
<?php
}
}
?>
<div class="clearer"></div>
<?php if ($pagename != "preview") { ?>
</div>
<?php } # End of header
$omit_searchbar_pages = array(
'index',
'search_advanced',
'preview',
'admin_header',
'login',
'user_request',
'user_password',
'user_change_password',
'document_viewer'
);
if ($pagename == "terms" && isset($_SERVER["HTTP_REFERER"]) && strpos($_SERVER["HTTP_REFERER"], "login") !== false && $terms_login) {
array_push($omit_searchbar_pages, 'terms');
}
# if config set to display search form in header or (usergroup search permission omitted and anonymous login panel not to be displayed, then do not show simple search bar
if (checkperm("s") || (is_anonymous_user() && $show_anonymous_login_panel)) {
# Include simple search sidebar?
if (isset($GLOBALS['modify_header_omit_searchbar_pages']) && is_array($GLOBALS['modify_header_omit_searchbar_pages'])) {
$omit_searchbar_pages = array_filter($GLOBALS['modify_header_omit_searchbar_pages']);
}
if (!in_array($pagename, $omit_searchbar_pages) && !$loginterms && ($k == '' || $internal_share_access)) {
?>
<div id="SearchBarContainer" class="ui-layout-east" >
<?php include __DIR__ . "/searchbar.php"; ?>
</div>
<?php
}
}
# Determine which content holder div to use
if (
$pagename == "login"
|| $pagename == "user_password"
|| $pagename == "user_request"
|| ($pagename == "user_change_password" && !is_authenticated())
) {
$div = "CentralSpaceLogin";
$uicenterclass = "NoSearch";
} else {
$div = "CentralSpace";
if (in_array($pagename, $omit_searchbar_pages)) {
$uicenterclass = "NoSearch";
} else {
$uicenterclass = "Search";
}
}
?>
<!--Main Part of the page-->
<!-- Global Trash Bin -->
<?php
render_trash("trash", "");
echo '<div id="UICenter" role="main" class="ui-layout-center ' . $uicenterclass . '">';
hook('afteruicenter');
if (!in_array($pagename, $not_authenticated_pages)) {
echo '<div id="CentralSpaceContainer">';
}
?>
<div id="<?php echo $div; ?>">
<?php
} // end if !ajax
// Update header links to add a class that indicates current location
// We parse URL for systems that are one level deep under web root
$parsed_url = parse_url($baseurl);
$scheme = @$parsed_url['scheme'];
$host = @$parsed_url['host'];
$port = (isset($parsed_url['port']) ? ":{$parsed_url['port']}" : "");
$activate_header_link = "{$scheme}://{$host}{$port}" . urlencode($_SERVER["REQUEST_URI"]);
if (!$disable_geocoding) {
get_geolibraries();
}
?>
<script>
// Set some vars for this page to enable/disable functionality
linkreload = <?php echo ($k != "" || $internal_share_access) ? "false" : "true" ?>;
b_progressmsgs = <?php echo $noauth_page ? "false" : "true" ?>;
jQuery(document).ready(function() {
ActivateHeaderLink(<?php echo json_encode($activate_header_link); ?>);
<?php if (!$ajax) { ?>
setThemePreference();
<?php } ?>
jQuery(document).mouseup(function(e) {
var linksContainer = jQuery("#DropdownCaret");
if (linksContainer.has(e.target).length === 0 && !linksContainer.is(e.target)) {
jQuery('#OverFlowLinks').hide();
}
});
<?php if (isset($user_pref_appearance) && $user_pref_appearance == "device" && !$ajax) { ?>
// Run on page load if using device default for appearance
updateHeaderImage();
// Listen for changes to colour scheme
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateHeaderImage);
<?php } ?>
});
window.onresize = function() {
ReloadLinks();
}
</script>
<?php
// Non-ajax specific hook
if ($k != "" && !$internal_share_access) {
?>
<style>
#CentralSpaceContainer {
padding-right: 0;
margin: 0px 10px 20px 25px;
}
</style>
<?php
}

181
include/header_links.php Normal file
View File

@@ -0,0 +1,181 @@
<nav aria-label="<?php echo escape($lang['mainmenu']) ?>">
<ul id="HeaderLinksContainer">
<?php if (!$use_theme_as_home && !$use_recent_as_home) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/home.php" onClick="return CentralSpaceLoad(this,true);">
<?php echo DASH_ICON . escape($lang["dash"]); ?>
</a>
</li>
<?php } ?>
<?php hook("topnavlinksafterhome"); ?>
<?php if ($advanced_search_nav) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/search_advanced.php" onClick="return CentralSpaceLoad(this,true);">
<i aria-hidden="true" class="fa fa-fw fa-search-plus"></i>
<?php echo escape($lang["advancedsearch"]); ?>
</a>
</li>
<?php } ?>
<?php if ($search_results_link) { ?>
<li class="HeaderLink">
<?php if ((checkperm("s")) && ((isset($_COOKIE["search_form_submit"]) ) || (isset($_COOKIE["search"]) && strlen($_COOKIE["search"]) > 0) || (isset($search) && (strlen($search) > 0) && (strpos($search, "!") === false)))) { # active search present ?>
<a href="<?php echo $baseurl?>/pages/search.php" onClick="return CentralSpaceLoad(this,true);">
<i aria-hidden="true" class="fa fa-fw fa-search"></i>
<?php echo escape($lang["searchresults"]); ?>
</a>
<?php } else { ?>
<a class="SearchResultsDisabled">
<i aria-hidden="true" class="fa fa-fw fa-search"></i>
<?php echo escape($lang["searchresults"]); ?>
</a>
<?php } ?>
</li>
<?php } ?>
<?php if (checkperm("s") && $enable_themes && !$theme_direct_jump && $themes_navlink) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/collections_featured.php" onClick="return CentralSpaceLoad(this,true);">
<?php echo FEATURED_COLLECTION_ICON . escape($lang["themes"]); ?>
</a>
</li>
<?php } ?>
<?php if (checkperm("s") && ($public_collections_top_nav)) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/collection_public.php" onClick="return CentralSpaceLoad(this,true);">
<i class="fas fa-shopping-bag"></i>
<?php echo escape($lang["publiccollections"]); ?>
</a>
</li>
<?php } ?>
<?php if (checkperm("s") && $mycollections_link && !checkperm("b")) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/collection_manage.php" onClick="return CentralSpaceLoad(this,true);">
<i class="fas fa-shopping-bag"></i>
<?php echo escape($lang["mycollections"]); ?>
</a>
</li>
<?php } ?>
<?php if (checkperm("s") && $recent_link) {
if ($recent_search_by_days) {
$recent_url_params = [
"search" => "",
"recentdaylimit" => $recent_search_by_days_default,
];
} else {
$recent_url_params = [
"search" => "!last" . $recent_search_quantity
];
}
$recent_url_params["order_by"] = "resourceid";
$recent_url_params["sort"] = "desc";
$recenturl = generateURL("$baseurl/pages/search.php", $recent_url_params);
?>
<li class="HeaderLink">
<a href="<?php echo $recenturl ?>" onClick="return CentralSpaceLoad(this,true);">
<?php echo RECENT_ICON . escape($lang["recent"]); ?>
</a>
</li>
<?php } ?>
<?php if (checkperm("s") && $myrequests_link && checkperm("q")) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/requests.php" onClick="return CentralSpaceLoad(this,true);">
<i aria-hidden="true" class="fa fa-fw fa-shopping-cart"></i>
<?php echo escape($lang["myrequests"]); ?>
</a>
</li>
<?php } ?>
<?php if (checkperm("d") || ($mycontributions_link && checkperm("c"))) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/contribute.php" onClick="return CentralSpaceLoad(this,true);">
<?php echo CONTRIBUTIONS_ICON . escape($lang["mycontributions"]); ?>
</a>
</li>
<?php } ?>
<?php if (($research_request) && ($research_link) && (checkperm("s")) && (checkperm("q"))) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/research_request.php" onClick="return CentralSpaceLoad(this,true);">
<i aria-hidden="true" class="fa fa-fw fa-question-circle"></i>
<?php echo escape($lang["researchrequest"]); ?>
</a>
</li>
<?php } ?>
<?php
/* ------------ Customisable top navigation ------------------- */
if (isset($custom_top_nav)) {
for ($n = 0; $n < count($custom_top_nav); $n++) {
if (!is_safe_url($custom_top_nav[$n]['link'])) {
debug("Unsafe link detected in configuration - {$custom_top_nav[$n]['link']}");
continue;
}
// External links should open in a new tab
if (!url_starts_with($baseurl, $custom_top_nav[$n]['link'])) {
$on_click = '';
$target = ' target="_blank"';
}
//Internal links can still open in the same tab
else {
if (isset($custom_top_nav[$n]['modal']) && $custom_top_nav[$n]['modal']) {
$on_click = ' onClick="return ModalLoad(this, true);"';
$target = '';
} elseif (!isset($custom_top_nav[$n]['modal']) || (isset($custom_top_nav[$n]['modal']) && !$custom_top_nav[$n]['modal'])) {
$on_click = ' onClick="return CentralSpaceLoad(this, true);"';
$target = '';
}
}
if (strpos($custom_top_nav[$n]['title'], '(lang)') !== false) {
$custom_top_nav_title = str_replace("(lang)", "", $custom_top_nav[$n]["title"]);
$custom_top_nav[$n]["title"] = $lang[$custom_top_nav_title];
}
?>
<li class="HeaderLink">
<a href="<?php echo $custom_top_nav[$n]["link"]; ?>"<?php echo $target . $on_click; ?>><?php
// Not escaping to allow links to have an icon (if applicable)
echo strip_tags_and_attributes(i18n_get_translated($custom_top_nav[$n]["title"]));
?></a>
</li>
<?php
}
} ?>
<?php if ($help_link) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/help.php" onClick="return <?php if (!$help_modal) {
?>CentralSpaceLoad(this,true);<?php
} else {
?>ModalLoad(this,true);<?php
} ?>">
<?php echo HELP_ICON . escape($lang["helpandadvice"]); ?>
</a>
</li>
<?php } ?>
<?php global $nav2contact_link; if ($nav2contact_link) { ?>
<li class="HeaderLink">
<a href="<?php echo $baseurl?>/pages/contact.php" onClick="return CentralSpaceLoad(this,true);">
<?php echo escape($lang["contactus"]); ?>
</a>
</li>
<?php }
hook("toptoolbaradder"); ?>
</ul><!-- close HeaderLinksContainer -->
</nav>
<script>
jQuery(document).ready(function() {
headerLinksDropdown();
});
</script>

1432
include/iiif_functions.php Normal file

File diff suppressed because it is too large Load Diff

4222
include/image_processing.php Executable file

File diff suppressed because it is too large Load Diff

400
include/job_functions.php Normal file
View File

@@ -0,0 +1,400 @@
<?php
// Functions to support offline jobs ($offline_job_queue = true)
// Offline jobs require a frequent cron/scheduled task to run tools/offline_jobs.php
/**
* Adds a job to the job_queue table.
*
* @param string $type
* @param array $job_data
* @param string $user
* @param string $time
* @param string $success_text
* @param string $failure_text
* @param string $job_code
* @param int $priority
* @return string|integer ID of newly created job or error text
*/
function job_queue_add($type = "", $job_data = array(), $user = "", $time = "", $success_text = "", $failure_text = "", $job_code = "", $priority = null)
{
global $lang, $userref;
if ($time == "") {
$time = date('Y-m-d H:i:s');
}
if ($type == "") {
return false;
}
if ($user == "") {
$user = isset($userref) ? $userref : 0;
}
// Assign priority based on job type if not explicitly passed
if (!is_int_loose($priority)) {
$priority = get_job_type_priority($type);
}
$job_data_json = json_encode($job_data, JSON_UNESCAPED_SLASHES); // JSON_UNESCAPED_SLASHES is needed so we can effectively compare jobs
if ($job_code == "") {
// Generate a code based on job data to avoid incorrect duplicate job detection
$job_code = $type . "_" . substr(md5(serialize($job_data)), 10);
}
// Check for existing job matching
$existing_user_jobs = job_queue_get_jobs($type, STATUS_ACTIVE, "", $job_code);
if (count($existing_user_jobs) > 0) {
return $lang["job_queue_duplicate_message"];
}
ps_query("INSERT INTO job_queue (type,job_data,user,start_date,status,success_text,failure_text,job_code, priority) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", array("s",$type,"s",$job_data_json,"i",$user,"s",$time,"i",STATUS_ACTIVE,"s",$success_text,"s",$failure_text,"s",$job_code,"i",(int)$priority));
return sql_insert_id();
}
/**
* Update the data/status/time of a job queue record.
*
* @param integer $ref
* @param array $job_data - pass empty array to leave unchanged
* @param string $newstatus
* @param string $newtime
* @return void
*/
function job_queue_update($ref, $job_data = array(), $newstatus = "", $newtime = "", $priority = null)
{
$update_sql = array();
$parameters = array();
if (count($job_data) > 0) {
$update_sql[] = "job_data = ?";
$parameters = array_merge($parameters, array("s",json_encode($job_data)));
}
if ($newtime != "") {
$update_sql[] = "start_date = ?";
$parameters = array_merge($parameters, array("s",$newtime));
}
if ($newstatus != "") {
$update_sql[] = "status = ?";
$parameters = array_merge($parameters, array("i",$newstatus));
}
if (is_int_loose($priority)) {
$update_sql[] = "priority = ?";
$parameters = array_merge($parameters, array("i",(int)$priority));
}
if (count($update_sql) == 0) {
return false;
}
$sql = "UPDATE job_queue SET " . implode(",", $update_sql) . " WHERE ref = ?";
$parameters = array_merge($parameters, array("i",$ref));
ps_query($sql, $parameters);
}
/**
* Delete a job queue entry if user owns job or user is admin
*
* @param mixed $ref
* @return void
*/
function job_queue_delete($ref)
{
global $userref;
$query = "DELETE FROM job_queue WHERE ref= ?";
$parameters = array("i",$ref);
if (!checkperm('a') && !php_sapi_name() == "cli") {
$query .= " AND user = ?";
$parameters = array_merge($parameters, array("i",$userref));
}
ps_query($query, $parameters);
}
/**
* Gets a list of offline jobs
*
* @param string $type Job type, can be a comma separated list of job types
* @param string $status Job status - see definitions.php
* @param int $user Job user
* @param string $job_code Unique job code
* @param string $job_order_by Column to order by - default is priority
* @param string $job_sort Sort order - ASC or DESC
* @param string $find Search jobs for this string
* @param bool $returnsql Return raw SQL
* @param int $maxjobs Maximum number of jobs to return
* @param bool $overdue Only return overdue jobs?
* @param array $find_by_job_ref Find queued jobs by their ref
* @return mixed Resulting array of requests or an SQL query object
*/
function job_queue_get_jobs($type = "", $status = -1, $user = "", $job_code = "", $job_order_by = "priority", $job_sort = "asc", $find = "", $returnsql = false, int $maxjobs = 0, bool $overdue = false, array $find_by_job_ref = [])
{
global $userref;
$condition = array();
$parameters = array();
if ($type != "") {
$types = explode(",", $type);
$condition[] = " type IN (" . ps_param_insert(count($types)) . ")";
$parameters = array_merge($parameters, ps_param_fill($types, "s"));
}
if (!checkperm('a') && PHP_SAPI != 'cli') {
// Don't show certain jobs for normal users
$hiddentypes = array();
$hiddentypes[] = "delete_file";
$condition[] = " type NOT IN (" . ps_param_insert(count($hiddentypes)) . ")";
$parameters = array_merge($parameters, ps_param_fill($hiddentypes, "s"));
}
if ((int)$status > -1) {
$condition[] = " status = ? ";
$parameters = array_merge($parameters, array("i",(int)$status));
}
if ($overdue) {
$condition[] = " start_date <= ? ";
$parameters = array_merge($parameters, array("s",date('Y-m-d H:i:s')));
}
if ((int)$user > 0) {
// Has user got access to see this user's jobs?
if ($user == $userref || checkperm_user_edit($user)) {
$condition[] = " user = ?";
$parameters = array_merge($parameters, array("i",(int)$user));
} elseif (isset($userref)) {
// Only show own jobs
$condition[] = " user = ?";
$parameters = array_merge($parameters, array("i",(int)$userref));
} else {
// No access - return empty array
return array();
}
} else {
// Requested jobs for all users - only possible for cron or system admin, set condition otherwise
if (PHP_SAPI != "cli" && !checkperm('a')) {
if (isset($userref)) {
// Only show own jobs
$condition[] = " user = ?";
$parameters = array_merge($parameters, array("i",(int)$userref));
} else {
// No access - return nothing
return array();
}
}
}
if ($job_code != "") {
$condition[] = " job_code = ?";
$parameters = array_merge($parameters, array("s",$job_code));
}
if ($find != "") {
$find = '%' . $find . '%';
$condition[] = " (j.ref LIKE ? OR j.job_data LIKE ? OR j.success_text LIKE ? OR j.failure_text LIKE ? OR j.user LIKE ? OR u.username LIKE ? OR u.fullname LIKE ?)";
}
$find_by_job_ref = array_values(array_filter($find_by_job_ref, is_positive_int_loose(...)));
if ($find_by_job_ref !== []) {
$condition[] = 'j.ref IN (' . ps_param_insert(count($find_by_job_ref)) . ')';
$parameters = array_merge($parameters, ps_param_fill($find_by_job_ref, 'i'));
}
$conditional_sql = "";
if (count($condition) > 0) {
$conditional_sql = " WHERE " . implode(" AND ", $condition);
}
// Check order by value is valid
if (!in_array(strtolower($job_order_by), array("priority", "ref", "type", "fullname", "status", "start_date"))) {
$job_order_by = "priority";
}
// Check sort value is valid
if (!in_array(strtolower($job_sort), array("asc", "desc"))) {
$job_sort = "ASC";
}
$limit = "";
if ($maxjobs > 0) {
$limit = " LIMIT ?";
$parameters = array_merge($parameters, ["i",$maxjobs]);
}
$sql = "SELECT j.ref, j.type, REPLACE(REPLACE(j.job_data,'\r',' '),'\n',' ') AS job_data, j.user, j.status, j.start_date, j.success_text, j.failure_text,j.job_code, j.priority, u.username, u.fullname FROM job_queue j LEFT JOIN user u ON u.ref = j.user " . $conditional_sql . " ORDER BY " . $job_order_by . " " . $job_sort . ", start_date " . $job_sort . $limit;
if ($returnsql) {
return new PreparedStatementQuery($sql, $parameters);
}
return ps_query($sql, $parameters);
}
/**
* Get details of specified offline job
*
* @param int $job identifier
* @return array
*/
function job_queue_get_job($ref)
{
$sql = "SELECT j.ref, j.type, j.job_data, j.user, j.status, j.start_date, j.priority, j.success_text, j.failure_text, j.job_code, u.username, u.fullname FROM job_queue j LEFT JOIN user u ON u.ref = j.user WHERE j.ref = ?";
$job_data = ps_query($sql, array("i",(int)$ref));
return (is_array($job_data) && count($job_data) > 0) ? $job_data[0] : array();
}
/**
* Delete all jobs in the specified state
*
* @param int $status to purge, whole queue will be purged if not set
* @return void
*/
function job_queue_purge($status = 0)
{
$deletejobs = job_queue_get_jobs('', $status == 0 ? '' : $status);
if (count($deletejobs) > 0) {
$deletejobs_sql = job_queue_get_jobs('', $status == 0 ? '' : $status, "", "", "priority", "asc", "", true);
ps_query(
"DELETE FROM job_queue
WHERE ref IN
(SELECT jobs.ref FROM
( " . $deletejobs_sql->sql . ") AS jobs)",
$deletejobs_sql->parameters
);
}
}
/**
* Run offline job
*
* @param array $job Metadata of the queued job as returned by job_queue_get_jobs()
* @param boolean $clear_process_lock Clear process lock for this job
*
* @return void
*/
function job_queue_run_job($job, $clear_process_lock)
{
// Runs offline job using defined job handler
$jobref = $job["ref"];
$job_data = json_decode($job["job_data"], true);
$jobuser = $job["user"];
if (!isset($jobuser) || $jobuser == 0 || $jobuser == "") {
$logmessage = " - Job could not be run as no user was supplied #{$jobref}" . PHP_EOL;
echo $logmessage;
debug($logmessage);
job_queue_update($jobref, $job_data, STATUS_ERROR);
return;
}
$jobuserdata = get_user($jobuser);
if (!$jobuserdata) {
$logmessage = " - Job #{$jobref} could not be run as invalid user ref #{$jobuser} was supplied." . PHP_EOL;
echo $logmessage;
debug($logmessage);
job_queue_update($jobref, $job_data, STATUS_ERROR);
return;
}
setup_user($jobuserdata);
$job_success_text = $job["success_text"];
$job_failure_text = $job["failure_text"];
// Variable used to avoid spinning off offline jobs from an already existing job.
// Example: create_previews() is using extract_text() and both can run offline.
global $offline_job_in_progress, $plugins;
$offline_job_in_progress = false;
if (is_process_lock('job_' . $jobref) && !$clear_process_lock) {
$logmessage = " - Process lock for job #{$jobref}" . PHP_EOL;
echo $logmessage;
debug($logmessage);
return;
} elseif ($clear_process_lock) {
$logmessage = " - Clearing process lock for job #{$jobref}" . PHP_EOL;
echo $logmessage;
debug($logmessage);
clear_process_lock("job_{$jobref}");
}
set_process_lock('job_' . $jobref);
$logmessage = "Running job #" . $jobref . ' at ' . date('Y-m-d H:i:s') . PHP_EOL;
echo $logmessage;
debug($logmessage);
$logmessage = " - Looking for " . __DIR__ . "/job_handlers/" . $job["type"] . ".php" . PHP_EOL;
echo $logmessage;
debug($logmessage);
if (file_exists(__DIR__ . "/job_handlers/" . $job["type"] . ".php")) {
$logmessage = " - Attempting to run job #" . $jobref . " using handler " . $job["type"] . PHP_EOL;
echo $logmessage;
debug($logmessage);
job_queue_update($jobref, $job_data, STATUS_INPROGRESS);
$offline_job_in_progress = true;
include __DIR__ . "/job_handlers/" . $job["type"] . ".php";
} else {
// Check for handler in plugin
$offline_plugins = $plugins;
// Include plugins for this job user's group
$group_plugins = ps_query("SELECT name, config, config_json, disable_group_select FROM plugins WHERE inst_version >= 0 AND disable_group_select = 0 AND find_in_set(?,enabled_groups) ORDER BY priority", array("i",$jobuserdata["usergroup"]), "plugins");
foreach ($group_plugins as $group_plugin) {
include_plugin_config($group_plugin['name'], $group_plugin['config'], $group_plugin['config_json']);
register_plugin($group_plugin['name']);
register_plugin_language($group_plugin['name']);
$offline_plugins[] = $group_plugin['name'];
}
foreach ($offline_plugins as $plugin) {
if (file_exists(__DIR__ . "/../plugins/" . $plugin . "/job_handlers/" . $job["type"] . ".php")) {
$logmessage = " - Attempting to run job #" . $jobref . " using handler " . $job["type"] . PHP_EOL;
echo $logmessage;
debug($logmessage);
job_queue_update($jobref, $job_data, STATUS_INPROGRESS);
$offline_job_in_progress = true;
include __DIR__ . "/../plugins/" . $plugin . "/job_handlers/" . $job["type"] . ".php";
break;
}
}
}
if (!$offline_job_in_progress) {
$logmessage = "Unable to find handlerfile: " . $job["type"] . PHP_EOL;
echo $logmessage;
debug($logmessage);
job_queue_update($jobref, $job_data, STATUS_ERROR, date('Y-m-d H:i:s'));
}
$logmessage = " - Finished job #" . $jobref . ' at ' . date('Y-m-d H:i:s') . PHP_EOL;
echo $logmessage;
debug($logmessage);
clear_process_lock('job_' . $jobref);
}
/**
* Get the default priority for a given job type
*
* @param string $type Name of job type e.g. 'collection_download'
*
* @return int
*/
function get_job_type_priority($type = "")
{
if (trim($type) != "") {
switch (trim($type)) {
case 'collection_download':
case 'create_download_file':
case 'config_export':
case 'csv_metadata_export':
return JOB_PRIORITY_USER;
break;
case 'create_previews':
case 'extract_text':
case 'replace_batch_local':
case 'create_alt_file':
case 'delete_file':
case 'update_resource':
case 'upload_processing':
return JOB_PRIORITY_SYSTEM;
break;
default:
return JOB_PRIORITY_SYSTEM;
break;
}
}
return JOB_PRIORITY_SYSTEM;
}

View File

@@ -0,0 +1,77 @@
<?php
/*
Job handler to process collection downloads
Requires the following job data:
$job_data['collection'] Collection ID
$job_data['collectiondata'] Collection data from get_collection()
$job_data['collection_resources'] Resources to include in download (was previously 'result')
$job_data['size'] Requested size
$job_data['exiftool_write_option'] Write embedded data (not for TAR downloads)
$job_data['useoriginal'] Use original if requested size not available?
$job_data['id'] Unique identifier - used to create a download.php link that is specific to the user
$job_data['includetext'] Include text file in download (config $zipped_collection_textfile)
$job_data['count_data_only_types'] Count of data only resources
$job_data['usage'] Download usage (selected index of $download_usage)
$job_data['usagecomment'] Download usage comment
$job_data['settings_id'] Index of selected option from $collection_download_settings array
$job_data['include_csv_file'] Include metadata CSV file?
$job_data['include_alternatives'] Include alternative files?
$job_data['k'] External access key if set
*/
include_once __DIR__ . '/../pdf_functions.php';
include_once __DIR__ . '/../csv_export_functions.php';
// Used by format chooser
if (isset($job_data["ext"])) {
global $job_ext;
$job_ext = $job_data["ext"];
}
// Set up the user who requested the collection download as it needs to be processed in its name
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.ref = ?";
$user_select_sql->parameters = ["i", $job['user']];
$user_data = validate_user($user_select_sql, true);
if (count($user_data) > 0) {
setup_user($user_data[0]);
} else {
job_queue_update($jobref, $job_data, STATUS_ERROR);
return;
}
$zipinfo = process_collection_download($job_data);
collection_log($job_data['collection'], LOG_CODE_COLLECTION_COLLECTION_DOWNLOADED, "", $job_data['size']);
if (!is_valid_rs_path($zipinfo["path"])) {
job_queue_update($jobref, $job_data, STATUS_ERROR);
message_add($job["user"], $GLOBALS["lang"]["nothing_to_download"]);
return;
}
if ($GLOBALS['offline_job_delete_completed']) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
$extension = "zip";
if (isset($job_data["archiver"]) && isset($job_data["settings_id"])) {
$extension = $GLOBALS["collection_download_settings"][$job_data["settings_id"]]['extension'] ?? "zip";
}
$urlparams = [
"userfile" => $user_data[0]["ref"] . "_" . $job_data["id"] . "." . $extension,
"filename" => pathinfo($zipinfo["filename"], PATHINFO_FILENAME),
];
$download_url = generateURL($GLOBALS['baseurl_short'] . "pages/download.php", $urlparams);
message_add($job["user"], $job_success_text, $download_url);
$delete_job_data = [];
$delete_job_data["file"] = $zipinfo["path"];
$delete_date = date('Y-m-d H:i:s', time() + (60 * 60 * 24 * DOWNLOAD_FILE_LIFETIME)); // Delete file after set number of days
$job_code = md5($zipinfo["path"]);
job_queue_add("delete_file", $delete_job_data, "", $delete_date, "", "", $job_code);

View File

@@ -0,0 +1,129 @@
<?php
/*
Create a zip file with system configuration and selected data
Requires the following:-
$job_data["exporttables"] - Array of table information to export
$job_data["obfuscate"] - Whether table data should be obfuscated or not
*/
global $baseurl, $baseurl_short, $userref, $offline_job_delete_completed, $lang,$mysql_bin_path, $mysql_server, $mysql_db,$mysql_username,$mysql_password,$scramble_key, $system_download_config, $system_download_config_force_obfuscation;
$exporttables = $job_data["exporttables"];
$obfuscate = ($system_download_config_force_obfuscation || $job_data["obfuscate"] == "true");
$userref = $job_data["userref"];
$separatesql = $job_data["separatesql"] == "true";
$path = $mysql_bin_path . "/mysqldump";
$cmd_db_pass = $mysql_password === '' ? '' : '-p' . escapeshellarg($mysql_password);
if (!$system_download_config) {
// Not permitted but shouldn't ever occur. Update job queue
job_queue_update($jobref, $job_data, STATUS_ERROR);
$message = $lang["exportfailed"] . " - " . str_replace("[config_option]", "\$system_download_config", $lang["error_check_config"]);
message_add($job["user"], $message, "", 0);
exit();
}
$jobuser = get_user($userref);
if (is_array($jobuser)) {
$jobusername = $jobuser["username"];
} else {
$joberror = true;
}
$jobsuccess = false;
if (!isset($joberror)) {
$randstring = md5(rand() . microtime());
$dumppath = get_temp_dir(false, md5($userref . $randstring . $scramble_key)) . "/mysql";
$zippath = get_temp_dir(false, 'user_downloads');
mkdir($dumppath, 0777, true);
$zipfile = $zippath . "/" . $userref . "_" . md5($jobusername . $randstring . $scramble_key) . ".zip";
$zip = new ZipArchive();
$zip->open($zipfile, ZIPARCHIVE::CREATE);
$zip->addFile(__DIR__ . "/../../include/config.php", "config.php");
foreach ($exporttables as $exporttable => $exportoptions) {
echo "Exporting table " . $exporttable . "\n";
$dumpfile = $separatesql ? $dumppath . "/" . $exporttable . ".sql" : $dumppath . "/resourcespace.sql";
run_command(
"{$path} -h db_host -u db_user {$cmd_db_pass} --no-tablespaces --no-data db_name export_table >> dump_file",
false,
[
'db_host' => $mysql_server,
'db_user' => $mysql_username,
'db_name' => $mysql_db,
'export_table' => $exporttable,
'dump_file' => $dumpfile,
]
);
$sql = "SET sql_mode = '';\n"; // Ensure that any old values that may not now be valid are still accepted into new DB
$output = fopen($dumpfile, 'a');
fwrite($output, $sql);
fclose($output);
// Get data
$exportcondition = isset($exportoptions["exportcondition"]) ? $exportoptions["exportcondition"] : "";
$datarows = ps_query("SELECT * FROM " . $exporttable . " " . $exportcondition, array());
if (count($datarows) > 0) {
// Call function to scramble the data based on per table configuration
array_walk($datarows, 'alter_data', (isset($exportoptions["scramble"]) && $obfuscate) ? $exportoptions["scramble"] : array());
// Get columns to insert
$columns = array_keys($datarows[0]);
$sql = "";
foreach ($datarows as $datarow) {
$datarow = array_map("safe_export", $datarow);
$sql .= "INSERT INTO " . $exporttable . " (" . implode(",", $columns) . ") VALUES (" . implode(",", $datarow) . ");\n";
}
$output = fopen($dumpfile, 'a');
fwrite($output, $sql);
fclose($output);
}
if ($separatesql) {
$zip->addFile($dumpfile, "mysql/" . $exporttable . ".sql");
}
}
if (!$separatesql) {
$zip->addFile($dumpfile, "mysql/resourcespace.sql");
}
$zip->close();
# Log this
log_activity($lang["exportdata"], LOG_CODE_DOWNLOADED);
debug("Job handler 'config_export' created zip download file {$dumpfile}");
if (file_exists($zipfile)) {
$download_url = $baseurl_short . "pages/download.php?userfile=" . $userref . "_" . $randstring . ".zip";
$message = $lang["exportcomplete"];
message_add($job["user"], $message, $download_url, 0);
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
$delete_job_data = array();
$delete_job_data["file"] = $zipfile;
$delete_date = date('Y-m-d H:i:s', time() + (60 * 60 * 24)); // Delete these after 1 day
$job_code = md5($zipfile);
job_queue_add("delete_file", $delete_job_data, "", $delete_date, "", "", $job_code);
$jobsuccess = true;
}
}
if (!$jobsuccess) {
// Job failed, update job queue
job_queue_update($jobref, $job_data, STATUS_ERROR);
$message = $lang["exportfailed"];
message_add($job["user"], $message, "", 0);
}
unlink($dumpfile);

View File

@@ -0,0 +1,82 @@
<?php
/*
Runs a job to create an alternative file from the specified command
Requires the following job data:-
$job_data["resource"] - Resource ID
$job_data["alt_name"] - name of alternative file
$job_data["alt_description"] - description of alternative file
$job_data["alt_extension"] - extension of alternative file
$job_data["command"] - command to create the file, must have %%TARGETFILE%%" as a placeholder since we haven't created the ID alternative file yet
e.g.
'/usr/bin/ffmpeg' -i /var/www_rs/include/../filestore/1/8/0/0/4/0_eb6ff5c241414l/110040_c246d960fbde50e.mp4 -t 11 -i /var/www/include/../filestore/1/8/0/0/4/0_eb6ff5c241414l/180040_alt_537_d5029d98370c888.mp3 -map 0:v -map 1:a -vf subtitles=/var/www_rs/include/../filestore/1/8/0/0/4/0_eb6ff5c241414l/180040_alt_533_dedf5ed7d156119.srt %%TARGETFILE%%
*/
include_once __DIR__ . "/../image_processing.php";
global $filename_field, $offline_job_prefixes;
$jobsuccess = false;
$job_cmd_ok = false;
$resource = get_resource_data($job_data["resource"]);
$origfilename = get_data_by_field($job_data["resource"], $filename_field);
$randstring = md5(rand() . microtime());
$newaltfile = add_alternative_file($job_data["resource"], $job_data["alt_name"], $job_data["alt_description"], str_replace("." . $resource["file_extension"], "." . $job_data["alt_extension"], $origfilename), $job_data["alt_extension"]);
$targetfile = get_resource_path($job_data["resource"], true, "", false, $job_data["alt_extension"], -1, 1, false, "", $newaltfile);
$shell_exec_cmd = $job_data["command"];
$shell_exec_params = $job_data["command_params"];
$shell_exec_params[$job_data["output_file_placeholder"]] = $targetfile;
// Check we are using a whitelisted command path to create file
foreach ($offline_job_prefixes as $offline_job_prefix) {
$cmd_path = get_utility_path($offline_job_prefix);
if (substr($shell_exec_cmd, 0, strlen($cmd_path)) == $cmd_path) {
$job_cmd_ok = true;
break;
}
}
// Skip if any other unwanted characters in command (|,<,>,!,&,#,; or `)
if ($job_cmd_ok && !preg_match("/(\||<|>|;|!|&|#|`)/i", $shell_exec_cmd)) {
global $config_windows;
if ($config_windows) {
$shell_exec_cmd = str_replace(array_keys($shell_exec_params), array_values($shell_exec_params), $shell_exec_cmd);
file_put_contents(get_temp_dir() . "/create_alt_" . $randstring . ".bat", $shell_exec_cmd);
echo "Running command " . $shell_exec_cmd . PHP_EOL;
$shell_exec_cmd = get_temp_dir() . "/create_alt_" . $randstring . ".bat";
$shell_exec_params = [];
$deletebat = true;
}
$output = run_command($shell_exec_cmd, false, $shell_exec_params);
if (file_exists($targetfile)) {
$newfilesize = filesize_unlimited($targetfile);
ps_query("update resource_alt_files set file_size = ? where resource = ? and ref = ?", array("i", $newfilesize, "i", $job_data["resource"], "i", $newaltfile));
global $alternative_file_previews, $lang, $baseurl, $view_title_field, $offline_job_delete_completed;
if ($alternative_file_previews) {
create_previews($job_data["resource"], false, $job_data["alt_extension"], false, false, $newaltfile);
}
$message = ($job_success_text != "") ? $job_success_text : $lang["alternative_file_created"] . ": " . str_replace(array('%ref','%title'), array($job_data['resource'],$resource['field' . $view_title_field]), $lang["ref-title"]) . "(" . $job_data["alt_name"] . "," . $job_data["alt_description"] . ")";
message_add($job["user"], $message, $GLOBALS["baseurl_short"] . "?r=" . $job_data["resource"], 0);
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
$jobsuccess = true;
}
if (isset($deletebat) && file_exists($shell_exec_cmd)) {
unlink($shell_exec_cmd);
}
}
if (!$jobsuccess) {
// Job failed, upate job queue
job_queue_update($jobref, $job_data, STATUS_ERROR);
$message = ($job_success_text != "") ? $job_success_text : $lang["alternative_file_creation_failed"] . ": " . str_replace(array('%ref','%title'), array($job_data['resource'],$resource['field' . $view_title_field]), $lang["ref-title"]) . "(" . $job_data["alt_name"] . "," . $job_data["alt_description"] . ")";
message_add($job["user"], $message, $baseurl_short . "?r=" . $job_data["resource"], 0);
}

View File

@@ -0,0 +1,81 @@
<?php
/*
Run a command that will create an output file, optionally specifying a download URL that can be sent to the user
Requires the following:-
$job_data["resource"] - Resource ID
$job_data["title"] - Download title/description
$job_data["command"] - command to run
$job_data["outputfile"] - target output file
$job_data["lifetime"] - [optional] length of time for which file will be available before automatic deletion
$job_data["url"] - [optional] URL to send to the user
If a file is to be created for only a specific user to download you can create a random string e.g. $randomstring and set the path and url for the job as below:-
$job_data["outputfile"] = get_temp_dir(false,'user_downloads') . "/" . $ref . "_" . md5($username . $randomstring . $scramble_key) . ".<file extension here>";
$job_data["url"]=$baseurl . "/pages/download.php?userfile=" . $ref . "_" . $randomstring . ".<file extension here>;
*/
include_once __DIR__ . "/../image_processing.php";
global $config_windows,$baseurl, $baseurl_short, $offline_job_prefixes;
$jobsuccess = false;
$job_cmd_ok = false;
$shell_exec_cmd = $job_data["command"];
$shell_exec_params = $job_data["command_params"];
$shell_exec_params[$job_data["output_file_placeholder"]] = $job_data["outputfile"];
// Check we are using a whitelisted command path to create file
foreach ($offline_job_prefixes as $offline_job_prefix) {
$cmd_path = get_utility_path($offline_job_prefix);
if (substr($shell_exec_cmd, 0, strlen($cmd_path)) == $cmd_path) {
$job_cmd_ok = true;
break;
}
}
// Skip if any other unwanted characters in command (|,<,>,!,&,#,; or `)
if ($job_cmd_ok && !preg_match("/(\||<|>|;|!|&|#|`)/i", $shell_exec_cmd)) {
if ($config_windows) {
$shell_exec_cmd = str_replace(array_keys($shell_exec_params), array_values($shell_exec_params), $shell_exec_cmd);
file_put_contents(get_temp_dir() . "/create_download_" . $randstring . ".bat", $shell_exec_cmd);
$shell_exec_cmd = get_temp_dir() . "/create_download_" . $randstring . ".bat";
$shell_exec_params = [];
$deletebat = true;
}
$output = run_command($shell_exec_cmd, false, $shell_exec_params);
if (file_exists($job_data["outputfile"])) {
global $lang, $baseurl, $offline_job_delete_completed;
$url = (isset($job_data["url"])) ? $job_data["url"] : (isset($job_data["resource"]) ? $baseurl_short . "?r=" . $job_data["resource"] : "");
$message = $job_success_text != "" ? $job_success_text : $lang["download_file_created"] . ": " . str_replace(array('%ref','%title'), array($job_data['resource'],$resource['field' . $view_title_field]), $lang["ref-title"]) . "(" . $job_data["alt_name"] . "," . $job_data["alt_description"] . ")";
message_add($job["user"], $message, $url, 0);
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
if (isset($job_data["lifetime"])) {
$delete_job_data = array();
$delete_job_data["file"] = $job_data["outputfile"];
$delete_date = date('Y-m-d H:i:s', time() + (60 * 60 * 24 * DOWNLOAD_FILE_LIFETIME));
$job_code = md5($job_data["outputfile"]);
job_queue_add("delete_file", $delete_job_data, "", $delete_date, "", "", $job_code);
}
$jobsuccess = true;
}
if (isset($deletebat) && file_exists($shell_exec_cmd)) {
unlink($shell_exec_cmd);
}
}
if (!$jobsuccess) {
// Job failed, update job queue
job_queue_update($jobref, $job_data, STATUS_ERROR);
$message = $job_failure_text != "" ? $job_failure_text : $lang["download_file_creation_failed"] . ": " . str_replace(array('%ref','%title'), array($job_data['resource'],$resource['field' . $view_title_field]), $lang["ref-title"]) . "(" . $job_data["alt_name"] . "," . $job_data["alt_description"] . ")";
$url = $baseurl_short . "?r=" . $job_data["resource"];
message_add($job["user"], $message, $url, 0);
}

View File

@@ -0,0 +1,81 @@
<?php
/*
Job handler for creating previews for a resource/ alternative
Requires the following job data:-
$job_data['resource'] - Resource ID
$job_data['thumbonly'] - Optional
$job_data['extension'] - Optional
$job_data['previewonly'] - Optional
$job_data['previewbased'] - Optional
$job_data['alternative'] - Optional
$job_data['ignoremaxsize'] - Optional
$job_data['ingested'] - Optional
$job_data['checksum_required'] - Optional
*/
include_once __DIR__ . '/../image_processing.php';
global $lang, $baseurl, $offline_job_delete_completed, $baseurl_short;
$resource = $job_data["resource"] ?? 0;
$thumbonly = $job_data["thumbonly"] ?? false;
$extension = $job_data["extension"] ?? 'jpg';
$previewonly = $job_data["previewonly"] ?? false;
$previewbased = $job_data["previewbased"] ?? false;
$alternative = $job_data["alternative"] ?? -1;
$ignoremaxsize = $job_data["ignoremaxsize"] ?? true;
$ingested = $job_data["ingested"] ?? false;
$checksum_required = $job_data["checksum_required"] ?? true;
// For messages
$url = isset($job_data['resource']) ? "{$baseurl_short}?r={$job_data['resource']}" : '';
$resdata = get_resource_data($resource);
if (!$resdata) {
job_queue_update($jobref, $job_data, STATUS_DISABLED);
return;
}
if ($resource > 0) {
delete_previews($resource);
}
if ($resource > 0 && create_previews($resource, $thumbonly, $extension, $previewonly, $previewbased, $alternative, $ignoremaxsize, $ingested, $checksum_required)) {
// Success - no message required
update_disk_usage($resource);
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
} else {
// Fail
$preview_attempts = $resdata["preview_attempts"];
if ($preview_attempts < SYSTEM_MAX_PREVIEW_ATTEMPTS) {
// Reschedule job to try again later in the event that 3rd party processing has failed e.g. Unoserver
// Increase gap between subsequent attempts
$retry_date = date('Y-m-d H:i:s', time() + (60 * 60 * pow(2, $preview_attempts + 1)));
job_queue_update($jobref, $job_data, STATUS_ACTIVE, $retry_date);
} else {
job_queue_update($jobref, $job_data, STATUS_ERROR);
get_config_option(['user' => $job['user']], 'user_pref_resource_notifications', $send_notifications, false);
if ($send_notifications) {
$create_previews_job_failure_text = str_replace('%RESOURCE', $resource, $lang['jq_create_previews_failure_text']);
$message = $job["failure_text"] != '' ? $job["failure_text"] : $create_previews_job_failure_text;
message_add($job['user'], $message, $url, 0);
}
}
}
unset(
$resource,
$thumbonly,
$extension,
$previewonly,
$previewbased,
$alternative,
$ignoremaxsize,
$ingested,
$checksum_required
);

View File

@@ -0,0 +1,63 @@
<?php
/*
Job handler to process CSV metadata download for result set
Requires the following job data:
$job_data["personaldata"] - (bool) Only include fields marked as likely to contains personal data?
$job_data["allavailable"] - (bool) Include data from all fields?
$job_data["exportresources"] - (array) List of resources to export
*/
include_once __DIR__ . '/../csv_export_functions.php';
global $lang, $baseurl_short, $offline_job_delete_completed, $scramble_key, $userref, $username;
$personaldata = $job_data["personaldata"];
$allavailable = $job_data["allavailable"];
$exportresources = $job_data["exportresources"];
$search = $job_data["search"];
$restypes = $job_data["restypes"];
$archive = $job_data["archive"];
$access = $job_data["access"];
$sort = $job_data["sort"];
// Set up the user who requested the metadata download as it needs to be processed with their access
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.ref = ?";
$user_select_sql->parameters = ["i",$job['user']];
$user_data = validate_user($user_select_sql, true);
if (count($user_data) > 0) {
setup_user($user_data[0]);
} else {
job_queue_update($jobref, $job_data, STATUS_ERROR);
return;
}
$randstring = md5(json_encode($job_data));
$csvfile = get_temp_dir(false, 'user_downloads') . "/" . $userref . "_" . md5($username . $randstring . $scramble_key) . ".csv";
$findstrings = array("[search]","[time]");
$replacestrings = array(mb_substr(safe_file_name($search), 0, 150), date("Ymd-H:i", time()));
$csv_filename = str_replace($findstrings, $replacestrings, $lang["csv_export_filename"]);
$csv_filename_noext = strip_extension($csv_filename);
$csvurl = $baseurl_short . "pages/download.php?userfile=" . $userref . "_" . $randstring . ".csv&filename=" . $csv_filename_noext;
generateResourcesMetadataCSV($exportresources, $personaldata, $allavailable, $csvfile);
log_activity($lang['csvExportResultsMetadata'], LOG_CODE_DOWNLOADED);
debug("Job handler 'csv_metadata_export' created zip download file {$csv_filename}");
$jobsuccess = true;
message_add($job["user"], $job_success_text, $csvurl);
$delete_job_data = array();
$delete_job_data["file"] = $csvfile;
$delete_date = date('Y-m-d H:i:s', time() + (60 * 60 * 24 * DOWNLOAD_FILE_LIFETIME)); // Delete file after set number of days
$job_code = md5($csvfile);
job_queue_add("delete_file", $delete_job_data, "", $delete_date, "", "", $job_code);

View File

@@ -0,0 +1,14 @@
<?php
/*
Delete a file
$job_data["file"] - full patch of file to delete
*/
if (isset($job_data["file"]) && file_exists($job_data["file"])) {
unlink($job_data["file"]);
job_queue_delete($jobref);
} else {
// Job failed, upate job queue
job_queue_update($jobref, $job_data, STATUS_ERROR);
}

View File

@@ -0,0 +1,28 @@
<?php
/*
Job handler to process collection downloads
Requires the following job data:
$job_data['ref'] - Resource ref
$job_data['extension'] - File extension
$job_data['path'] - Path can be set to use an alternate file, for example, in the case of unoconv
*/
include_once __DIR__ . '/../image_processing.php';
global $offline_job_delete_completed;
$ref = $job_data["ref"];
$extension = $job_data["extension"];
$path = $job_data["path"] ?? '';
extract_text($ref, $extension, $path);
// May be needed elsewhere in the code further up
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
unset($ref, $extension, $path);

View File

@@ -0,0 +1,192 @@
<?php
/*
Job handler to fetch files from a local path for batch replacement of resources
Requires the following job data:-
$job_data['import_path'] - Folder to scan for files to import
*/
global $lang, $baseurl_short, $offline_job_delete_completed, $fstemplate_alt_threshold;
global $notify_on_resource_change_days, $replace_batch_existing;
$local_path = $job_data['import_path'];
$minref = $job_data['batch_replace_min'];
$maxref = $job_data['batch_replace_max'];
$collectionid = $job_data['batch_replace_col'];
$filename_field = $job_data['filename_field'];
$no_exif = ($job_data['no_exif'] == "yes") ? true : false ;
if (!file_exists($local_path)) {
job_queue_update($jobref, $job_data, STATUS_ERROR);
}
$logtext = array();
include_once __DIR__ . '/../image_processing.php';
if (!isset($collectionid) || $collectionid == 0) {
$conditions = array();
$minref = max((int)($minref), $fstemplate_alt_threshold);
$firstref = max($fstemplate_alt_threshold, $minref);
$sql_params = array("i", $minref);
$sql_condition = "";
if ($maxref > 0) {
$sql_condition = " AND ref <= ?";
$sql_params = array_merge($sql_params, array("i", (int)$maxref));
}
$replace_resources = ps_array("SELECT ref value FROM resource WHERE ref >= ? " . $sql_condition . " ORDER BY ref ASC", $sql_params, 0);
$logtext[] = "Replacing files for resource IDs. Min ID: " . $minref . (($maxref > 0) ? " Max ID: " . $maxref : "");
} else {
$replace_resources = get_collection_resources($collectionid);
$logtext[] = "Replacing resources within collection " . $collectionid . " only";
}
$logtext[] = "Embedded (EXIF) data extraction: " . ($no_exif ? "OFF" : "ON");
$replaced = array();
$errors = array();
$foldercontents = new DirectoryIterator($local_path);
foreach ($foldercontents as $objectindex => $object) {
if ($object->isDot() || $object->isDir() || !($object->isReadable())) {
continue;
}
$filename = $object->getFilename();
$extension = $object->getExtension();
$full_path = $local_path . DIRECTORY_SEPARATOR . $filename;
// get resource by $filename_field
if ($filename_field != 0) {
$target_resources = ps_array("SELECT resource value FROM resource_node rn LEFT JOIN node n ON n.ref = rn.node WHERE n.resource_type_field = ? AND n.name = ?", ["i", $filename_field, "s", $filename], "");
$valid_resources = array_values(array_intersect($target_resources, $replace_resources));
if (count($valid_resources) == 1) {
$valid_resource = $valid_resources[0];
// A single resource has been found with the same filename
$rsfile = get_resource_path($valid_resource, true, '', true, $extension);
try {
copy($full_path, $rsfile);
$success = filesize_unlimited($full_path) == filesize_unlimited($rsfile);
} catch (Exception $e) {
$success = false;
$errors[] = "ERROR - Copy operation failed for {$full_path} : " . $e->getMessage();
}
if ($success) {
ps_query("update resource set file_extension = lower(?) where ref = ?", array("s", $extension, "i", $valid_resource));
resource_log($valid_resource, "u", 0);
if (!$no_exif) {
extract_exif_comment($valid_resource, $extension);
}
create_previews($valid_resource, false, $extension);
if ($notify_on_resource_change_days != 0) {
notify_resource_change($valid_resource);
}
$replaced[] = $valid_resource;
try_unlink($full_path);
} else {
$errors[] = "Failed to copy file from : " . $full_path;
}
} elseif (count($valid_resources) == 0) {
// No resource found with the same filename
$errors[] = "Failed to find matching file for: " . $filename;
} else {
// Multiple resources found with the same filename
if ($replace_batch_existing) {
$copy_error = false;
foreach ($valid_resources as $valid_resource) {
$rsfile = get_resource_path($valid_resource, true, '', true, $extension);
try {
copy($full_path, $rsfile);
$success = filesize_unlimited($full_path) == filesize_unlimited($rsfile);
} catch (Exception $e) {
$success = false;
$errors[] = "ERROR - Copy operation failed for {$full_path} : " . $e->getMessage();
}
if ($success) {
ps_query("update resource set file_extension = lower(?) where ref = ?", array("s", $extension, "i", $valid_resource));
resource_log($valid_resource, "u", 0);
if (!$no_exif) {
extract_exif_comment($valid_resource, $extension);
}
create_previews($valid_resource, false, $extension);
if ($notify_on_resource_change_days != 0) {
notify_resource_change($valid_resource);
}
$replaced[] = $valid_resource;
} else {
$errors[] = "Failed to copy file from : " . $full_path;
$copy_error = true;
}
}
// Attempt to delete
if (!$copy_error) {
try_unlink($full_path);
}
} else {
// Multiple resources found with the same filename
$resourcelist = implode(",", $valid_resources);
$errors[] = "ERROR - multiple resources found with filename '" . $filename . "'. Resource IDs : " . $resourcelist;
}
}
} else {
# Overwrite an existing resource using the number from the filename.
$targetresource = $object->getBasename("." . $extension);
if ((string)(int)($targetresource) == (string)$targetresource && in_array($targetresource, $replace_resources) && !resource_file_readonly($targetresource)) {
$rsfile = get_resource_path($targetresource, true, '', true, $extension);
try {
copy($full_path, $rsfile);
$success = filesize_unlimited($full_path) == filesize_unlimited($rsfile);
} catch (Exception $e) {
$success = false;
$errors[] = "ERROR - Copy operation failed for {$full_path} : " . $e->getMessage();
}
if ($success) {
ps_query("update resource set file_extension = lower(?) where ref = ?", array("s", $extension, "i", $targetresource));
resource_log($targetresource, "u", 0);
if (!$no_exif) {
extract_exif_comment($targetresource, $extension);
}
create_previews($targetresource, false, $extension);
if ($notify_on_resource_change_days != 0) {
notify_resource_change($targetresource);
}
$replaced[] = $targetresource;
unlink($full_path);
} else {
$errors[] = "Failed to copy file from : " . $full_path;
}
} else {
// No resource found with the same filename
$errors[] = "ERROR - no ref matching filename: '" . $filename . "', id: " . $targetresource;
}
}
}
$logtext[] = "Replaced " . count($replaced) . " resource files: -";
if (count($replaced) > 0) {
$logtext[] = "Replaced resource files for IDs:";
$logtext[] = implode(",", $replaced);
}
if (count($errors) > 0) {
$logtext[] = "ERRORS: -";
$logtext = array_merge($logtext, $errors);
job_queue_update($jobref, $job_data, STATUS_ERROR);
} else {
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
$jobsuccess = true;
}
echo " --> " . implode("\n --> ", $logtext) . "\n";
message_add($job["user"], implode("<br />", $logtext), (count($replaced) > 0) ? $baseurl_short . "pages/search.php?search=!list" . implode(":", $replaced) : "");

View File

@@ -0,0 +1,38 @@
<?php
include_once __DIR__ . '/../image_processing.php';
# $job_data["r"]
# $job_data["title"]
# $job_data["ingest"]
# $job_data["createPreviews"]
# $job_data["archive"] -> optional based on $upload_then_process_holding_state
global $baseurl, $offline_job_delete_completed,$baseurl_short;
$resource = get_resource_data($job_data["r"]);
$status = false;
if ($resource !== false) {
$status = update_resource($job_data["r"], $resource['file_path'], $resource['resource_type'], $job_data["title"], $job_data["ingest"], $job_data["createPreviews"], $resource['file_extension'], true);
# update the archive status
if (isset($job_data['archive']) && $job_data['archive'] !== '') {
update_archive_status($job_data["resource"], $job_data["archive"]);
}
}
$url = isset($job_data['r']) ? $baseurl_short . "?r=" . $job_data['r'] : '';
if ($status === false) {
# fail
message_add($job['user'], $job_failure_text, $url, 0);
job_queue_update($jobref, $job_data, STATUS_ERROR);
} else {
# only delete the job if completed successfully;
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
}

View File

@@ -0,0 +1,115 @@
<?php
include_once __DIR__ . '/../image_processing.php';
# $job_data["resource"]
# $job_data["extract"] -> Should the embedded metadata be extracted during this process? Please note that this is used
# for the no_exif param where false means to extract metadata!
# $job_data["revert"]
# $job_data["autorotate"]
# $job_data["archive"] -> optional based on $upload_then_process_holding_state
# $job_data["upload_file_by_url"] -> optional. If NOT empty, means upload_file_by_url should be used instead
# $job_data["alternative"] -> optional. If alternative ID is passed, then process upload for the alternative
# $job_data["extension"] -> optional. Used for alternative uploads.
# $job_data["file_path"] -> optional. Used for alternative uploads.
$upload_file_by_url = isset($job_data["upload_file_by_url"]) && is_string($job_data["upload_file_by_url"]) ? trim($job_data["upload_file_by_url"]) : "";
$alternative = isset($job_data["alternative"]) && is_int($job_data["alternative"]) ? $job_data["alternative"] : null;
$extension = isset($job_data["extension"]) && is_string($job_data["extension"]) ? trim($job_data["extension"]) : null;
$file_path = isset($job_data["file_path"]) && is_string($job_data["file_path"]) ? trim($job_data["file_path"]) : null;
// Set up the user who triggered this event - the upload should be done as them
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.ref = ?";
$user_select_sql->parameters = ["i",$job['user']];
$user_data = validate_user($user_select_sql, true);
if (!is_array($user_data) || count($user_data) == 0) {
job_queue_update($jobref, $job_data, STATUS_ERROR);
return;
}
setup_user($user_data[0]);
$resource = get_resource_data($job_data["resource"]);
$status = false;
// Process a resource upload
if ($resource !== false && is_null($alternative)) {
if ($upload_file_by_url != "") {
$status = upload_file_by_url(
$job_data["resource"],
!$job_data["extract"],
$job_data["revert"],
$job_data["autorotate"],
$job_data["upload_file_by_url"]
);
} else {
$status = upload_file($job_data["resource"], !$job_data["extract"], $job_data["revert"], $job_data["autorotate"], "", true);
}
# update the archive status
if (isset($job_data['archive']) && $job_data['archive'] !== '') {
update_archive_status($job_data["resource"], $job_data["archive"]);
}
}
// Process a resource alternative upload
elseif ($resource !== false && !is_null($alternative) && $alternative > 0 && $extension != "") {
$alt_path = get_resource_path($job_data["resource"], true, "", true, $extension, -1, 1, false, "", $alternative);
if (is_null($file_path) && $upload_file_by_url != "") {
$tmp_file_path = temp_local_download_remote_file(
$upload_file_by_url,
uniqid("{$job_data['resource']}_{$alternative}_")
);
$file_to_upload = new SplFileInfo($tmp_file_path ?: '');
} elseif (!is_null($file_path) && $file_path != "") {
$file_to_upload = new SplFileInfo($file_path);
} else {
$file_to_upload = new SplFileInfo('');
}
// Move the provided file to the alternative file location
$process_file_upload = process_file_upload(
new SplFileInfo($file_to_upload),
new SplFileInfo($alt_path),
['mime_file_based_detection' => false]
);
if ($process_file_upload['success']) {
chmod($alt_path, 0777);
if (isset($job_data['autorotate']) && $job_data['autorotate']) {
AutoRotateImage($alt_path);
}
if ($GLOBALS['alternative_file_previews']) {
create_previews($job_data['resource'], false, $extension, false, false, $alternative);
}
update_disk_usage($job_data['resource']);
$status = true;
} else {
$job_failure_text .= $process_file_upload['error']->i18n($GLOBALS['lang']);
}
}
global $baseurl, $offline_job_delete_completed, $baseurl_short;
$url = isset($job_data['resource']) ? $baseurl_short . "?r=" . $job_data['resource'] : '';
if ($status === false) {
# fail
message_add($job['user'], $job_failure_text, $url, 0);
job_queue_update($jobref, $job_data, STATUS_ERROR);
} else {
# success
message_add($job['user'], $job_success_text, $url, 0);
# only delete the job if completed successfully;
if ($offline_job_delete_completed) {
job_queue_delete($jobref);
} else {
job_queue_update($jobref, $job_data, STATUS_COMPLETE);
}
}

View File

@@ -0,0 +1,108 @@
<?php
global $k;
$k = $k ?? "";
?>
<script type="text/javascript">
jQuery(document).ready(function() {
jQuery.fn.reverse = [].reverse;
jQuery(document).keyup(function (e)
{
if(jQuery("input,textarea").is(":focus"))
{
// don't listen to keyboard arrows when focused on form elements
<?php hook("keyboardnavtextfocus");?>
}
else if (jQuery('#lightbox').is(':visible'))
{
// Don't listen to keyboard arrows if viewing resources in lightbox
}
else
{
var share='<?php echo escape($k) ?>';
var modAlt=e.altKey;
var modShift=e.shiftKey;
var modCtrl=e.ctrlKey;
var modMeta=e.metaKey;
var modOn=(modAlt || modShift || modCtrl || modMeta);
switch (e.which)
{
<?php hook("addhotkeys"); //this comes first so overriding the below is possible ?>
// Left arrow
case <?php echo $keyboard_navigation_prev; ?>:
if (jQuery('.prevLink').length > 0) {
jQuery('.prevLink').click();
break;
}
if (<?php if ($keyboard_navigation_pages_use_alt) {
echo "modAlt&&";
} ?>(jQuery('.prevPageLink').length > 0)) {
jQuery('.prevPageLink').click();
break;
}
// Right arrow
case <?php echo $keyboard_navigation_next; ?>:
if (jQuery('.nextLink').length > 0) {
jQuery('.nextLink').click();
break;
}
if (<?php if ($keyboard_navigation_pages_use_alt) {
echo "modAlt&&";
} ?>(jQuery('.nextPageLink').length > 0)) {
jQuery('.nextPageLink').click();
break;
}
case <?php echo $keyboard_navigation_add_resource; ?>: if (jQuery('.addToCollection').length > 0) jQuery('.addToCollection:not(.ResourcePanelIcons .addToCollection)').click();
break;
case <?php echo $keyboard_navigation_prev_page; ?>: if (jQuery('.prevLink').length > 0) jQuery('.prevLink').click();
break;
case <?php echo $keyboard_navigation_next_page; ?>: if (jQuery('.nextLink').length > 0) jQuery('.nextLink').click();
break;
case <?php echo $keyboard_navigation_all_results; ?>: if (jQuery('.upLink').length > 0) jQuery('.upLink').click();
break;
case <?php echo $keyboard_navigation_toggle_thumbnails; ?>: if (jQuery('#toggleThumbsLink').length > 0) jQuery('#toggleThumbsLink').click();
break;
case <?php echo $keyboard_navigation_zoom; ?>: if (jQuery('.enterLink').length > 0) window.location=jQuery('.enterLink').attr("href");
break;
case <?php echo $keyboard_navigation_close; ?>: ModalClose();
break;
case <?php echo $keyboard_navigation_view_all; ?>: if(!modOn){CentralSpaceLoad('<?php echo $baseurl;?>/pages/search.php?search=!collection'+document.getElementById("currentusercollection").innerHTML+'&k='+share,true)};
break;
<?php if (($pagename == 'search' && $keyboard_navigation_video_search) || ($pagename == 'view' && $keyboard_navigation_video_view) || ($pagename == 'preview' && $keyboard_navigation_video_preview)) {?>
case <?php echo $keyboard_navigation_video_search_play_pause?>:
<?php if ($pagename == 'view' || $pagename == 'preview') { ?>
vidActive=document.getElementById('introvideo<?php echo $ref?>');
<?php } else { ?>
vidActive=document.getElementById('introvideo'+vidActiveRef);
<?php } ?>
//console.log("active="+vidActive);
videoPlayPause(vidActive);
break;
case <?php echo $keyboard_navigation_video_search_forwards?>:
//console.log("forward button pressed");
//console.log("Player is "+vidActive);
// clear
clearInterval(intervalRewind);
// get current playback rate
curPlayback=vidActive.playbackRate();
//console.log("Current playback rate is "+curPlayback);
if(playback=='forward'){
newPlayback=curPlayback+1;
}
else{
newPlayback=1;
}
playback='forward';
//console.log("New playback rate is "+newPlayback);
vidActive.playbackRate(newPlayback);
break;
<?php } ?>
}
}
});
});
</script>

835
include/language_functions.php Executable file
View File

@@ -0,0 +1,835 @@
<?php
# Language functions
# Functions for the translation of the application
/**
* Translates field names / values using two methods: First it checks if $text exists in the current $lang (after $text is sanitized and $mixedprefix - one by one if an array - and $suffix are added). If not found in the $lang, it tries to translate $text using the i18n_get_translated function.
*
* @param string $text
* @param string $mixedprefix
* @param string $suffix
* @return string
*/
function lang_or_i18n_get_translated($text, $mixedprefix, $suffix = "")
{
$text = trim((string) $text);
global $lang;
if (is_array($mixedprefix)) {
$prefix = $mixedprefix;
} else {
$prefix = array($mixedprefix);
}
for ($n = 0; $n < count($prefix); $n++) {
$langindex = $prefix[$n] . strip_tags(strtolower(str_replace(array(", ", " ", "\t", "/", "(", ")"), array("-", "_", "_", "and", "", ""), $text))) . $suffix;
# Checks if there is a $lang (should be defined for all standard field names / values).
if (isset($lang[$langindex])) {
$return = $lang[$langindex];
break;
}
}
if (isset($return)) {
return $return;
} else {
return i18n_get_translated($text);
} # Performs an i18n translation (of probably a custom field name / value).
}
/**
* For field names / values using the i18n syntax, return the version in the current user's language. Format is ~en:Somename~es:Someothername
*
* @param string|null $text
* @param string|null $lang_domain As an optional fallback if an i18n string is not provided, check the system language translations for a matching translation instead.
* @return string
*/
function i18n_get_translated($text, $lang_domain = null)
{
$text ??= '';
global $language,$defaultlanguage,$lang;
$asdefaultlanguage = $defaultlanguage;
if (!isset($asdefaultlanguage)) {
$asdefaultlanguage = 'en';
}
# Split
$s = explode("~", $text);
# Not a translatable field?
if (count($s) < 2) {
if (!is_null($lang_domain)) {
# Search the provided $lang domain for a match and return that if present.
$key = $lang_domain . "-" . preg_replace("/[^a-z0-9\-]/", '', strtolower(str_replace(" ", "-", $text)));
if (array_key_exists($key, $lang)) {
return $lang[$key];
}
}
return $text;
}
# Find the current language and return it
$default = "";
for ($n = 1; $n < count($s); $n++) {
# Not a translated string, return as-is
if (substr($s[$n], 2, 1) != ":" && substr($s[$n], 5, 1) != ":" && substr($s[$n], 0, 1) != ":") {
return $text;
}
# Support both 2 character and 5 character language codes (for example en, en-US).
$p = strpos($s[$n], ':');
$textLanguage = substr($s[$n], 0, $p);
if (strtolower($textLanguage) == strtolower($language)) {
return substr($s[$n], $p + 1);
}
if (strtolower($textLanguage) == strtolower($asdefaultlanguage) || $p == 0 || $n == 1) {
$default = substr($s[$n], $p + 1);
}
}
# Translation not found? Return default language
# No default language entry? Then consider this a broken language string and return the string unprocessed.
if ($default != "") {
return $default;
} else {
return $text;
}
}
/**
* Translates collection names
*
* @param mixed $mixedcollection
* @param string $index
* @return string
*/
function i18n_get_collection_name($mixedcollection, $index = "name")
{
global $lang;
# The function handles both strings and arrays.
if (!is_array($mixedcollection)) {
$name_untranslated = $mixedcollection ?? "";
} else {
$name_untranslated = $mixedcollection[$index] ?? "";
# Check if it is a Smart Collection
if (isset($mixedcollection['savedsearch']) && ($mixedcollection['savedsearch'] != null)) {
return escape($lang['smartcollection'] . ": " . i18n_get_translated($name_untranslated));
}
}
# Check if it is a Default Collection (n)
$name_translated = preg_replace('/(^Default Collection)(|(\s\d+))$/', $lang["mycollection"] . '$2', $name_untranslated, -1, $translated);
if ($translated == 1) {
return escape($name_translated);
}
# Check if it is a Upload YYMMDDHHMMSS
$upload_date = preg_replace('/(^Upload)\s(\d{12})$/', '$2', $name_untranslated, -1, $translated);
if ($translated != 1) {
$upload_date = preg_replace('/(^Upload)\s(\d{14})$/', '$2', $name_untranslated, -1, $translated);
}
if ($translated == 1) {
# Translate date into MySQL ISO format to be able to use nicedate()
if (strlen($upload_date) == 14) {
$year = substr($upload_date, 0, 4);
$upload_date = substr($upload_date, 2);
} else {
$year = substr($upload_date, 0, 2);
if ((int)$year > (int)date('y')) {
$year = ((int)substr(date('Y'), 0, 2) - 1) . $year;
} else {
$year = substr(date('Y'), 0, 2) . $year;
}
}
$month = substr($upload_date, 2, 2);
$day = substr($upload_date, 4, 2);
$hour = substr($upload_date, 6, 2);
$minute = substr($upload_date, 8, 2);
$second = substr($upload_date, 10, 2);
$date = nicedate("$year-$month-$day $hour:$minute:$second", true);
return escape($lang['upload'] . ' ' . $date);
}
# Check if it is a Research Request
if (substr($name_untranslated, 0, 9) == "Research:") {
return escape("{$lang["research"]}: " . i18n_get_translated(substr($name_untranslated, 9)));
}
# Ordinary collection - translate with i18n_get_translated
return escape(i18n_get_translated($name_untranslated));
}
/**
* For field names / values using the i18n syntax, return all language versions, as necessary for indexing.
*
* @param string $text The text to process
* @return string
*/
function i18n_get_indexable($text)
{
// Make sure keywords don't get squashed together, then trim
$text = str_replace(array("<br />","<br>","\\r","\\n","&nbsp;"), " ", $text);
$text = trim($text);
$text = preg_replace('/~(.*?):/', ',', $text);// remove i18n strings, which shouldn't be in the keywords
# For multiple keywords, parse each keyword.
if (substr($text, 0, 1) != "," && (strpos($text, ",") !== false) && (strpos($text, "~") !== false)) {
$s = explode(",", $text);
$out = "";
for ($n = 0; $n < count($s); $n++) {
if ($n > 0) {
$out .= ",";
}
$out .= i18n_get_indexable(trim($s[$n]));
}
return $out;
}
# Split
$s = explode("~", $text);
# Not a translatable field?
if (count($s) < 2) {
return $text;
}
$out = "";
for ($n = 1; $n < count($s); $n++) {
if (substr($s[$n], 2, 1) != ":") {
return $text;
}
if ($out != "") {
$out .= ",";
}
$out .= substr($s[$n], 3);
}
return $out;
}
/**
* For a string in the language format, return all translations as an associative array
* E.g. "en"->"English translation";
* "fr"->"French translation"
*
* @param string $value A string in the language format
* @return array
*/
function i18n_get_translations($value)
{
#
global $defaultlanguage;
if (strpos($value, "~") === false) {
return array($defaultlanguage => $value);
}
$s = explode("~", $value);
$return = array();
for ($n = 1; $n < count($s); $n++) {
$e = explode(":", $s[$n], 2);
if (count($e) == 2 && strlen($e[0]) > 1) {
$return[$e[0]] = $e[1];
}
}
return $return;
}
/**
* Returns a string with all occurrences of the $mixedplaceholder in $subject replaced with the $mixedreplace.
* If $mixedplaceholder is a string but $mixedreplace is an array, the $mixedreplace is imploded to a string using $separator.
* The replace values are formatted according to the formatting of the placeholders.
* The placeholders may be written in UPPERCASE, lowercase or Uppercasefirst.
* Each placeholder will be replaced by the replace value,
* written with the same case as the placeholder.
* It's possible to also include "?" as a placeholder for legacy reasons.
*
* Example #1:
* str_replace_formatted_placeholder("%extension", $resource["file_extension"], $lang["originalfileoftype"], true)
* will search for the three words "%EXTENSION", "%extension" and "%Extension" and also the char "?"
* in the string $lang["originalfileoftype"]. If the found placeholder is %extension
* it will be replaced by the value of $resource["file_extension"],
* written in lowercase. If the found placeholder instead would have been "?" the value
* would have been written in UPPERCASE.
*
* Example #2:
* str_replace_formatted_placeholder("%resourcetypes%", $searched_resource_types_names_array,
* $lang["resourcetypes-collections"], false, $lang["resourcetypes_separator"])
* will search for the three words "%RESOURCETYPES%", "%resourcetypes%" and "%Resourcetypes%"
* in the string $lang["resourcetypes-collections"]. If the found placeholder is %resourcetypes%
* all elements in $searched_resource_types_names_array will be written in lowercase and separated by
* $lang["resourcetypes_separator"] before the resulting string will replace the placeholder.
*
* @param mixed $mixedplaceholder
* @param mixed $mixedreplace
* @param mixed $subject
* @param mixed $question_mark
* @param string $separator
* @return string
*/
function str_replace_formatted_placeholder($mixedplaceholder, $mixedreplace, $subject, $question_mark = false, $separator = ", ")
{
# Creates a multi-dimensional array of the placeholders written in different case styles.
$array_placeholder = array();
if (is_array($mixedplaceholder)) {
$placeholder = $mixedplaceholder;
} else {
$placeholder = array($mixedplaceholder);
}
for ($n = 0; $n < count($placeholder); $n++) {
$array_placeholder[$n] = array(strtoupper((string)$placeholder[$n]), strtolower((string)$placeholder[$n]), ucfirstletter((string)$placeholder[$n]));
}
# Creates a multi-dimensional array of the replace values written in different case styles.
if (is_array($mixedreplace)) {
$replace = $mixedreplace;
} else {
$replace = array($mixedreplace);
}
for ($n = 0; $n < count($replace); $n++) {
$array_replace[$n] = array(strtoupper((string)$replace[$n]), strtolower((string)$replace[$n]), ucfirst(strtolower((string)$replace[$n])));
}
# Adds "?" to the arrays if required.
if ($question_mark) {
$array_placeholder[] = "?";
$array_replace[] = strtoupper((string)$replace[0]);
}
# Replaces the placeholders with the replace values and returns the new string.
$result = $subject;
if (count($placeholder) == 1 && count($replace) > 1) {
# The placeholder shall be replaced by an imploded array.
$array_replace_strings = array(implode($separator, array_map(function ($column) {
return $column[0];
}, $array_replace)), implode($separator, array_map(function ($column) {
return $column[1];
}, $array_replace)), implode($separator, array_map(function ($column) {
return $column[2];
}, $array_replace)));
$result = str_replace($array_placeholder[0], $array_replace_strings, $result);
} else {
for ($n = 0; $n < count($placeholder); $n++) {
if (!isset($array_replace[$n][0])) {
break;
} else {
$result = str_replace($array_placeholder[$n], $array_replace[$n], $result);
}
}
}
return $result;
}
/**
* Returns a string with the first LETTER of $string capitalized.
* Compare with ucfirst($string) which returns a string with first CHAR of $string capitalized:
* ucfirstletter("abc") / ucfirstletter("%abc") returns "Abc" / "%Abc"
* ucfirst("abc") / ucfirst("%abc") returns "Abc" / "%abc"
*
* Search for the first letter ([a-zA-Z]), which may or may not be followed by other characters (.*).
* Replaces the found substring ('$0') with the same substring but now with the first character capitalized, using ucfirst().
* Note the /e modifier: If this modifier is set, preg_replace() does normal substitution of backreferences in the replacement
* string, evaluates it as PHP code, and uses the result for replacing the search string.
*
* @param mixed $string
* @return string|array|null
*/
function ucfirstletter($string)
{
return preg_replace_callback("/[a-zA-Z].*/", "ucfirstletter_callback", $string);
}
/**
* Callback used by ucfirstletter
*
* @param array $matches
* @return void
*/
function ucfirstletter_callback($matches)
{
return ucfirst($matches[0]);
}
/**
* Normalize the text if function available
*
* @param string $keyword Keyword to normalize
* @param bool $user_language Flag to enable normalizing based on the current user language.
* Some languages consider characters with accents to be different characters
* and therefore order them after z and other consider them to be the same as the
* character without the accent.
* See https://www.php.net/manual/en/class.normalizer.php for more information
* @return string Normalized keyword
*/
function normalize_keyword($keyword, bool $user_language = false)
{
global $keywords_remove_diacritics,$language_normalize_mapping;
if (function_exists('normalizer_normalize')) {
if ($user_language && key_exists($GLOBALS["language"], $language_normalize_mapping)) {
$form = $language_normalize_mapping[$GLOBALS["language"]];
} else {
$form = Normalizer::FORM_C;
}
$keyword = normalizer_normalize($keyword, $form);
}
if ($keywords_remove_diacritics) {
$keyword = remove_accents($keyword);
}
return $keyword;
}
/**
* This function and seems_utf8 are reused from WordPress. See documentation/licenses/wordpress.txt for license information
*
* Converts all accent characters to ASCII characters.
*
* If there are no accent characters, then the string given is just returned.
*
* @param string $string Text that might have accent characters
* @return string Filtered string with replaced "nice" characters.
*/
function remove_accents($string)
{
if (!preg_match('/[\x80-\xff]/', $string)) {
return $string;
}
if (seems_utf8($string)) {
$chars = array(
// Decompositions for Latin-1 Supplement
chr(195) . chr(128) => 'A', chr(195) . chr(129) => 'A',
chr(195) . chr(130) => 'A', chr(195) . chr(131) => 'A',
chr(195) . chr(132) => 'A', chr(195) . chr(133) => 'A',
chr(195) . chr(135) => 'C', chr(195) . chr(136) => 'E',
chr(195) . chr(137) => 'E', chr(195) . chr(138) => 'E',
chr(195) . chr(139) => 'E', chr(195) . chr(140) => 'I',
chr(195) . chr(141) => 'I', chr(195) . chr(142) => 'I',
chr(195) . chr(143) => 'I', chr(195) . chr(145) => 'N',
chr(195) . chr(146) => 'O', chr(195) . chr(147) => 'O',
chr(195) . chr(148) => 'O', chr(195) . chr(149) => 'O',
chr(195) . chr(150) => 'O', chr(195) . chr(153) => 'U',
chr(195) . chr(154) => 'U', chr(195) . chr(155) => 'U',
chr(195) . chr(156) => 'U', chr(195) . chr(157) => 'Y',
chr(195) . chr(159) => 's', chr(195) . chr(160) => 'a',
chr(195) . chr(161) => 'a', chr(195) . chr(162) => 'a',
chr(195) . chr(163) => 'a', chr(195) . chr(164) => 'a',
chr(195) . chr(165) => 'a', chr(195) . chr(167) => 'c',
chr(195) . chr(168) => 'e', chr(195) . chr(169) => 'e',
chr(195) . chr(170) => 'e', chr(195) . chr(171) => 'e',
chr(195) . chr(172) => 'i', chr(195) . chr(173) => 'i',
chr(195) . chr(174) => 'i', chr(195) . chr(175) => 'i',
chr(195) . chr(177) => 'n', chr(195) . chr(178) => 'o',
chr(195) . chr(179) => 'o', chr(195) . chr(180) => 'o',
chr(195) . chr(181) => 'o', chr(195) . chr(182) => 'o',
chr(195) . chr(182) => 'o', chr(195) . chr(185) => 'u',
chr(195) . chr(186) => 'u', chr(195) . chr(187) => 'u',
chr(195) . chr(188) => 'u', chr(195) . chr(189) => 'y',
chr(195) . chr(191) => 'y',
// Decompositions for Latin Extended-A
chr(196) . chr(128) => 'A', chr(196) . chr(129) => 'a',
chr(196) . chr(130) => 'A', chr(196) . chr(131) => 'a',
chr(196) . chr(132) => 'A', chr(196) . chr(133) => 'a',
chr(196) . chr(134) => 'C', chr(196) . chr(135) => 'c',
chr(196) . chr(136) => 'C', chr(196) . chr(137) => 'c',
chr(196) . chr(138) => 'C', chr(196) . chr(139) => 'c',
chr(196) . chr(140) => 'C', chr(196) . chr(141) => 'c',
chr(196) . chr(142) => 'D', chr(196) . chr(143) => 'd',
chr(196) . chr(144) => 'D', chr(196) . chr(145) => 'd',
chr(196) . chr(146) => 'E', chr(196) . chr(147) => 'e',
chr(196) . chr(148) => 'E', chr(196) . chr(149) => 'e',
chr(196) . chr(150) => 'E', chr(196) . chr(151) => 'e',
chr(196) . chr(152) => 'E', chr(196) . chr(153) => 'e',
chr(196) . chr(154) => 'E', chr(196) . chr(155) => 'e',
chr(196) . chr(156) => 'G', chr(196) . chr(157) => 'g',
chr(196) . chr(158) => 'G', chr(196) . chr(159) => 'g',
chr(196) . chr(160) => 'G', chr(196) . chr(161) => 'g',
chr(196) . chr(162) => 'G', chr(196) . chr(163) => 'g',
chr(196) . chr(164) => 'H', chr(196) . chr(165) => 'h',
chr(196) . chr(166) => 'H', chr(196) . chr(167) => 'h',
chr(196) . chr(168) => 'I', chr(196) . chr(169) => 'i',
chr(196) . chr(170) => 'I', chr(196) . chr(171) => 'i',
chr(196) . chr(172) => 'I', chr(196) . chr(173) => 'i',
chr(196) . chr(174) => 'I', chr(196) . chr(175) => 'i',
chr(196) . chr(176) => 'I', chr(196) . chr(177) => 'i',
chr(196) . chr(178) => 'IJ',chr(196) . chr(179) => 'ij',
chr(196) . chr(180) => 'J', chr(196) . chr(181) => 'j',
chr(196) . chr(182) => 'K', chr(196) . chr(183) => 'k',
chr(196) . chr(184) => 'k', chr(196) . chr(185) => 'L',
chr(196) . chr(186) => 'l', chr(196) . chr(187) => 'L',
chr(196) . chr(188) => 'l', chr(196) . chr(189) => 'L',
chr(196) . chr(190) => 'l', chr(196) . chr(191) => 'L',
chr(197) . chr(128) => 'l', chr(197) . chr(129) => 'L',
chr(197) . chr(130) => 'l', chr(197) . chr(131) => 'N',
chr(197) . chr(132) => 'n', chr(197) . chr(133) => 'N',
chr(197) . chr(134) => 'n', chr(197) . chr(135) => 'N',
chr(197) . chr(136) => 'n', chr(197) . chr(137) => 'N',
chr(197) . chr(138) => 'n', chr(197) . chr(139) => 'N',
chr(197) . chr(140) => 'O', chr(197) . chr(141) => 'o',
chr(197) . chr(142) => 'O', chr(197) . chr(143) => 'o',
chr(197) . chr(144) => 'O', chr(197) . chr(145) => 'o',
chr(197) . chr(146) => 'OE',chr(197) . chr(147) => 'oe',
chr(197) . chr(148) => 'R',chr(197) . chr(149) => 'r',
chr(197) . chr(150) => 'R',chr(197) . chr(151) => 'r',
chr(197) . chr(152) => 'R',chr(197) . chr(153) => 'r',
chr(197) . chr(154) => 'S',chr(197) . chr(155) => 's',
chr(197) . chr(156) => 'S',chr(197) . chr(157) => 's',
chr(197) . chr(158) => 'S',chr(197) . chr(159) => 's',
chr(197) . chr(160) => 'S', chr(197) . chr(161) => 's',
chr(197) . chr(162) => 'T', chr(197) . chr(163) => 't',
chr(197) . chr(164) => 'T', chr(197) . chr(165) => 't',
chr(197) . chr(166) => 'T', chr(197) . chr(167) => 't',
chr(197) . chr(168) => 'U', chr(197) . chr(169) => 'u',
chr(197) . chr(170) => 'U', chr(197) . chr(171) => 'u',
chr(197) . chr(172) => 'U', chr(197) . chr(173) => 'u',
chr(197) . chr(174) => 'U', chr(197) . chr(175) => 'u',
chr(197) . chr(176) => 'U', chr(197) . chr(177) => 'u',
chr(197) . chr(178) => 'U', chr(197) . chr(179) => 'u',
chr(197) . chr(180) => 'W', chr(197) . chr(181) => 'w',
chr(197) . chr(182) => 'Y', chr(197) . chr(183) => 'y',
chr(197) . chr(184) => 'Y', chr(197) . chr(185) => 'Z',
chr(197) . chr(186) => 'z', chr(197) . chr(187) => 'Z',
chr(197) . chr(188) => 'z', chr(197) . chr(189) => 'Z',
chr(197) . chr(190) => 'z', chr(197) . chr(191) => 's',
// Euro Sign
chr(226) . chr(130) . chr(172) => 'E',
// GBP (Pound) Sign
chr(194) . chr(163) => '');
$string = strtr($string, $chars);
} else {
// Assume ISO-8859-1 if not UTF-8
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)
. chr(195) . chr(196) . chr(197) . chr(199) . chr(200) . chr(201) . chr(202)
. chr(203) . chr(204) . chr(205) . chr(206) . chr(207) . chr(209) . chr(210)
. chr(211) . chr(212) . chr(213) . chr(214) . chr(216) . chr(217) . chr(218)
. chr(219) . chr(220) . chr(221) . chr(224) . chr(225) . chr(226) . chr(227)
. chr(228) . chr(229) . chr(231) . chr(232) . chr(233) . chr(234) . chr(235)
. chr(236) . chr(237) . chr(238) . chr(239) . chr(241) . chr(242) . chr(243)
. chr(244) . chr(245) . chr(246) . chr(248) . chr(249) . chr(250) . chr(251)
. chr(252) . chr(253) . chr(255);
$chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy";
$string = strtr($string, $chars['in'], $chars['out']);
$double_chars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254));
$double_chars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th');
$string = str_replace($double_chars['in'], $double_chars['out'], $string);
}
return $string;
}
/**
* Looks for particular patterns to attempt to determine if the provided string is in UTF8 format
*
* @param string $str
* @return boolean True if it's possibly UTF8
*/
function seems_utf8($str)
{
$length = strlen($str);
for ($i = 0; $i < $length; $i++) {
$c = ord($str[$i]);
if ($c < 0x80) {
$n = 0; # 0bbbbbbb
} elseif (($c & 0xE0) == 0xC0) {
$n = 1; # 110bbbbb
} elseif (($c & 0xF0) == 0xE0) {
$n = 2; # 1110bbbb
} elseif (($c & 0xF8) == 0xF0) {
$n = 3; # 11110bbb
} elseif (($c & 0xFC) == 0xF8) {
$n = 4; # 111110bb
} elseif (($c & 0xFE) == 0xFC) {
$n = 5; # 1111110b
} else {
return false; # Does not match any model
}
for ($j = 0; $j < $n; $j++) { # n bytes matching 10bbbbbb follow ?
if ((++$i == $length) || ((ord($str[$i]) & 0xC0) != 0x80)) {
return false;
}
}
}
return true;
}
/**
* Use the browser settings to determine the default / preferred language
*
* @param boolean $strict_mode
* @return string The language string the user may prefer
*/
function http_get_preferred_language($strict_mode = false)
{
global $languages;
if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return null;
}
$accepted_languages = preg_split('/,\s*/', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
$current_lang = false;
$current_quality = 0;
$language_map = array();
foreach ($languages as $key => $value) {
$language_map[strtolower($key)] = $key;
}
foreach ($accepted_languages as $accepted_language) {
$res = preg_match('/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', $accepted_language, $matches);
if (!$res) {
continue;
}
$lang_code = explode('-', $matches[1]);
// Use specified quality, if any
if (isset($matches[2])) {
$lang_quality = (float)$matches[2];
} else {
$lang_quality = 1.0;
}
while (count($lang_code)) {
$short = strtolower(join('-', $lang_code));
if (array_key_exists($short, $language_map) && $lang_quality > $current_quality) {
$current_lang = $language_map[$short];
$current_quality = $lang_quality;
}
if ($strict_mode) {
break;
}
array_pop($lang_code);
}
}
return $current_lang;
}
/**
* Set the user's current language based on get/post/cookie values as appropriate.
*
* @return string The language string set
*/
function setLanguage()
{
global $browser_language,$disable_languages,$defaultlanguage,$languages,$baseurl_short;
$language = "";
if (isset($_GET["language_set"])) {
$language = $_GET["language_set"];
if (array_key_exists($language, $languages)) {
# Cannot use the general.php: rs_setcookie() here since general may not have been included.
# Set new cookie
setcookie("language", $language, time() + (3600 * 24 * 1000));
setcookie("language", $language, time() + (3600 * 24 * 1000), $baseurl_short . "pages/", '', false, true);
return $language;
} else {
$language = "";
}
}
if (isset($_GET["language"]) && array_key_exists((string)$_GET["language"], $languages)) {
return $_GET["language"];
}
if (isset($_POST["language"]) && array_key_exists((string)$_POST["language"], $languages)) {
return $_POST["language"];
}
if (isset($_COOKIE["language"]) && array_key_exists((string)$_COOKIE["language"], $languages)) {
return $_COOKIE["language"];
}
if (!$disable_languages && $browser_language && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$language = http_get_preferred_language();
if (!empty($language) && array_key_exists($language, $languages)) {
return $language;
}
}
if (($disable_languages || $language === "") && isset($defaultlanguage)) {
return $defaultlanguage;
}
# Final case.
return 'en';
}
/**
* Load all site text for the given page and language into the global $lang array
*
* @param array $lang Passed by reference
* @param string $pagename Pagename
* @param string $language Language
* @return void
*/
function lang_load_site_text(&$lang, $pagename, $language = "")
{
global $defaultlanguage, $site_text;
$site_text = array();
$results = ps_query("SELECT language,name,text FROM site_text WHERE (page=? OR page='all' OR page='') AND (specific_to_group IS NULL OR specific_to_group=0)", array("s",$pagename), "sitetext");
for ($n = 0; $n < count($results); $n++) {
$site_text[$results[$n]["language"] . "-" . $results[$n]["name"]] = $results[$n]["text"];
}
$query = " SELECT `name`,
`text`,
`page`,
`language`, specific_to_group
FROM site_text
WHERE (`language` = ? OR `language` = ?)
AND (specific_to_group IS NULL OR specific_to_group = 0)
";
$parameters = array("s",$language,"s",$defaultlanguage);
if ($pagename != "admin_content") {
// Load all content on the admin_content page to allow management.
$query .= "AND (page = ? OR page = 'all' OR page = '' " . (($pagename == "dash_tile") ? " OR page = 'home'" : "") . ")";
$parameters[] = "s";
$parameters[] = $pagename;
}
$results = ps_query($query, $parameters, "sitetext");
// Create a new array to hold customised text at any stage, may be overwritten in authenticate.php. Needed so plugin lang file can be overidden if plugin only enabled for specific groups
$GLOBALS['customsitetext'] = array();
// Set the default language first, user language second so we can override the default with any language specific entries
foreach ([$defaultlanguage, $language] as $check_lang) {
foreach ($results as $result) {
if ($result['language'] != $check_lang) {
continue;
}
if ($result['page'] == '') {
$lang[$result['name']] = $result['text'];
$GLOBALS['customsitetext'][$result['name']] = $result['text'];
} else {
$lang["{$result['page']}__{$result['name']}"] = $result['text'];
}
}
}
}
/**
* Return an array of all available language strings for the given id, with the language code as the key
*
* @param string $langid The identifier of the lang string
*
* @return array
*
*/
function i18n_get_all_translations(string $langid): array
{
global $lang;
$savedlang = $lang;
$alltranslations = [];
foreach ($GLOBALS["languages"] as $langcode => $availlanguage) {
if ($langcode != "en") {
if (substr($langcode, 2, 1) != '-') {
$langcode = substr($langcode, 0, 2);
}
$use_error_exception_cache = $GLOBALS["use_error_exception"] ?? false;
$GLOBALS["use_error_exception"] = true;
try {
include __DIR__ . "/../languages/" . safe_file_name($langcode) . ".php";
} catch (Exception $e) {
debug("Unable to include language file $langcode.php" . $e->getMessage());
}
$GLOBALS["use_error_exception"] = $use_error_exception_cache;
}
$alltranslations[$langcode] = $lang[$langid];
}
// Revert to original
$lang = $savedlang;
return $alltranslations;
}
/**
* Merge values for across multiple translation strings.
* Where a value is missing for a language used elsewhere i18n_get_translated will be used.
* If no value is provided and no English (en) string is found then blank will be returned.
*
* Example input: ["~en:cheese~fr:fromage","~en:bread~fr:pain"]
*
* Example output: ["~en:cheese, bread","~fr:fromage, pain"]
*
* @param array $values Translation strings to merge.
*
* @return array Each language string as a comma separated list.
*/
function i18n_merge_translations(array $values): array
{
if (count($values) <= 1) {
return $values;
}
$values_split = array_map('i18n_get_translations', $values);
$languages = [];
foreach ($values_split as $value) {
$languages = array_unique(array_merge($languages, array_keys($value)));
}
$options = [];
foreach ($values_split as $value) {
foreach ($languages as $language) {
if (key_exists($language, $value)) {
$options[$language][] = $value[$language];
} else {
$options[$language][] = i18n_get_translated($value['en'] ?? "");
}
}
}
return array_map(
fn(string $lang, array $values): string => "~$lang:" . implode(", ", $values),
array_keys($options),
array_values($options)
);
}
/**
* Get the name of a language in your own language plus its native name.
* Used for language selection dropdowns.
*
* @param string $language_key Key of $languages array, e.g. "fr"
* @param string $language_value The value (full name) from the $languages array, e.g. "Français"
*/
function get_display_language($language_key, $language_value): string
{
global $lang;
$display_lang = $lang["language-" . $language_key];
if ($lang["language-" . $language_key ] !== $language_value) {
$display_lang .= " (" . $language_value . ")";
}
return $display_lang;
}
/**
* Simple check to determine if the text string supplied contains i18n language translations.
* Returns true if match found e.g. '~en:test~en-US:test'. Returns false if no code is found. e.g. 'test'.
*
* @param string $text Text string to check e.g. a node name or field value.
*/
function is_i18n_language_string($text): bool
{
return preg_match('/~(.*?):/', $text) === 1;
}

480
include/log_functions.php Normal file
View File

@@ -0,0 +1,480 @@
<?php
/**
* Log activity in the system (e.g user deleted a user)
*
* @param string $note Notes/comments regarding this activity
* @param string $log_code
* @param string $value_new
* @param string $remote_table
* @param string $remote_column
* @param string $remote_ref
* @param string $ref_column_override
* @param string $value_old
* @param string $user User that ran the activity
* @param boolean $generate_diff
*
* @return void
*/
function log_activity($note = null, $log_code = LOG_CODE_UNSPECIFIED, $value_new = null, $remote_table = null, $remote_column = null, $remote_ref = null, $ref_column_override = null, $value_old = null, $user = null, $generate_diff = false)
{
if (is_null($log_code)) {
$log_code = LOG_CODE_UNSPECIFIED;
}
if (!function_exists('log_diff')) {
include_once __DIR__ . '/resource_functions.php';
}
if (is_null($user)) {
global $userref;
$user = isset($userref) && !is_null($userref) ? (int) $userref : 0;
}
if (is_null($value_old) && !is_null($remote_table) && !is_null($remote_column) && !is_null($remote_ref)) { // only try and get the old value if not explicitly set and we have table details
$row = ps_query("SELECT " . columns_in($remote_table) . " FROM `{$remote_table}` WHERE `" . (is_null($ref_column_override) ? 'ref' : $ref_column_override) . "` = ?", array("i",$remote_ref));
if (isset($row[0][$remote_column])) {
$value_old = $row[0][$remote_column];
$log_code = $log_code == LOG_CODE_UNSPECIFIED ? LOG_CODE_EDITED : $log_code;
} else {
$log_code = $log_code == LOG_CODE_UNSPECIFIED ? LOG_CODE_CREATED : $log_code;
}
}
if ($value_old == $value_new && ($log_code == LOG_CODE_EDITED || $log_code == LOG_CODE_COPIED)) { // return if the value has not changed
return;
}
$parameters = array("i",$user,"s",(!LOG_CODE_validate($log_code) ? LOG_CODE_UNSPECIFIED : $log_code));
$parameters[] = "s";
$parameters[] = (is_null($note) ? null : $note);
$parameters[] = "s";
$parameters[] = (is_null($value_old) ? null : $value_old);
$parameters[] = "s";
$parameters[] = (is_null($value_new) ? null : $value_new);
$parameters[] = "s";
$parameters[] = (!is_null($value_old) && !is_null($value_new) && $generate_diff ? log_diff($value_old, $value_new) : '');
$parameters[] = "s";
$parameters[] = (is_null($remote_table) ? null : $remote_table);
$parameters[] = "s";
$parameters[] = (is_null($remote_column) ? null : $remote_column);
$parameters[] = "s";
$parameters[] = (is_null($remote_ref) ? null : mb_strcut($remote_ref, 0, 100));
ps_query("INSERT INTO `activity_log` (`logged`,`user`,`log_code`,`note`,`value_old`,`value_new`,`value_diff`,`remote_table`,`remote_column`,`remote_ref`)
VALUES (NOW()," . ps_param_insert(count($parameters) / 2) . ")", $parameters);
}
/**
* Log script messages on screen and optionally in a file. If debug_log is enabled, it will also write the message in the
* debug log file.
*
* @uses debug()
*
* @param string $message
* @param resource $file
*
* @return void
*/
function logScript($message, $file = null)
{
$date_time = date('Y-m-d H:i:s');
if (PHP_SAPI == 'cli') {
echo "{$date_time} {$message}" . PHP_EOL;
}
// Log in debug as well, with extended information to show the backtrace
global $debug_extended_info;
$orig_debug_extended_info = $debug_extended_info;
$debug_extended_info = true;
debug($message);
$debug_extended_info = $orig_debug_extended_info;
// If a file resource has been passed, then write to that file as well
if (!is_null($file) && (is_resource($file) && 'file' == get_resource_type($file) || 'stream' == get_resource_type($file))) {
fwrite($file, "{$date_time} {$message}" . PHP_EOL);
}
}
/**
* Retrieve entries from resource log based on date or references
*
* @param integer $minref (Optional) Minimum ref of resource log entry to return (default 0)
* @param integer $days (Optional) Number of days to return. e.g 3 = all results for today, yesterday and the day before. Default = 7 (ignored if minref supplied)
* @param integer $maxrecords (Optional) Maximum number of records to return. Default = all rows (0)
* @param array $fields (Optional) Limit results to particular metadata field(s)
* @param array $log_codes (Optional) Limit results to particular log code(s)
*
* @return array
*/
function resource_log_last_rows($minref = 0, $days = 7, $maxrecords = 0, array $fields = [], array $log_codes = [])
{
if (!checkperm('v')) {
return array();
}
$parameters = array();
$sql = "SELECT date, ref, resource, type, resource_type_field AS field, user, notes, diff, usageoption FROM resource_log WHERE type != 'l'";
if ($minref > 0) {
$sql .= " AND ref >= ?";
$parameters[] = "i";
$parameters[] = (int)$minref;
} else {
$sql .= " AND datediff(now(),date) < ?";
$parameters[] = "i";
$parameters[] = (int)$days;
}
$fields = array_filter($fields, 'is_positive_int_loose');
if ($fields !== []) {
$sql .= sprintf(' AND resource_type_field IN (%s)', ps_param_insert(count($fields)));
$parameters = array_merge($parameters, ps_param_fill($fields, 'i'));
}
$log_codes = array_filter($log_codes, 'LOG_CODE_validate');
if ($log_codes !== []) {
$sql .= sprintf(' AND BINARY `type` IN (%s)', ps_param_insert(count($log_codes)));
$parameters = array_merge($parameters, ps_param_fill($log_codes, 's'));
}
if ($maxrecords > 0) {
$sql .= " LIMIT " . (int)$maxrecords;
}
return ps_query($sql, $parameters);
}
/**
* Get activity log entries from log tables (e.g activity_log, resource_log and collection_log)
*
* @uses ps_query()
*
* @param string $search Search text to filter down results using fuzzy searching
* @param integer $offset Specifies the offset of the first row to return
* @param integer $rows Specifies the maximum number of rows to return
* @param array $where_statements Where statements for log tables
* Example of where statements:
* $where_statements = array(
* 'activity_log' => "`activity_log`.`user`='{$actasuser}' AND ",
* 'resource_log' => "`resource_log`.`user`='{$actasuser}' AND ",
* 'collection_log' => "`collection_log`.`user`='{$actasuser}' AND ",
* );
* @param string $table Table name (e.g resource_type_field, user, resource)
* @param integer $table_reference ID of the record in the referred table
* @param boolean $count Switch for if the result should be a single count or the result set
*
* @return array
*/
function get_activity_log($search, $offset, $rows, array $where_statements, $table, $table_reference, $count = false)
{
$where_activity_log_statement = $where_statements['activity_log'];
$where_resource_log_statement = $where_statements['resource_log'];
$where_collection_log_statement = $where_statements['collection_log'];
$log_codes = array_values(LOG_CODE_get_all());
$col_when_statements = $res_when_statements = "";
$col_when_parameters = $res_when_parameters = [];
$res_log_codes_processed = [];
foreach ($log_codes as $log_code) {
$log_code_description = "";
if (!isset($GLOBALS['lang']["log_code_{$log_code}"]) || in_array($log_code, $res_log_codes_processed)) {
if (!isset($GLOBALS['lang']["collectionlog-{$log_code}"])) {
continue;
}
$log_code_description = $GLOBALS['lang']["collectionlog-{$log_code}"];
$col_when_statements .= " WHEN BINARY(?) THEN ?";
$col_when_parameters = array_merge($col_when_parameters, ['s', $log_code, 's', $log_code_description]);
continue;
}
$log_code_description = $GLOBALS['lang']["log_code_{$log_code}"];
$res_when_statements .= " WHEN BINARY(?) THEN ?";
$res_when_parameters = array_merge($res_when_parameters, ['s', $log_code, 's', $log_code_description]);
$res_log_codes_processed[] = $log_code;
}
$count_statement_start = "";
$count_statement_end = "";
$sql_query = new PreparedStatementQuery();
$sql_query->sql = "
SELECT
`activity_log`.`logged` AS 'datetime',
`user`.`username` AS 'user',
CASE BINARY(`activity_log`.`log_code`) " . $res_when_statements . $col_when_statements . " ELSE `activity_log`.`log_code` END AS 'operation',
`activity_log`.`note` AS 'notes',
NULL AS 'resource_field',
`activity_log`.`value_old` AS 'old_value',
`activity_log`.`value_new` AS 'new_value',
if(`activity_log`.`value_diff`='','',concat('<pre>',`activity_log`.`value_diff`,'</pre>')) AS 'difference',
'' AS 'access_key',
`activity_log`.`remote_table`AS 'table',
`activity_log`.`remote_column` AS 'column',
`activity_log`.`remote_ref` AS 'table_reference'
FROM `activity_log`
LEFT OUTER JOIN `user` ON `activity_log`.`user`=`user`.`ref`
WHERE
{$where_activity_log_statement}";
$sql_query->parameters = array_merge($sql_query->parameters, array_merge($res_when_parameters, $col_when_parameters));
$search_block =
"`activity_log`.`ref` LIKE ?
OR `activity_log`.`logged` LIKE ?
OR `user`.`username` LIKE ?
OR `activity_log`.`note` LIKE ?
OR `activity_log`.`value_old` LIKE ?
OR `activity_log`.`value_new` LIKE ?
OR `activity_log`.`value_diff` LIKE ?
OR `activity_log`.`remote_table` LIKE ?
OR `activity_log`.`remote_column` LIKE ?
OR `activity_log`.`remote_ref` LIKE ?
OR ? LIKE (CASE BINARY(`activity_log`.`log_code`)";
$sql_query->sql .= "(" . $search_block . " " . $res_when_statements . $col_when_statements . " ELSE `activity_log`.`log_code` END) )";
$sql_query->parameters = array_merge($sql_query->parameters, ps_fill_param_array($search_block, "%{$search}%", 's'), array_merge($res_when_parameters, $col_when_parameters));
$sql_query->sql .=
"UNION
SELECT
`resource_log`.`date` AS 'datetime',
`user`.`username` AS 'user',
CASE BINARY(`resource_log`.`type`) {$res_when_statements} ELSE `resource_log`.`type` END AS 'operation',
`resource_log`.`notes` AS 'notes',
`resource_type_field`.`title` AS 'resource_field',
`resource_log`.`previous_value` AS 'old_value',
'' AS 'new_value',
if(`resource_log`.`diff`='','',concat('<pre>',`resource_log`.`diff`,'</pre>')) AS 'difference',
`resource_log`.`access_key` AS 'access_key',
'resource' AS 'table',
'ref' AS 'column',
`resource_log`.`resource` AS 'table_reference'
FROM `resource_log`
LEFT OUTER JOIN `user` ON `resource_log`.`user`=`user`.`ref`
LEFT OUTER JOIN `resource_type_field` ON `resource_log`.`resource_type_field`=`resource_type_field`.`ref`
WHERE
{$where_resource_log_statement}";
$sql_query->parameters = array_merge($sql_query->parameters, $res_when_parameters);
$search_block =
"`resource_log`.`ref` LIKE ?
OR `resource_log`.`date` LIKE ?
OR `user`.`username` LIKE ?
OR `resource_log`.`notes` LIKE ?
OR `resource_log`.`previous_value` LIKE ?
OR 'resource' LIKE ?
OR 'ref' LIKE ?
OR `resource_log`.`resource` LIKE ?
OR ? LIKE (CASE BINARY(`resource_log`.`type`)";
$sql_query->sql .= "(" . $search_block . " {$res_when_statements} ELSE `resource_log`.`type` END) )";
$sql_query->parameters = array_merge($sql_query->parameters, ps_fill_param_array($search_block, "%{$search}%", 's'), $res_when_parameters);
$sql_query->sql .=
"UNION
SELECT
`collection_log`.`date` AS 'datetime',
`user`.`username` AS 'user',
CASE BINARY(`collection_log`.`type`) {$col_when_statements} ELSE `collection_log`.`type` END AS 'operation',
`collection_log`.`notes` AS 'notes',
NULL AS 'resource_field',
'' AS 'old_value',
'' AS 'new_value',
'' AS 'difference',
'' AS 'access_key',
if(`collection_log`.`resource` IS NULL,'collection','resource') AS 'table',
'ref' AS 'column',
if(`collection_log`.`resource` IS NULL,`collection_log`.`collection`,`collection_log`.`resource`) AS 'table_reference'
FROM `collection_log`
LEFT OUTER JOIN `user` ON `collection_log`.`user`=`user`.`ref`
LEFT OUTER JOIN `collection` ON `collection_log`.`collection`=`collection`.`ref`
WHERE
{$where_collection_log_statement}";
$sql_query->parameters = array_merge($sql_query->parameters, $col_when_parameters);
$search_block =
"`collection_log`.`collection` LIKE ?
OR `collection_log`.`date` LIKE ?
OR `collection_log`.`notes` LIKE ?
OR `collection_log`.`resource` LIKE ?
OR `collection`.`name` LIKE ?
OR `user`.`username` LIKE ?
OR ? LIKE (CASE BINARY(`collection_log`.`type`)";
$sql_query->sql .= "(" . $search_block . " {$col_when_statements} ELSE `collection_log`.`type` END) )";
$sql_query->parameters = array_merge($sql_query->parameters, ps_fill_param_array($search_block, "%{$search}%", 's'), $col_when_parameters);
$sql_query->sql .= "ORDER BY `datetime` DESC";
# Wrap the query as a subquery within a table selection if necessary
if (trim($table) !== '') {
$sql_query->sql = "SELECT * FROM ({$sql_query->sql}) AS `logs` WHERE `logs`.`table` = ? ";
$sql_query->parameters = array_merge($sql_query->parameters, ['s', $table]);
if (is_numeric($table_reference) && $table_reference > 0) {
$sql_query->sql .= "AND `logs`.`table_reference` = ?";
$sql_query->parameters = array_merge($sql_query->parameters, ['i', $table_reference]);
}
}
$limit = sql_limit($offset, $rows);
if ($count) {
$count_statement_start = "SELECT COUNT(*) AS value FROM (";
$count_statement_end = ") AS count_select";
$sql_query->sql = $count_statement_start . $sql_query->sql . $count_statement_end;
return ps_value($sql_query->sql, $sql_query->parameters, 0);
} else {
$sql_query->sql .= " " . $limit;
return ps_query($sql_query->sql, $sql_query->parameters);
}
}
/**
* Use resource log to obtain a count of resources downloaded by the specified user in the last X days
*
* @param integer $userref User reference
* @param integer $user_dl_days The number of days to check the resource log for
*
* @return integer download count
*/
function get_user_downloads($userref, $user_dl_days)
{
$daylimit = (int)$user_dl_days != 0 ? (int)$user_dl_days : 99999;
$parameters = array("i",(int)$userref, "i",$daylimit * 60 * 60 * 24);
return ps_value("SELECT COUNT(DISTINCT resource) value
FROM resource_log rl
WHERE rl.type = 'd'
AND rl.user = ?
AND TIMESTAMPDIFF(SECOND, date, now()) <= ?", $parameters, 0);
}
/**
* Add detail of node changes to resource log
*
* Note that this function originally required only the added and removed nodes to be passed.
* This was prior to node reversion changes, which requires the logging of all the existing resource nodes
*
* @param integer $resource Resource ID
* @param array $nodes_new Array of new node IDs
* @param array $nodes_current Array of old node IDs
* @param string $lognote Optional note to add to log entry
* @param array $nodes_renamed Optional array of old node names with node id as key e.g. [345 => 'oldname',678 => "pastname"]
*
* @return boolean Success/failure
*/
function log_node_changes($resource, $nodes_new, $nodes_current, $lognote = "", $nodes_renamed = [])
{
if ((string)(int)$resource !== (string)$resource) {
return false;
}
// Find treefields - required so that old value will be logged with full path
$treefields = array_column(get_resource_type_fields("", "ref", "asc", "", [FIELD_TYPE_CATEGORY_TREE]), "ref");
$nodefieldchanges = array();
if (count($nodes_current) != count($nodes_new) || array_diff($nodes_new, $nodes_current) != array_diff($nodes_current, $nodes_new)) {
foreach ($nodes_current as $node) {
$nodedata = array();
if (get_node($node, $nodedata, false)) {
if (in_array($nodedata["resource_type_field"], $treefields) && $nodedata["parent"] > 0) {
$parents = get_node_strings(get_parent_nodes($nodedata["ref"], true, true), false, false);
$nodefieldchanges[$nodedata["resource_type_field"]][0][] = reset($parents);
} else {
$nodefieldchanges[$nodedata["resource_type_field"]][0][] = $nodedata["name"];
}
}
}
foreach ($nodes_new as $node) {
$nodedata = array();
if (get_node($node, $nodedata, false)) {
if (in_array($nodedata["resource_type_field"], $treefields) && $nodedata["parent"] > 0) {
$parentnodes = get_parent_nodes($nodedata["ref"], true, true);
$parents = get_node_strings($parentnodes, false, false);
$nodefieldchanges[$nodedata["resource_type_field"]][1][] = reset($parents);
} else {
$nodefieldchanges[$nodedata["resource_type_field"]][1][] = $nodedata["name"];
}
}
}
}
foreach ($nodes_renamed as $nodeid => $oldname) {
if (!in_array($nodeid, $nodes_new)) {
// $nodes_renamed contains a node that's not being used.
// This could be after changing from unique node to one used elsewhere.
continue;
}
$nodedata = array();
if (get_node($nodeid, $nodedata, false)) { // Don't use cache - always get the latest node name when writing to the log
$nodefieldchanges[$nodedata["resource_type_field"]][0][] = $oldname;
$nodefieldchanges[$nodedata["resource_type_field"]][1][] = $nodedata["name"];
}
}
foreach ($nodefieldchanges as $key => $value) {
if (isset($value[0]) && isset($value[1]) && array_diff($value[0], $value[1]) == array_diff($value[1], $value[0])) {
// No difference
continue;
}
// Log changes to each field separately
$fromvalue = isset($value[0]) ? implode(NODE_NAME_STRING_SEPARATOR, $value[0]) : "";
$tovalue = isset($value[1]) ? implode(NODE_NAME_STRING_SEPARATOR, $value[1]) : "";
resource_log($resource, LOG_CODE_EDITED, $key, $lognote, $fromvalue, $tovalue);
}
return true;
}
/**
* Log search events
*
* @param string $search Actual search string {@see do_search()}
* @param array $resource_types Resource types filter
* @param array $archive_states Archive states filter
* @param integer $result_count Search result count
*
* @return void
*/
function log_search_event(string $search, array $resource_types, array $archive_states, int $result_count)
{
global $userref;
$resource_types = array_filter($resource_types, 'is_int_loose');
$archive_states = array_filter($archive_states, 'is_int_loose');
$parameters = array();
$parameters[] = "s";
$parameters[] = ($search === '' || !is_string_loose($search) ? null : $search);
$parameters[] = "s";
$parameters[] = (empty($resource_types) ? null : implode(', ', $resource_types));
$parameters[] = "s";
$parameters[] = (empty($archive_states) ? null : implode(', ', $archive_states));
$parameters[] = "i";
$parameters[] = (is_null($userref) ? null : (int)$userref);
$parameters[] = "i";
$parameters[] = (is_int_loose($result_count) ? (int)$result_count : 0);
$q = "INSERT INTO search_log (search_string, resource_types, archive_states, `user`, result_count) VALUES (?,?,?,?,?)";
return ps_query($q, $parameters);
}
/**
* Generate a fingerprint which could then be used as a trace ID for event correlation purposes.
*
* @param array $components Data making up our fingerprint.
*/
function generate_trace_id(array $components): string
{
return md5(implode('--', $components));
}

View File

@@ -0,0 +1,27 @@
<script>
var SlideshowImages = new Array();
var SlideshowCurrent = -1;
var SlideshowTimer = 0;
var big_slideshow_timer = <?php echo $slideshow_photo_delay; ?>;
<?php
foreach (get_slideshow_files_data() as $slideshow_file_info) {
if ((bool) $slideshow_file_info['login_show'] === false) {
continue;
}
$image_download_url = "{$baseurl_short}pages/download.php?slideshow={$slideshow_file_info['ref']}";
$image_resource = isset($slideshow_file_info['link']) ? $slideshow_file_info['link'] : '';
?>
RegisterSlideshowImage('<?php echo $image_download_url; ?>', '<?php echo $image_resource; ?>');
<?php
}
?>
jQuery(document).ready(function()
{
ClearTimers();
ActivateSlideshow(true);
});
</script>
<div id="login_box">

380
include/login_functions.php Normal file
View File

@@ -0,0 +1,380 @@
<?php
/**
* Performs the login using the global $username, and $password. Since the "externalauth" hook
* is allowed to change the credentials later on, the $password_hash needs to be global as well.
*
* @return array Containing the login details ('valid' determines whether or not the login succeeded).
*/
function perform_login($loginuser = "", $loginpass = "")
{
global $scramble_key, $lang, $max_login_attempts_wait_minutes, $max_login_attempts_per_ip, $max_login_attempts_per_username,
$username, $password, $password_hash, $session_hash, $usergroup;
$result = [];
$result['valid'] = $valid = false;
if (trim($loginpass) != "") {
$password = trim($loginpass);
}
if (trim($loginuser) != "") {
$username = trim($loginuser);
}
$username_as_entered = $username; # The username supplied at login before update below via get_user_by_username();
// If a special key is sent, which is a hash based on the username and scramble key, then allow a login
// using this hash as the password. This is for the 'log in as this user' feature.
$impersonate_user = hash_equals(getval('userkey', ''), hash_hmac("sha256", "login_as_user" . $loginuser . date("Ymd"), $scramble_key, true));
// Get user record
$user_ref = get_user_by_username($username);
$found_user_record = ($user_ref !== false);
if ($found_user_record) {
$user_data = get_user($user_ref);
// Did the user log in using their email address? If so update username variable
if (
mb_strtolower($user_data['username']) !== mb_strtolower($username)
&& mb_strtolower($user_data['email']) === mb_strtolower($username)
) {
$username = $user_data['username'];
}
}
// User logs in
if (
$found_user_record
&& rs_password_verify($password, (string) $user_data['password'], ['username' => $username])
&& $password != ""
) {
$password_hash_info = get_password_hash_info();
$algo = $password_hash_info['algo'];
$options = $password_hash_info['options'];
if (password_needs_rehash($user_data['password'], $algo, $options)) {
$password_hash = rs_password_hash("RS{$username}{$password}");
if ($password_hash === false) {
trigger_error('Failed to rehash password!');
}
ps_query("UPDATE user SET `password` = ? WHERE ref = ?", array("s",$password_hash,"i",$user_ref));
} else {
$password_hash = $user_data['password'];
}
$valid = true;
} elseif (
// An admin logs in as this user
$found_user_record
&& $impersonate_user
&& rs_password_verify($password, (string) $user_data['password'], ['username' => $username, 'impersonate_user' => true])
) {
$password_hash = $user_data['password'];
$valid = true;
}
$ip = get_ip();
# This may change the $username, $password, and $password_hash
$externalresult = hook("externalauth", "", array($username_as_entered, $password)); #Attempt external auth if configured
if ($externalresult) {
// Get user data as per old method
$user_ref = get_user_by_username($username);
$found_user_record = ($user_ref !== false);
if ($found_user_record) {
$user_data = get_user($user_ref);
$valid = true;
}
}
if ($valid) {
$userref = $user_data['ref'];
$usergroup = $user_data['usergroup'];
$expires = $user_data['account_expires'];
$approved = $user_data['approved'];
if ($approved == 2) {
$result['error'] = $lang["accountdisabled"];
log_activity('Account Disabled', LOG_CODE_FAILED_LOGIN_ATTEMPT, $ip, "user", "last_ip", $userref, null, null, $user_ref);
return $result;
}
if ($expires != "" && $expires != "0000-00-00 00:00:00" && strtotime($expires) <= time()) {
$result['error'] = $lang["accountexpired"];
log_activity('Account Expired', LOG_CODE_FAILED_LOGIN_ATTEMPT, $ip, "user", "last_ip", $userref, null, null, $user_ref);
return $result;
}
$session_hash = generate_session_hash($password_hash);
$result['valid'] = true;
$result['session_hash'] = $session_hash;
$result['password_hash'] = $password_hash;
$result['ref'] = $userref;
$language = getval("language", "");
update_user_access(
$userref,
[
"session" => $session_hash,
"lang" => $language,
"login_tries" => 0,
"last_ip" => $ip,
]
);
// Update user local time zone (if provided)
$get_user_local_timezone = getval('user_local_timezone', null);
set_config_option($userref, 'user_local_timezone', $get_user_local_timezone);
# Log this
daily_stat("User session", $userref);
log_activity(null, LOG_CODE_LOGGED_IN, $ip, "user", "last_ip", ($userref != "" ? $userref : "null"), null, '', ($userref != "" ? $userref : "null"));
# Blank the IP address lockout counter for this IP
ps_query("DELETE FROM ip_lockout WHERE ip = ?", array("s",$ip));
return $result;
}
# Invalid login
if (isset($externalresult["error"])) {
$result['error'] = $externalresult["error"];
} // We may have been given a better error to display
else {
$result['error'] = $lang["loginincorrect"];
}
hook("loginincorrect");
# Add / increment a lockout value for this IP
$lockouts = ps_value("select count(*) value from ip_lockout where ip=? and tries<?", array("s",$ip,"i",$max_login_attempts_per_ip), "");
if ($lockouts > 0) {
# Existing row with room to move
$tries = ps_value("select tries value from ip_lockout where ip=?", array("s",$ip), 0);
$tries++;
if ($tries == $max_login_attempts_per_ip) {
# Show locked out message.
$result['error'] = str_replace("?", $max_login_attempts_wait_minutes, $lang["max_login_attempts_exceeded"]);
$log_message = 'Max login attempts from IP exceeded - IP: ' . $ip;
log_activity($log_message, LOG_CODE_FAILED_LOGIN_ATTEMPT, $tries, 'ip_lockout', 'ip', $ip, 'ip');
}
# Increment
ps_query("update ip_lockout set last_try=now(),tries=tries+1 where ip=?", array("s",$ip));
} else {
# New row
ps_query("delete from ip_lockout where ip=?", array("s",$ip));
ps_query("insert into ip_lockout (ip,tries,last_try) values (?,1,now())", array("s",$ip));
}
# Increment a lockout value for any matching username.
$ulocks = ps_query("select ref,login_tries,login_last_try from user where username=?", array("s",$username));
if (count($ulocks) > 0) {
$tries = $ulocks[0]["login_tries"];
if ($tries == "") {
$tries = 1;
} else {
$tries++;
}
if ($tries > $max_login_attempts_per_username) {
$tries = 1;
}
if ($tries == $max_login_attempts_per_username) {
# Show locked out message.
$result['error'] = str_replace("?", $max_login_attempts_wait_minutes, $lang["max_login_attempts_exceeded"]);
$log_message = 'Max login attempts exceeded';
log_activity($log_message, LOG_CODE_FAILED_LOGIN_ATTEMPT, $ip, 'user', 'ref', ($user_ref != false ? $user_ref : null), null, null, ($user_ref != false ? $user_ref : null));
}
ps_query("update user set login_tries=?,login_last_try=now() where username=?", array("i",$tries,"s",$username));
}
if ($valid !== true && !isset($log_message)) {
if (isset($result['error']) && $result['error'] != '') {
$log_message = strip_tags($result['error']);
} else {
$log_message = 'Failed Login';
}
log_activity(
$log_message, # Note
LOG_CODE_FAILED_LOGIN_ATTEMPT, # Log Code
$ip, # Value New
($user_ref != false ? 'user' : null), # Remote Table
($user_ref != false ? 'last_ip' : null), # Remote Column
($user_ref != false ? $user_ref : null), # Remote Ref
null, # Ref Column Override
null, # Value Old
($user_ref != false ? $user_ref : null) # User Ref
);
}
return $result;
}
/**
* Generates a unique session hash for user authentication.
*
* This function creates a session hash based on either a completely randomized method or a method
* that combines a password hash with the current date. It ensures that the generated hash is unique
* by checking against existing sessions in the database.
*
* @param string $password_hash The hashed password of the user, used for generating the session hash.
* @return string Returns a unique session hash.
*/
function generate_session_hash($password_hash)
{
# Generates a unique session hash
global $randomised_session_hash,$scramble_key;
if ($randomised_session_hash) {
# Completely randomised session hashes. May be more secure, but allows only one user at a time.
while (true) {
$session = md5(generateSecureKey(128));
if (ps_value("select count(*) value from user where session=?", array("s",$session), 0) == 0) {
return $session;
} # Return a unique hash only.
}
} else {
# Session hash is based on the password hash and the date, so there is one new session hash each day. Allows two users to use the same login.
$suffix = "";
while (true) {
$session = md5($scramble_key . $password_hash . date("Ymd") . $suffix);
if (ps_value("select count(*) value from user where session=? and password<>?", array("s",$session,"s",$password_hash), 0) == 0) {
return $session;
} # Return a unique hash only.
$suffix .= "."; # Extremely unlikely case that this was not a unique session (hash collision) - alter the string slightly and try again.
}
}
}
/**
* Set login cookies
*
* @param integer $user User ref
* @param string $session_hash User session hash
* @param string $language Language code (e.g en)
* @param boolean $user_preferences Set colour theme from user preferences
*
* @return void
*/
function set_login_cookies($user, $session_hash, $language = "", $user_preferences = true)
{
global $baseurl, $baseurl_short, $allow_keep_logged_in, $default_res_types, $language;
$expires = 0;
if ((string)(int)$user != (string)$user || $user < 1) {
debug("set_login_cookies() - invalid paramters passed : " . func_get_args());
return false;
}
if ($allow_keep_logged_in && getval("remember", "") != "") {
$expires = 100;
} # remember login for 100 days
if ($language != "") {
# Store language cookie
rs_setcookie("language", $language, 1000); # Only used if not global cookies
rs_setcookie("language", $language, 1000, $baseurl_short . "pages/");
}
# Set the session cookie. Do this for all paths that nay set the cookie as otherwise we can end up with a valid cookie at e.g. pages/team or pages/ajax
rs_setcookie("user", "", 0, $baseurl_short);
rs_setcookie("user", "", 0, $baseurl_short . "pages");
rs_setcookie("user", "", 0, $baseurl_short . "pages/team");
rs_setcookie("user", "", 0, $baseurl_short . "pages/admin");
rs_setcookie("user", "", 0, $baseurl_short . "pages/ajax");
# Set user cookie, setting secure only flag if a HTTPS site, and also setting the HTTPOnly flag so this cookie cannot be probed by scripts (mitigating potential XSS vuln.)
rs_setcookie("user", $session_hash, $expires, $baseurl_short, "", substr($baseurl, 0, 5) == "https", true);
# Set default resource types
rs_setcookie('restypes', $default_res_types);
}
/**
* ResourceSpace password hashing
*
* @uses password_hash - @see https://www.php.net/manual/en/function.password-hash.php
*
* @param string $password Password
*
* @return string|false Password hash or false on failure
*/
function rs_password_hash(string $password)
{
$phi = get_password_hash_info();
$algo = $phi['algo'];
$options = $phi['options'];
// Pepper password with a known (by the application) secret.
$hmac = hash_hmac('sha256', $password, $GLOBALS['scramble_key']);
return password_hash($hmac, $algo, $options);
}
/**
* ResourceSpace verify password
*
* @param string $password Password
* @param string $hash Password hash
* @param array $data Extra data required for matching hash expectations (e.g username, impersonate_user). Key is the variable name,
* value is the actual value for that variable.
*
* @return boolean
*/
function rs_password_verify(string $password, string $hash, array $data)
{
// Prevent hashes being entered directly while still supporting direct entry of plain text passwords (for systems that
// were set up prior to MD5 password encryption was added). If a special key is sent, which is the MD5 hash of the
// username and the secret scramble key, then allow a login using the MD5 password hash as the password. This is for
// the 'log in as this user' feature.
$impersonate_user = $data['impersonate_user'] ?? false;
$hash_info = password_get_info($hash);
$pass_info = password_get_info($password);
$is_like_v1_hash = (mb_strlen($password) === 32);
$is_like_v2_hash = (mb_strlen($password) === 64);
$is_v3_hash = ($hash_info['algo'] === $pass_info['algo'] && $hash_info['algoName'] !== 'unknown');
if (!$impersonate_user && ($is_v3_hash || $is_like_v2_hash || $is_like_v1_hash)) {
return false;
}
$RS_madeup_pass = "RS{$data['username']}{$password}";
$hash_v1 = md5($RS_madeup_pass);
$hash_v2 = hash('sha256', $hash_v1);
// Most common case: hash is at version 3 (ie. hash generated using password_hash from PHP)
if (password_verify(hash_hmac('sha256', $RS_madeup_pass, $GLOBALS['scramble_key']), $hash)) {
return true;
} elseif ($hash_v2 === $hash) {
return true;
} elseif ($hash_v1 === $hash) {
return true;
}
// Legacy: Plain text password - when passwords were not hashed at all (very old code - should really not be the
// case anymore) -or- when someone resets it manually in the database
elseif ($password === $hash) {
return true;
} elseif (
isset($GLOBALS["scramble_key_old"]) && $GLOBALS["migrating_scrambled"]
&& password_verify(hash_hmac('sha256', $RS_madeup_pass, $GLOBALS['scramble_key_old']), $hash)
) {
// Force user to change password if password_expiry is enabled
ps_query("UPDATE user SET password_last_change = '1970-01-01' WHERE username = ?", array("s",$data['username']));
return true;
}
return false;
}
/**
* Helper function to get the password hash information (algorithm and options) from the global scope.
*
* @return array
*/
function get_password_hash_info()
{
return [
'algo' => ($GLOBALS['password_hash_info']['algo'] ?? PASSWORD_BCRYPT),
'options' => ($GLOBALS['password_hash_info']['options'] ?? ['cost' => 12])
];
}

199
include/map_basemaps.php Normal file
View File

@@ -0,0 +1,199 @@
<?php
// Leaflet.js Basemap Selections
if ($geo_leaflet_maps_sources) {
?>
<!--Determine basemaps and map groups for user selection-->
var baseMaps = [
{ groupName: "<?php echo escape($lang['map_osm_group']);?>", <!--OSM group-->
expanded: true,
layers: {
<?php if (isset($map_osm) && $map_osm) {
?> "<?php echo escape($lang['map_osm']);?>" : osm_mapnik, <?php
} ?>
<?php if (isset($map_osmde) && $map_osmde) {
?> "<?php echo escape($lang['map_osmde']);?>" : osm_de, <?php
} ?>
<?php if (isset($map_osmfr) && $map_osmfr) {
?> "<?php echo escape($lang['map_osmfr']);?>" : osm_fr, <?php
} ?>
<?php if (isset($map_osmbzh) && $map_osmbzh) {
?> "<?php echo escape($lang['map_osmbzh']);?>" : osm_bzh, <?php
} ?>
<?php if (isset($map_osmhot) && $map_osmhot) {
?> "<?php echo escape($lang['map_osmhot']);?>" : osm_hot, <?php
} ?>
<?php if (isset($map_osmmtb) && $map_osmmtb) {
?> "<?php echo escape($lang['map_osmmtb']);?>" : osm_mtb, <?php
} ?>
<?php if (isset($map_osmhikebike) && $map_osmhikebike) {
?> "<?php echo escape($lang['map_osmhikebike']);?>" : osm_hikebike, <?php
} ?>
<?php if (isset($map_otm) && $map_otm) {
?> "<?php echo escape($lang['map_otm']);?>" : osm_otm, <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_usgs_group']);?>", <!--USGS The National Map group-->
expanded: true,
layers: {
<?php if (isset($map_usgstopo) && $map_usgstopo) {
?> "<?php echo escape($lang['map_usgstopo']);?>" : usgs_topo, <?php
} ?>
<?php if (isset($map_usgsimagery) && $map_usgsimagery) {
?> "<?php echo escape($lang['map_usgsimagery']);?>" : usgs_imagery, <?php
} ?>
<?php if (isset($map_usgsimagerytopo) && $map_usgsimagerytopo) {
?> "<?php echo escape($lang['map_usgsimagerytopo']);?>" : usgs_imagerytopo <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_esri_group']);?>", <!--ESRI group-->
expanded: true,
layers: {
<?php if (isset($map_esristreet) && $map_esristreet) {
?> "<?php echo escape($lang['map_esristreet']);?>" : esri_street, <?php
} ?>
<?php if (isset($map_esridelorme) && $map_esridelorme) {
?> "<?php echo escape($lang['map_esridelorme']);?>" : esri_delorme, <?php
} ?>
<?php if (isset($map_esritopo) && $map_esritopo) {
?> "<?php echo escape($lang['map_esritopo']);?>" : esri_topo, <?php
} ?>
<?php if (isset($map_esriimagery) && $map_esriimagery) {
?> "<?php echo escape($lang['map_esriimagery']);?>" : esri_imagery, <?php
} ?>
<?php if (isset($map_esriterrain) && $map_esriterrain) {
?> "<?php echo escape($lang['map_esriterrain']);?>" : esri_terrain, <?php
} ?>
<?php if (isset($map_esrirelief) && $map_esrirelief) {
?> "<?php echo escape($lang['map_esrirelief']);?>" : esri_relief, <?php
} ?>
<?php if (isset($map_esriphysical) && $map_esriphysical) {
?> "<?php echo escape($lang['map_esriphysical']);?>" : esri_physical, <?php
} ?>
<?php if (isset($map_esriocean) && $map_esriocean) {
?> "<?php echo escape($lang['map_esriocean']);?>" : esri_ocean, <?php
} ?>
<?php if (isset($map_esrinatgeo) && $map_esrinatgeo) {
?> "<?php echo escape($lang['map_esrinatgeo']);?>" : esri_natgeo, <?php
} ?>
<?php if (isset($map_esrigray) && $map_esrigray) {
?> "<?php echo escape($lang['map_esrigray']);?>" : esri_gray <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_stamen_group']);?>", <!--Stamen group-->
expanded: true,
layers: {
<?php if (isset($map_stamentoner) && $map_stamentoner) {
?> "<?php echo escape($lang['map_stamentoner']);?>" : stamen_toner, <?php
} ?>
<?php if (isset($map_stamentonerlt) && $map_stamentonerlt) {
?> "<?php echo escape($lang['map_stamentonerlt']);?>" : stamen_tonerlt, <?php
} ?>
<?php if (isset($map_stamentonerback) && $map_stamentonerback) {
?> "<?php echo escape($lang['map_stamentonerback']);?>" : stamen_tonerback, <?php
} ?>
<?php if (isset($map_stamenterrain) && $map_stamenterrain) {
?> "<?php echo escape($lang['map_stamenterrain']);?>" : stamen_terrain, <?php
} ?>
<?php if (isset($map_stamenterrainback) && $map_stamenterrainback) {
?> "<?php echo escape($lang['map_stamenterrainback']);?>" : stamen_terrainback, <?php
} ?>
<?php if (isset($map_stamenrelief) && $map_stamenrelief) {
?> "<?php echo escape($lang['map_stamenrelief']);?>" : stamen_relief, <?php
} ?>
<?php if (isset($map_stamenwatercolor) && $map_stamenwatercolor) {
?> "<?php echo escape($lang['map_stamenwatercolor']);?>" : stamen_watercolor <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_hydda_group']);?>", <!--Hydda group-->
expanded: true,
layers: {
<?php if (isset($map_hyddafull) && $map_hyddafull) {
?> "<?php echo escape($lang['map_hyddafull']);?>" : hydda_full, <?php
} ?>
<?php if (isset($map_hyddabase) && $map_hyddabase) {
?> "<?php echo escape($lang['map_hyddabase']);?>" : hydda_base <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_nasagibs_group']);?>", <!--NASA GIBS group-->
expanded: true,
layers: {
<?php if (isset($map_nasagibscolor) && $map_nasagibscolor) {
?> "<?php echo escape($lang['map_nasagibscolor']);?>" : nasa_gibscolor, <?php
} ?>
<?php if (isset($map_nasagibsfalsecolor) && $map_nasagibsfalsecolor) {
?> "<?php echo escape($lang['map_nasagibsfalsecolor']);?>" : nasa_gibsfalsecolor, <?php
} ?>
<?php if (isset($map_nasagibsnight) && $map_nasagibsnight) {
?> "<?php echo escape($lang['map_nasagibsnight']);?>" : nasa_gibsnight <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_tf_group']);?>", <!--Thunderforest group-->
expanded: true,
layers: {
<?php if (isset($map_tfocm) && $map_tfocm) {
?> "<?php echo escape($lang['map_tfocm']);?>" : tf_ocm, <?php
} ?>
<?php if (isset($map_tftransport) && $map_tftransport) {
?> "<?php echo escape($lang['map_tftransport']);?>" : tf_transport, <?php
} ?>
<?php if (isset($map_tftransportdark) && $map_tftransportdark) {
?> "<?php echo escape($lang['map_tftransportdark']);?>" : tf_transportdark, <?php
} ?>
<?php if (isset($map_tflandscape) && $map_tflandscape) {
?> "<?php echo escape($lang['map_tflandscape']);?>" : tf_landscape, <?php
} ?>
<?php if (isset($map_tfoutdoors) && $map_tfoutdoors) {
?> "<?php echo escape($lang['map_tfoutdoors']);?>" : tf_outdoors, <?php
} ?>
<?php if (isset($map_tfpioneer) && $map_tfpioneer) {
?> "<?php echo escape($lang['map_tfpioneer']);?>" : tf_pioneer, <?php
} ?>
<?php if (isset($map_tfmobileatlas) && $map_tfmobileatlas) {
?> "<?php echo escape($lang['map_tfmobileatlas']);?>" : tf_mobileatlas, <?php
} ?>
<?php if (isset($map_tfneighbourhoodX) && $map_tfneighbourhood) {
?> "<?php echo escape($lang['map_tfneighbourhood']);?>" : tf_neighbourhood <?php
} ?>
}
},
{ groupName: "<?php echo escape($lang['map_mapbox_group']);?>", <!--Mapbox group-->
expanded: true,
layers: {
<?php if (isset($map_mapbox) && $map_mapbox) {
?> "<?php echo escape($lang['map_mapbox']);?>" : mapbox <?php
} ?>
}
}
];
<?php
} else {
?>
var baseMaps = [
<?php
foreach ($geo_leaflet_sources as $leaflet_source) {
echo "{ groupName: \"" . escape(i18n_get_translated($leaflet_source["name"])) . "\",
expanded: true,
layers: {\n";
foreach ($leaflet_source["variants"] as $variant => $varopts) {
$variantname = isset($varopts["name"]) ? $varopts["name"] : $leaflet_source["name"];
echo "\"" . $variantname . "\" : " . mb_strtolower($leaflet_source["code"] . "_" . $variant) . ",\n";
}
echo "}},\n";
}
echo "];\n";
}

794
include/map_functions.php Normal file
View File

@@ -0,0 +1,794 @@
<?php
// To add additional basemap sources, see http://leaflet-extras.github.io/leaflet-providers/preview/index.html for the provider names, attribution, maximum zoom level, and any other required provider parameters, and add to the appropriate basemap group below or create a new basemap group. Will also need to add additional code into the <!--Determine basemaps and map groups for user selection--> section on each PHP page using Leaflet maps (../pages/geo_search.php), the Leaflet Providers section in ../include/config.default.php, and the appropriate providers group section in ../languages/en.php.
// Define available Leaflet basemaps groups and layers using leaflet.providers.js, L.TileLayer.PouchDBCached.js, and styledLayerControl.js.
use Gettext\Languages\Exporter\Php;
/**
* Generates OpenStreetMap basemaps for use in a Leaflet map.
*
* This function defines various tile layers from OpenStreetMap and related providers,
* setting properties such as caching, retina display support, maximum zoom levels,
* and attribution for each layer.
*
* @return string JavaScript code that initializes the OSM basemaps for Leaflet.
*/
function leaflet_osm_basemaps()
{
global $map_default_cache, $map_retina;
return "<!--OpenStreetMap (OSM) basemap group-->
var osm_attribute = 'Map data © <a href=\"http://openstreetmap.org\">OpenStreetMap</a> contributors';
var osm_mapnik = L.tileLayer.provider('OpenStreetMap.Mapnik', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 19,
attribution: osm_attribute
});
var osm_de = L.tileLayer.provider('OpenStreetMap.DE', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 18,
attribution: osm_attribute
});
var osm_fr_attribute = '&copy; Openstreetmap France | &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>';
var osm_fr = L.tileLayer.provider('OpenStreetMap.France', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 20,
attribution: osm_fr_attribute
});
var osm_ch = L.tileLayer.provider('OpenStreetMap.CH', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 18,
attribution: osm_attribute
});
var osm_bzh = L.tileLayer.provider('OpenStreetMap.BZH', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 19,
attribution: osm_attribute
});
var osm_hot = L.tileLayer.provider('OpenStreetMap.HOT', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 19,
attribution: osm_attribute
});
var osm_hikebike = L.tileLayer.provider('HikeBike.HikeBike', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 19,
attribution: osm_attribute
});
var osm_mtb = L.tileLayer.provider('MtbMap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: osm_attribute
});
var osm_otm_attribute = 'Map data: &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>, <a href=\"http://viewfinderpanoramas.org\">SRTM</a> | Map style: &copy; <a href=\"https://opentopomap.org\">OpenTopoMap</a> (<a href=\"https://creativecommons.org/licenses/by-sa/3.0/\">CC-BY-SA</a>)';
var osm_otm = L.tileLayer.provider('OpenTopoMap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 17,
attribution: osm_otm_attribute
}); ";
}
/**
* Generates ESRI basemaps for use in a Leaflet map.
*
* This function defines various tile layers from ESRI, setting properties such as caching,
* retina display support, maximum zoom levels, and attribution for each layer.
*
* @return string JavaScript code that initializes the ESRI basemaps for Leaflet.
*/
function leaflet_esri_basemaps() // ESRI basemaps.
{
global $map_default_cache, $map_retina;
return "<!--ESRI basemap group-->
var esri_street_attribute = 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012';
var esri_street = L.tileLayer.provider('Esri.WorldStreetMap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: esri_street_attribute
});
var esri_delorme_attribute = 'Tiles &copy; Esri &mdash; Copyright: &copy;2012 DeLorme';
var esri_delorme = L.tileLayer.provider('Esri.DeLorme', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 1,
maxZoom: 11,
attribution: esri_delorme_attribute
});
var esri_topo_attribute = 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community';
var esri_topo = L.tileLayer.provider('Esri.WorldTopoMap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: esri_topo_attribute
});
var esri_imagery_attribute = 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
var esri_imagery = L.tileLayer.provider('Esri.WorldImagery', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: esri_imagery_attribute
});
var esri_terrain_attribute = 'Tiles &copy; Esri &mdash; Source: USGS, Esri, TANA, DeLorme, and NPS';
var esri_terrain = L.tileLayer.provider('Esri.WorldTerrain', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 13,
attribution: esri_terrain_attribute
});
var esri_relief_attribute = 'Tiles &copy; Esri &mdash; Source: Esri';
var esri_relief = L.tileLayer.provider('Esri.WorldShadedRelief', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 13,
attribution: esri_relief_attribute
});
var esri_physical_attribute = 'Tiles &copy; Esri &mdash; Source: US National Park Service';
var esri_physical = L.tileLayer.provider('Esri.WorldPhysical', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 8,
attribution: esri_physical_attribute
});
var esri_ocean_attribute = 'Tiles &copy; Esri &mdash; Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri';
var esri_ocean = L.tileLayer.provider('Esri.OceanBasemap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 13,
attribution: esri_ocean_attribute
});
var esri_natgeo_attribute = 'Tiles &copy; Esri &mdash; National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC';
var esri_natgeo = L.tileLayer.provider('Esri.NatGeoWorldMap', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 16,
attribution: esri_natgeo_attribute
});
var esri_gray_attribute = 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ';
var esri_gray = L.tileLayer.provider('Esri.WorldGrayCanvas', {
useCache: '" . ( $map_default_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 16,
attribution: esri_gray_attribute
}); ";
}
function leaflet_stamen_basemaps() // Stamen basemaps.
{
global $map_layer_cache, $map_retina;
return "<!--Stamen basemap group-->
var stamen_attribute = 'Map tiles by <a href=\"http://stamen.com\">Stamen Design</a>, <a href=\"http://creativecommons.org/licenses/by/3.0\">CC BY 3.0</a> &mdash; Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>';
var stamen_toner = L.tileLayer.provider('Stamen.Toner', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 20,
ext: 'png',
attribution: stamen_attribute
});
var stamen_tonerlt = L.tileLayer.provider('Stamen.TonerLite', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 20,
ext: 'png',
attribution: stamen_attribute
});
var stamen_tonerback = L.tileLayer.provider('Stamen.TonerBackground', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 20,
ext: 'png',
attribution: stamen_attribute
});
var stamen_terrain = L.tileLayer.provider('Stamen.Terrain', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 18,
ext: 'png',
attribution: stamen_attribute
});
var stamen_terrainback = L.tileLayer.provider('Stamen.TerrainBackground', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 18,
ext: 'png',
attribution: stamen_attribute
});
var stamen_relief = L.tileLayer.provider('Stamen.TopOSMRelief', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 0,
maxZoom: 20,
ext: 'jpg',
attribution: stamen_attribute
});
var stamen_watercolor = L.tileLayer.provider('Stamen.Watercolor', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 1,
maxZoom: 16,
ext: 'jpg',
attribution: stamen_attribute
}); ";
}
/**
* Generates Hydda basemaps for use in a Leaflet map.
*
* @return string JavaScript code that initializes the Hydda basemaps for Leaflet.
*/
function leaflet_hydda_basemaps() // Hydda basemaps.
{
global $map_layer_cache, $map_retina;
return "<!--Hydda basemap group-->
var hydda_attribute = 'Tiles courtesy of <a href=\"http://openstreetmap.se/\" target=\"_blank\">OpenStreetMap Sweden</a> &mdash; Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>';
var hydda_full = L.tileLayer.provider('Hydda.Full', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 18,
attribution: hydda_attribute
});
var hydda_base = L.tileLayer.provider('Hydda.Base', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 18,
attribution: hydda_attribute
}); ";
}
/**
* Generates NASA basemaps for use in a Leaflet map.
*
* @return string JavaScript code that initializes the NASA basemaps for Leaflet.
*/
function leaflet_nasa_basemaps() // NASA basemaps.
{
global $map_layer_cache, $map_retina;
return "<!--NASA GIBS basemap group-->
var nasa_attribute = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (<a href=\"https://earthdata.nasa.gov\">ESDIS</a>) with funding provided by NASA/HQ.';
var nasa_gibscolor = L.tileLayer.provider('NASAGIBS.ModisTerraTrueColorCR', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 1,
maxZoom: 9,
format: 'jpg',
attribution: nasa_attribute
});
var nasa_gibsfalsecolor = L.tileLayer.provider('NASAGIBS.ModisTerraBands367CR', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 1,
maxZoom: 9,
format: 'jpg',
attribution: nasa_attribute
});
var nasa_gibsnight = L.tileLayer.provider('NASAGIBS.ViirsEarthAtNight2012', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
minZoom: 1,
maxZoom: 8,
format: 'jpg',
attribution: nasa_attribute
}); ";
}
/**
* Generates USGS basemaps for use in a Leaflet map.
*
* @return string JavaScript code that initializes the USGS basemaps for Leaflet.
*/
function leaflet_usgs_basemaps() // U.S. Geological Survey The National Map basemaps.
{
global $map_layer_cache, $map_retina;
return "<!--USGS The National Map basemaps group-->
var usgstnm_attribute = 'Map data <a href=\"https://www.doi.gov\">U.S. Department of the Interior</a> | <a href=\"https://www.usgs.gov\">U.S. Geological Survey</a>';
var usgs_topo = L.tileLayer.provider('USGSTNM.USTopo', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: usgstnm_attribute
});
var usgs_imagery = L.tileLayer.provider('USGSTNM.USImagery', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: usgstnm_attribute
});
var usgs_imagerytopo = L.tileLayer.provider('USGSTNM.USImageryTopo', {
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: usgstnm_attribute
}); ";
}
/**
* Generates Thunderforest basemaps for use in a Leaflet map.
*
* @return string JavaScript code that initializes the Thunderforest basemaps for Leaflet.
*/
function leaflet_thunderforest_basemaps() // Thunderforest basemaps.
{
global $map_layer_cache, $map_retina, $map_tfapi;
return "<!--Thunderforest basemap group (requires an API key)-->
var tf_attribute = '&copy; <a href=\"http://www.thunderforest.com/\">Thunderforest</a>, &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>';
var tf_ocm = L.tileLayer.provider('Thunderforest.OpenCycleMap', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_transport = L.tileLayer.provider('Thunderforest.Transport', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_transportdark = L.tileLayer.provider('Thunderforest.TransportDark', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_landscape = L.tileLayer.provider('Thunderforest.Landscape', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_outdoors = L.tileLayer.provider('Thunderforest.Outdoors', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_pioneer = L.tileLayer.provider('Thunderforest.Pioneer', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_mobileatlas = L.tileLayer.provider('Thunderforest.MobileAtlas', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
});
var tf_neighbourhood = L.tileLayer.provider('Thunderforest.Neighbourhood', {
apikey: '<?php echo $map_tfapi?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
maxZoom: 22,
attribution: tf_attribute
}); ";
}
function leaflet_mapbox_basemaps() // Mapbox basemaps.
{
global $map_layer_cache, $map_retina, $map_mapboxid, $map_mapboxtoken, $map_mapboxattribution;
return "<!--Mapbox basemaps group (requires API keys)-->
var mapbox = L.tileLayer.provider('MapBox', {
id: '<?php echo $map_mapboxid?>',
accessToken: '<?php echo $map_mapboxtoken?>',
useCache: '" . ( $map_layer_cache ? "true" : "false" ) . "',
detectRetina: '" . ( $map_retina ? "true" : "false" ) . "',
attribution: '<?php echo $map_mapboxattribution?>'
}); ";
}
/**
* Generates Mapbox basemaps for use in a Leaflet map.
*
* This function initializes a Mapbox tile layer, requiring an API ID and access token.
* It also allows configuration for caching, retina display support, and attribution.
*
* @return string JavaScript code that initializes the Mapbox basemap for Leaflet.
*/
function leaflet_map_zoom($map_zoom)
{
global $resource, $geolocation_default_bounds;
// If no zoom level is set or is non-numeric, define as 0 to enable automatic zoom assignment below.
$zoom = trim((string) $map_zoom);
if (!is_int_loose($zoom)) {
$zoom = 2;
$arr_bounds = explode(",", $geolocation_default_bounds);
if (count($arr_bounds) == 3) {
$zoom = end($arr_bounds);
}
}
if (!($zoom >= 2 && $zoom <= 21)) {
$zoom = 16;
$siglon = round(100000 * abs($resource['geo_long'])) % 100000;
$siglat = round(100000 * abs($resource['geo_lat'])) % 100000;
if ($siglon % 100000 == 0 && $siglat % 100000 == 0) {
$zoom = 3;
} elseif ($siglon % 10000 == 0 && $siglat % 10000 == 0) {
$zoom = 6;
} elseif ($siglon % 1000 == 0 && $siglat % 1000 == 0) {
$zoom = 10;
} elseif ($siglon % 100 == 0 && $siglat % 100 == 0) {
$zoom = 15;
}
}
return $zoom;
}
/**
* Parses the resource polygon string to extract latitude and longitude bounds and formats the polygon string.
*
* @param array $fields The resource fields array containing polygon data.
* @param bool $minmax Flag to determine if minimum and maximum latitude and longitude values should be calculated.
* @return array An associative array containing the minimum and maximum latitude and longitude values,
* as well as the formatted polygon string for Leaflet display.
*/
function leaflet_polygon_parsing($fields, $minmax = true)
{
global $map_polygon_field;
// Search resource $fields array for the $map_polygon_field.
$key1 = array_search($map_polygon_field, array_column($fields, 'ref'));
if ($minmax) {
// Strip coordinate pair parathenses from polygon array.
$values = str_replace(')', '', str_replace('(', '', explode(',', $fields[$key1]['value'])));
// Determine minimum and maximum latitude values.
$lat_values = array($values[0], $values[2], $values[4], $values[6]);
$polygon['lat_min'] = min($lat_values);
$polygon['lat_max'] = max($lat_values);
// Determine minimum and maximum longitude values.
$long_values = array($values[1], $values[3], $values[5], $values[7]);
$polygon['long_min'] = min($long_values);
$polygon['long_max'] = max($long_values);
}
// Format polygon string for Leaflet footprint display below.
$polygon1 = str_replace('(', '[', $fields[$key1]['value']);
$polygon1 = str_replace(')', ']', $polygon1);
$polygon['values'] = '[' . $polygon1 . ']';
return $polygon;
}
/**
* Validates geolocation coordinates to ensure they are numeric and within acceptable bounds.
*
* @param mixed $coordinate The coordinate value to check.
* @param string $type The type of coordinate ('latitude' or 'longitude').
* @return bool True if the coordinate is valid; otherwise, false.
*/
function leaflet_coordinate_check($coordinate, $type)
{
$check = false;
if (!is_numeric($coordinate)) {
return false;
}
if ($type == 'latitude' && $coordinate >= -20037508.34 && $coordinate <= 20037508.34) {
$check = true;
}
if ($type == 'longitude' && $coordinate >= -20037508.34 && $coordinate <= 20037508.34) {
$check = true;
}
return $check;
}
/**
* Creates a map color markers legend for Leaflet maps.
*
* This function generates HTML for a legend that displays markers with colors corresponding to resource types
* or custom metadata field values. If a custom metadata field is defined, the legend reflects that; otherwise,
* it shows the default resource types and their associated colors.
*
* @return void Outputs the HTML for the legend.
*/
function leaflet_markers_legend()
{
global $lang, $marker_metadata_field, $marker_metadata_array, $MARKER_COLORS;
if (!isset($marker_metadata_field) || $lang['custom_metadata_markers'] == '') { ?>
<b> <?php echo escape($lang['legend_text']); ?>&nbsp;</b>
<?php
$restypes = get_resource_types();
foreach ($restypes as $restype) {
$markercolour = (isset($restype["colour"]) && $restype["colour"] > 0) ? (int)$restype["colour"] : ($restype['ref'] % count($MARKER_COLORS));
echo "<img src='../lib/leaflet_plugins/leaflet-colormarkers-1.0.0/img/marker-icon-" . strtolower($MARKER_COLORS[$markercolour]) . ".png' alt='" . $MARKER_COLORS[$markercolour] . " Icon' style='width:19px;height:31px;'>" . $restype["name"] . "&nbsp;";
}
} else // Custom metadata field color markers legend.
{ ?>
<b> <?php echo escape($lang['custom_metadata_markers']); ?>&nbsp;</b> <?php
// Loop through and create the custom color marker legend text, ignoring the first 'unset' item
for ($i = 0; $i < count($marker_metadata_array); $i++) {
$ltext[$i] = $marker_metadata_array[$i]['min'] . "-" . $marker_metadata_array[$i]['max'];
}
for ($i = 0; $i < count($marker_metadata_array); $i++) {
?> <img src="../lib/leaflet_plugins/leaflet-colormarkers-1.0.0/img/marker-icon-<?php echo strtolower($MARKER_COLORS[$i])?>.png" alt="<?php echo $MARKER_COLORS[$i]; ?> Icon" style="width:19px;height:31px;"> <?php echo $ltext[$i]; ?> &nbsp; <?php
}
}
}
/**
* Adds map providers for Leaflet maps.
*
* This function generates a JavaScript snippet that defines various tile layer providers for use in Leaflet maps.
* It supports OpenStreetMap and ESRI basemaps, as well as custom providers defined in the global variable `$geo_leaflet_sources`.
* The function also includes options for caching tile layers and handling different zoom levels and attribution.
*
* @return void Outputs the HTML and JavaScript for adding map providers to Leaflet.
*/
function header_add_map_providers()
{
global $geo_leaflet_sources, $baseurl, $geo_tile_caching;
?>
<script>
// Copied from leaflet-providers.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['leaflet'], factory);
} else if (typeof modules === 'object' && module.exports) {
// define a Common JS module that relies on 'leaflet'
module.exports = factory(require('leaflet'));
} else {
// Assume Leaflet is loaded into global object L already
factory(L);
}
}(this, function (L) {
'use strict';
L.TileLayer.Provider = L.TileLayer.extend({
initialize: function (arg, options) {
var providers = L.TileLayer.Provider.providers;
var parts = arg.split('.');
var providerName = parts[0];
var variantName = parts[1];
if (!providers[providerName]) {
throw 'No such provider (' + providerName + ')';
}
var provider = {
url: providers[providerName].url,
options: providers[providerName].options
};
// overwrite values in provider from variant.
if (variantName && 'variants' in providers[providerName]) {
if (!(variantName in providers[providerName].variants)) {
throw 'No such variant of ' + providerName + ' (' + variantName + ')';
}
var variant = providers[providerName].variants[variantName];
var variantOptions;
if (typeof variant === 'string') {
variantOptions = {
variant: variant
};
} else {
variantOptions = variant.options;
}
provider = {
url: variant.url || provider.url,
options: L.Util.extend({}, provider.options, variantOptions)
};
}
// replace attribution placeholders with their values from toplevel provider attribution,
// recursively
var attributionReplacer = function (attr) {
if (attr.indexOf('{attribution.') === -1) {
return attr;
}
return attr.replace(/\{attribution.(\w*)\}/g,
function (match, attributionName) {
return attributionReplacer(providers[attributionName].options.attribution);
}
);
};
provider.options.attribution = attributionReplacer(provider.options.attribution);
// Compute final options combining provider options with any user overrides
var layerOpts = L.Util.extend({}, provider.options, options);
L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts);
}
});
/**
* Definition of providers.
* see http://leafletjs.com/reference.html#tilelayer for options in the options map.
*/
L.TileLayer.Provider.providers = {
<?php
foreach ($geo_leaflet_sources as $leaflet_source) {
echo escape($leaflet_source["code"]) . ": {\n";
if ($geo_tile_caching) {
// Is this the search page? If so need to get collection ID to authenticate external shares
$searchparts = explode(" ", getval("search", ""));
$collection = str_replace("!collection", "", $searchparts[0]);
$resource = getval("ref", ""); // For resource view page
$urlparams = array(
"provider" => $leaflet_source["code"],
"resource" => $resource,
"collection" => $collection,
"k" => getval("k", ""),
);
$sourceurl = generateURL($baseurl . "/pages/ajax/tiles.php", $urlparams) . "&x={x}&y={y}&z={z}";
} else {
$sourceurl = $leaflet_source["url"];
}
echo " url: '" . $sourceurl . "',\n";
echo " options: {\n";
if (isset($leaflet_source["maxZoom"]) && is_int_loose($leaflet_source["maxZoom"])) {
echo " maxZoom: " . (int)$leaflet_source["maxZoom"] . ",\n";
}
if (isset($leaflet_source["attribution"])) {
echo " attribution: '" . $leaflet_source["attribution"] . "',\n";
}
echo " },\n"; // End of options
echo " variants: {\n";
foreach ($leaflet_source["variants"] as $variant => &$variantdata) {
echo $variant . ": {\n ";
if (isset($variantdata["url"])) {
if ($geo_tile_caching) {
$urlparams["variant"] = $variant;
$variantdata["url"] = generateURL($baseurl . "/pages/ajax/tiles.php", $urlparams) . "&x={x}&y={y}&z={z}";
}
echo " url: '" . $variantdata["url"] . "'\n";
}
echo "},\n";
}
echo " },\n"; // End of variants
echo "},\n"; // End of leaflet source
}
?>
ResourceSpace: {
url: '<?php echo $baseurl; ?>/pages/ajax/tiles.php?x={x}&y={y}&z={z}',
options: {
maxZoom: 3,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
variants: { OSM: {}}
}
};
L.tileLayer.provider = function (provider, options) {
return new L.TileLayer.Provider(provider, options);
};
return L;
}));
</script>
<?php
}
/**
* Checks the current page and includes necessary geolocation libraries for Leaflet maps.
*
* This function loads the Leaflet Control Geocoder plugin and checks if the current page requires geolocation capabilities.
* It also handles polyfills for browser compatibility, specifically for Internet Explorer and Edge.
*
* @return void Outputs the HTML and JavaScript for including geolocation libraries if applicable.
*/
function get_geolibraries()
{
global $baseurl, $pagename, $map_default_cache, $map_layer_cache, $geo_leaflet_maps_sources,
$map_zoomnavbar, $map_kml;
$map_pages = array(
"geo_edit",
"geo_search",
"search",
"view",
"edit",
);
if (!in_array($pagename, $map_pages)) {
return false;
}?>
<!--Leaflet Control Geocoder 1.10.0 plugin files-->
<link rel="stylesheet" href="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-control-geocoder-1.10.0/dist/Control.Geocoder.css"/>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-control-geocoder-1.10.0/dist/Control.Geocoder.min.js"></script>
<?php
}
/**
* Set bounds for default map view (geo_search.php and geo_edit.php)
*
* @return void
*/
function set_geo_map_centerview()
{
global $geolocation_default_bounds;
$centerparts = explode(",", $geolocation_default_bounds);
echo "\n mapcenterview= L.CRS.EPSG3857.unproject(L.point(" . $centerparts[0] . "," . $centerparts[1] . "));\n";
echo "mapdefaultzoom = " . (int)$centerparts[2] . ";\n";
}
/**
* Outputs the script tag for including additional Leaflet plugins.
*
* This function loads the necessary JavaScript for the Leaflet Marker Cluster library, which helps in managing
* large sets of markers on Leaflet maps by clustering them for better visualization.
*
* @return void Outputs the script tag for the Marker Cluster plugin.
*/
function get_geo_maps_scripts()
{
global $baseurl;
?>
<script src="<?php echo $baseurl?>/lib/leaflet_plugins/leaflet-markercluster-1.4.1/dist/leaflet.markercluster.min.js"></script>
<?php
}

View File

@@ -0,0 +1,73 @@
<?php
// Leaflet.js Basemap Processing
if ($geo_leaflet_maps_sources) {
echo leaflet_osm_basemaps();
echo leaflet_usgs_basemaps();
echo leaflet_esri_basemaps();
echo leaflet_stamen_basemaps();
echo leaflet_hydda_basemaps();
echo leaflet_nasa_basemaps();
echo leaflet_thunderforest_basemaps();
echo leaflet_mapbox_basemaps();
?>
<!-- Define Leaflet default basemap attribution-->
<?php switch ($map_default) {
case 'OpenStreetMap.Mapnik' || 'OpenStreetMap.DE' || 'OpenStreetMap.BZH' || 'OpenStreetMap.HOT' || 'MtbMap' || 'HikeBike.HikeBike':
?> var default_attribute = osm_attribute; <?php
break;
case 'OpenStreetMap.France':
?> var default_attribute = osm_fr_attribute; <?php
break;
case 'OpenTopoMap':
?> var default_attribute = osm_otm_attribute; <?php
break;
case 'OpenMapSurfer.Roads':
?> var default_attribute = oms_attribute; <?php
break;
default:
?> var default_attribute = ''; <?php
}
} else {
foreach ($geo_leaflet_sources as $leaflet_source) {
foreach ($leaflet_source["variants"] as $variant => $varopts) {
$varcode = mb_strtolower($leaflet_source["code"] . "_" . $variant);
$varoptions = array();
$varoptions["maxZoom"] = $leaflet_source["maxZoom"];
$varoptions["attribution"] = $leaflet_source["attribution"];
// Options can be set at root or overridden by a variant
if (isset($varopts["options"])) {
foreach ($varopts["options"] as $option => $optval) {
$varoptions[$option] = $optval;
}
}
$attribution = isset($varoptions["options"]["attribution"]) ? $varoptions["options"]["attribution"] : $varoptions["attribution"];
echo "var " . $varcode . "_attribute = '" . $attribution . "';\n";
if (mb_strtolower($map_default) == mb_strtolower($leaflet_source["code"] . "." . $variant)) {
echo "default_attribute = '" . $attribution . "';\n";
}
echo "var " . $varcode . " = L.tileLayer.provider('" . $leaflet_source["code"] . "." . $variant . "', {\n";
echo " useCache: '" . ($map_default_cache ? "true" : "false") . "',\n";
echo " detectRetina: '" . ($map_retina ? "true" : "false") . "',\n";
foreach ($varoptions as $varoption => $optval) {
if ($varoption == "attribution") {
echo " " . escape($varoption) . ": '" . $optval . "',\n";
} else {
echo " " . escape($varoption) . ": " . escape($optval) . ",\n";
}
}
echo "});\n";
}
}
echo "var rs_default = L.tileLayer.provider('ResourceSpace.OSM', {
useCache: 'true',
detectRetina: 'false',
maxZoom: 3,
attribution: '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',
});\n\n";
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,407 @@
<?php
/**
* Metadata related functions
*
* Functions related to resource metadata in general
*
* @package ResourceSpace\Includes
*/
/**
* Run FITS on a file and get the output back
*
* @uses get_utility_path()
* @uses run_command()
*
* @param string $file_path Physical path to the file
*
* @return bool | SimpleXMLElement
*/
function runFitsForFile($file_path)
{
global $fits_path;
$fits = get_utility_path('fits');
$fits_path_escaped = escapeshellarg($fits_path);
$file = escapeshellarg($file_path);
if (false === $fits) {
debug('ERROR: FITS library could not be located!');
return false;
}
putenv("LD_LIBRARY_PATH={$fits_path_escaped}/tools/mediainfo/linux");
$return = run_command("{$fits} -i {$file} -xc");
if (trim($return) != "") {
return new SimpleXMLElement($return);
}
return false;
}
/**
* Get metadata value for a FITS field
*
* @param SimpleXMLElement $xml FITS metadata XML
* @param string $fits_field A ResourceSpace specific FITS field mapping which allows ResourceSpace to know exactly where
* to look for that value in XML by converting it to an XPath query string.
* Example:
* video.mimeType would point to
*
* <metadata>
* <video>
* [...]
* <mimeType toolname="MediaInfo" toolversion="0.7.75" status="SINGLE_RESULT">video/quicktime</mimeType>
* [...]
* </video>
* </metadata>
*
* @return string
*/
function getFitsMetadataFieldValue(SimpleXMLElement $xml, $fits_field)
{
// IMPORTANT: Not using "fits" namespace (or any for that matter) will yield no results
// TODO: since there can be multiple namespaces (especially if run with -xc options) we might need to implement the
// ability to use namespaces directly from RS FITS Field.
$xml->registerXPathNamespace('fits', 'http://hul.harvard.edu/ois/xml/ns/fits/fits_output');
// Convert fits field mapping from rs format to namespaced XPath format
// Example rs field mapping for an xml element value
// rs field is one.two.three which converts to an xpath filter of //fits:one/fits:two/fits:three
// Example rs field mapping for an xml attribute value (attributes are not qualified by the namespace)
// rs attribute is one.two.three/@four which converts to an xpath filter of //fits:one/fits:two/fits:three/@four
$fits_path = explode('.', $fits_field);
// Reassemble with the namespace
$fits_filter = "//fits:" . implode('/fits:', $fits_path);
$result = $xml->xpath($fits_filter);
if (!isset($result) || false === $result || 0 === count($result)) {
return '';
}
// First result entry carries the element or attribute value
if (isset($result[0]) && !is_array($result[0])) {
return $result[0];
}
return '';
}
/**
* Extract FITS metadata from a file for a specific resource.
*
* @uses get_resource_data()
* @uses ps_query()
* @uses runFitsForFile()
* @uses getFitsMetadataFieldValue()
* @uses update_field()
*
* @param string $file_path Path to the file from which you will extract FITS metadata
* @param integer|array $resource Resource ID or resource array (as returned by get_resource_data())
*
* @return boolean
*/
function extractFitsMetadata($file_path, $resource)
{
if (get_utility_path('fits') === false) {
return false;
}
if (!file_exists($file_path)) {
return false;
}
if (!is_array($resource) && !is_numeric($resource)) {
return false;
}
if (!is_array($resource) && is_numeric($resource) && 0 < $resource) {
$resource = get_resource_data($resource);
}
$resource_type = $resource['resource_type'];
// Get a list of all the fields that have a FITS field set
$allfields = get_resource_type_fields($resource_type);
$rs_fields_to_read_for = array_filter($allfields, function ($field) {
return trim((string)$field["fits_field"]) != "";
});
if (0 === count($rs_fields_to_read_for)) {
return false;
}
// Run FITS and extract metadata
$fits_xml = runFitsForFile($file_path);
if (!$fits_xml) {
return false;
}
$fits_updated_fields = array();
foreach ($rs_fields_to_read_for as $rs_field) {
$fits_fields = explode(',', (string)$rs_field['fits_field']);
foreach ($fits_fields as $fits_field) {
$fits_field_value = getFitsMetadataFieldValue($fits_xml, $fits_field);
if ('' == $fits_field_value) {
continue;
}
update_field($resource['ref'], $rs_field['ref'], $fits_field_value);
$fits_updated_fields[] = $rs_field['ref'];
}
}
if (0 < count($fits_updated_fields)) {
return true;
}
return false;
}
/**
* Check date conforms to "yyyy-mm-dd hh:mm" format or any valid partital of that e.g. yyyy-mm.
*
* @uses check_date_parts()
*
* @param string string form of the date to check
*
* @return string
*/
function check_date_format($date)
{
global $lang;
if (is_null($date)) {
$date = "";
}
// Check the format of the date to "yyyy-mm-dd hh:mm:ss"
if (
(preg_match("/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})$/", $date, $parts))
// Check the format of the date to "yyyy-mm-dd hh:mm"
|| (preg_match("/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})$/", $date, $parts))
// Check the format of the date to "yyyy-mm-dd"
|| (preg_match("/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/", $date, $parts))
) {
if (!checkdate($parts[2], $parts[3], $parts[1])) {
return str_replace("%date%", $date, $lang["invalid_date_error"]);
}
return str_replace("%date%", $date, check_date_parts($parts));
}
// Check the format of the date to "yyyy-mm" pads with 01 to ensure validity
elseif (preg_match("/^([0-9]{4})-([0-9]{2})$/", $date, $parts)) {
array_push($parts, '01');
return str_replace("%date%", $date, check_date_parts($parts));
}
// Check the format of the date to "yyyy" pads with 01 to ensure validity
elseif (preg_match("/^([0-9]{4})$/", $date, $parts)) {
array_push($parts, '01', '01');
return str_replace("%date%", $date, check_date_parts($parts));
}
// If it matches nothing return unknown format error
return str_replace("%date%", $date, $lang["unknown_date_format_error"]);
}
/**
* Check datepart conforms to its formatting and error out each section accordingly
*
* @param array array of the date parts
*
* @return string
*/
function check_date_parts($parts)
{
global $lang;
// Initialise error list holder
$invalid_parts = array();
// Check day part
if (!checkdate('01', $parts[3], '2000')) {
array_push($invalid_parts, 'day');
}
// Check day month
if (!checkdate($parts[2], '01', '2000')) {
array_push($invalid_parts, 'month');
}
// Check year part
if (!checkdate('01', '01', $parts[1])) {
array_push($invalid_parts, 'year');
}
// Check time part
if (
isset($parts[4])
&& isset($parts[5])
&& !strtotime($parts[4] . ':' . $parts[5])
) {
array_push($invalid_parts, 'time');
}
// No errors found return false
if (empty($invalid_parts)) {
return false;
}
// Return errors found
else {
return str_replace("%parts%", implode(", ", $invalid_parts), $lang["date_format_error"]);
}
}
function check_view_display_condition($fields, $n, $fields_all)
{
#Check if field has a display condition set
$displaycondition = true;
if ($fields[$n]["display_condition"] != "") {
$s = explode(";", $fields[$n]["display_condition"]);
$condref = 0;
foreach ($s as $condition) { # Check each condition
$displayconditioncheck = false;
$s = explode("=", $condition);
for ($cf = 0; $cf < count($fields_all); $cf++) { # Check each field to see if needs to be checked
if ($s[0] == $fields_all[$cf]["name"]) { # this field needs to be checked
$checkvalues = $s[1];
$validvalues = explode("|", $checkvalues);
$validvalues = array_map("i18n_get_translated", $validvalues);
$validvalues = array_map("strtoupper", $validvalues);
$v = trim_array(explode(",", $fields_all[$cf]["value"] ?? ""));
if ($fields_all[$cf]['type'] == FIELD_TYPE_CATEGORY_TREE) {
$tree_values = array_merge(...array_map(function ($value) {
return explode('/', $value);
}, $v));
$v = array_unique(array_merge($v, $tree_values));
}
$v = array_map("i18n_get_translated", $v);
$v = array_map("strtoupper", $v);
foreach ($validvalues as $validvalue) {
if (in_array($validvalue, $v)) {
$displayconditioncheck = true;
} # this is a valid value
}
if (!$displayconditioncheck) {
$displaycondition = false;
}
}
} # see if next field needs to be checked
$condref++;
} # check next condition
}
return $displaycondition;
}
/**
* updates the value of fieldx field further to a metadata field value update
*
* @param integer $metadata_field_ref - metadata field ref
*
*/
function update_fieldx(int $metadata_field_ref): void
{
global $NODE_FIELDS;
if ($metadata_field_ref > 0 && in_array($metadata_field_ref, get_resource_table_joins())) {
$fieldinfo = get_resource_type_field($metadata_field_ref);
$allresources = ps_array("SELECT ref value FROM resource WHERE ref>0 ORDER BY ref ASC", []);
if (in_array($fieldinfo['type'], $NODE_FIELDS)) {
if ($fieldinfo['type'] === FIELD_TYPE_CATEGORY_TREE) {
$all_tree_nodes_ordered = get_cattree_nodes_ordered($fieldinfo['ref'], null, true);
// remove the fake "root" node which get_cattree_nodes_ordered() is adding since we won't be using get_cattree_node_strings()
array_shift($all_tree_nodes_ordered);
$all_tree_nodes_ordered = array_values($all_tree_nodes_ordered);
foreach ($allresources as $resource) {
// category trees are using full paths to node names
$resource_nodes = array_keys(get_cattree_nodes_ordered($fieldinfo['ref'], $resource, false));
$node_names_paths = [];
foreach ($resource_nodes as $node_ref) {
$node_names_paths[] = implode(
'/',
array_column(compute_node_branch_path($all_tree_nodes_ordered, $node_ref), 'name')
);
}
update_resource_field_column(
$resource,
$metadata_field_ref,
implode($GLOBALS['field_column_string_separator'], $node_names_paths)
);
}
} else {
foreach ($allresources as $resource) {
$resnodes = get_resource_nodes($resource, $metadata_field_ref, true);
uasort($resnodes, 'node_orderby_comparator');
$resvals = array_column($resnodes, "name");
$resdata = implode($GLOBALS['field_column_string_separator'], $resvals);
update_resource_field_column($resource, $metadata_field_ref, $resdata);
}
}
} else {
foreach ($allresources as $resource) {
update_resource_field_column($resource, $metadata_field_ref, get_data_by_field($resource, $metadata_field_ref));
}
}
}
}
/**
* Extract and store dimensions, resolution, and unit (if available) from exif data
* Exiftool output format (tab delimited): widthxheight resolution unit (e.g., 1440x1080 300 inches)
*
* @param string $file_path Path to the original file.
* @param int $ref Reference of the resource.
* @param boolean $remove_original Option to remove the original record. Used by update_resource_dimensions.php
*
* @return void
*/
function exiftool_resolution_calc($file_path, $ref, $remove_original = false)
{
$exiftool_fullpath = get_utility_path("exiftool");
$command = $exiftool_fullpath . " -s -s -s %s ";
$command .= escapeshellarg($file_path);
$exif_output = run_command(sprintf($command, "-composite:imagesize"));
if ($exif_output != '') {
if ($remove_original) {
ps_query("DELETE FROM resource_dimensions WHERE resource= ?", ['i', $ref]);
}
$wh = explode("x", $exif_output);
if (count($wh) > 1) {
$width = $wh[0];
$height = $wh[1];
$filesize = filesize_unlimited($file_path);
$sql_insert = "insert into resource_dimensions (resource,width,height,file_size";
$sql_params = [
's', $ref,
'i', $width,
'i', $height,
's', $filesize
];
$exif_resolution = run_command(sprintf($command, '-xresolution'));
if (is_numeric($exif_resolution) && $exif_resolution > 0) {
$sql_insert .= ',resolution';
$sql_params[] = 'd';
$sql_params[] = $exif_resolution;
}
$exif_unit = run_command(sprintf($command, '-resolutionunit'));
if ($exif_unit != '') {
$sql_insert .= ',unit';
$sql_params[] = 's';
$sql_params[] = $exif_unit;
}
$sql_insert .= ")";
$sql_values = "values (" . ps_param_insert((count($sql_params) / 2)) . ")";
$sql = $sql_insert . $sql_values;
ps_query($sql, $sql_params);
}
}
}

View File

@@ -0,0 +1,585 @@
<?php
// function to automatically migrate options lists to nodes
function migrate_resource_type_field_check(&$resource_type_field)
{
if (
!isset($resource_type_field['options']) ||
is_null($resource_type_field['options']) ||
$resource_type_field['options'] == '' ||
($resource_type_field['type'] == 7 && preg_match('/^' . MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX_CATEGORY_TREE . '/', $resource_type_field['options'])) ||
preg_match('/^' . MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX . '/', $resource_type_field['options'])
) {
return; // get out of here as there is nothing to do
}
// Delete all nodes for this resource type field
// This is to prevent systems that migrated to have old values that have been removed from a default field
// example: Country field
delete_nodes_for_resource_type_field($resource_type_field['ref']);
if ($resource_type_field['type'] == 7) { // category tree
migrate_category_tree_to_nodes($resource_type_field['ref'], $resource_type_field['options']);
// important! this signifies that this field has been migrated by prefixing with -1,,MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX
ps_query("UPDATE `resource_type_field` SET `options` = CONCAT('" . MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX_CATEGORY_TREE . "', options) WHERE `ref` = ?", array("i", $resource_type_field['ref']));
} elseif ($resource_type_field['type'] == FIELD_TYPE_DYNAMIC_KEYWORDS_LIST) {
$options = preg_split('/\s*,\s*/', $resource_type_field['options']);
$order = 10;
foreach ($options as $option) {
set_node(null, $resource_type_field['ref'], $option, null, $order);
$order += 10;
}
// important! this signifies that this field has been migrated by -replacing- with MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX
// Note as the dynamic keyword fields can reach the database column length limit this no longer appends the old 'options' text - the migration script will pick up any missing options later from existing resource_data values
ps_query("UPDATE `resource_type_field` SET `options` = '" . MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX . "' WHERE `ref` = ?", array("i", $resource_type_field['ref']));
} else // general comma separated fields
{
$options = preg_split('/\s*,\s*/', $resource_type_field['options']);
$order = 10;
foreach ($options as $option) {
set_node(null, $resource_type_field['ref'], $option, null, $order);
$order += 10;
}
// important! this signifies that this field has been migrated by prefixing with MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX
ps_query("UPDATE `resource_type_field` SET `options` = CONCAT('" . MIGRATION_FIELD_OPTIONS_DEPRECATED_PREFIX . "',',',options) WHERE `ref` = ?", array("i", $resource_type_field['ref']));
}
clear_query_cache("schema");
}
function migrate_category_tree_to_nodes($resource_type_field_ref, $category_tree_options)
{
$options = array();
$option_lines = preg_split('/\r\n|\r|\n/', $category_tree_options);
$order = 10;
// first pass insert current nodes into nodes table
foreach ($option_lines as $line) {
$line_fields = preg_split('/\s*,\s*/', $line);
if (count($line_fields) != 3) {
continue;
}
$id = trim($line_fields[0]);
$parent_id = trim($line_fields[1]);
$name = trim($line_fields[2]);
$ref = set_node(null, $resource_type_field_ref, $name, null, $order);
$options['node_id_' . $id] = array(
'id' => $id,
'name' => $name,
'parent_id' => $parent_id,
'order' => $order,
'ref' => $ref
);
$order += 10;
}
// second pass is to set parent refs
foreach ($options as $option) {
$ref = $option['ref'];
$name = $option['name'];
$order = $option['order'];
$parent_id = $option['parent_id'];
if ($parent_id == '') {
continue;
}
$parent_ref = isset($options['node_id_' . $parent_id]) ? $options['node_id_' . $parent_id]['ref'] : null;
set_node($ref, $resource_type_field_ref, $name, $parent_ref, $order);
}
}
function migrate_filter($filtertext, $allowpartialmigration = false)
{
global $FIXED_LIST_FIELD_TYPES;
if (trim($filtertext) == "") {
return false;
}
$all_fields = get_resource_type_fields();
// Don't migrate if already migrated
$existingrules = ps_query("SELECT ref, name FROM filter");
$logtext = "FILTER MIGRATION: Migrating filter rule. Current filter text: '" . $filtertext . "'\n";
// Check for existing rule (will only match if name hasn't been changed)
$filterid = array_search($filtertext, array_column($existingrules, 'name'));
if ($filterid !== false) {
$logtext .= "FILTER MIGRATION: - Filter already migrated. ID = " . $existingrules[$filterid]["ref"] . "\n";
return $existingrules[$filterid]["ref"];
} else {
$truncated_filter_name = mb_strcut($filtertext, 0, 200);
// Create filter. All migrated filters will have AND rules
ps_query("INSERT INTO filter (name, filter_condition) VALUES (?, ?)", array("s", $truncated_filter_name, "i", RS_FILTER_ALL));
$filterid = sql_insert_id();
$logtext .= "FILTER MIGRATION: - Created new filter. ID = " . $filterid . "'\n";
}
$filter_rules = explode(";", $filtertext);
$errors = array();
$n = 1;
foreach ($filter_rules as $filter_rule) {
$rulevalid = false;
$logtext .= "FILTER MIGRATION: -- Parsing filter rule #" . $n . " : '" . $filter_rule . "'\n";
$rule_parts = explode("=", $filter_rule);
$rulefields = $rule_parts[0];
if (isset($rule_parts[1])) {
$rulevalues = explode("|", trim($rule_parts[1]));
} else {
$errors[] = "Invalid filter, no values are set.";
return $errors;
}
// Create filter_rule
$logtext .= "FILTER MIGRATION: -- Creating filter_rule for '" . $filter_rule . "'\n";
ps_query("INSERT INTO filter_rule (filter) VALUES (?)", array("i", $filterid));
$new_filter_rule = sql_insert_id();
$logtext .= "FILTER MIGRATION: -- Created filter_rule # " . $new_filter_rule . "\n";
$nodeinsert = array(); // This will contain the SQL value sets to be inserted for this rule
$nodeinsertparams = array();
$rulenot = substr($rulefields, -1) == "!";
$node_condition = RS_FILTER_NODE_IN;
if ($rulenot) {
$rulefields = substr($rulefields, 0, -1);
$node_condition = RS_FILTER_NODE_NOT_IN;
}
// If there is an OR between the fields we need to get all the possible options (nodes) into one array
$rulefieldarr = explode("|", $rulefields);
$all_valid_nodes = array();
foreach ($rulefieldarr as $rulefield) {
$all_fields_index = array_search(mb_strtolower($rulefield), array_map("mb_strtolower", array_column($all_fields, 'name')));
$field_ref = $all_fields[$all_fields_index]["ref"];
$field_type = $all_fields[$all_fields_index]["type"];
$logtext .= "FILTER MIGRATION: --- filter field name: '" . $rulefield . "' , field id #" . $field_ref . "\n";
if (!in_array($field_type, $FIXED_LIST_FIELD_TYPES)) {
$errors[] = "Invalid field '" . $field_ref . "' specified for rule: '" . $filtertext . "', skipping";
$logtext .= "FILTER MIGRATION: --- Invalid field '" . $field_ref . "', skipping\n";
continue;
}
$field_nodes = get_nodes($field_ref, null, (FIELD_TYPE_CATEGORY_TREE == $field_type ? true : false));
$all_valid_nodes = array_merge($all_valid_nodes, $field_nodes);
}
foreach ($rulevalues as $rulevalue) {
// Check for value in field options
$logtext .= "FILTER MIGRATION: --- Checking for filter rule value : '" . $rulevalue . "'\n";
$nodeidx = array_search(mb_strtolower($rulevalue), array_map("mb_strtolower", array_column($all_valid_nodes, 'name')));
if ($nodeidx !== false) {
$nodeid = $all_valid_nodes[$nodeidx]["ref"];
$logtext .= "FILTER MIGRATION: --- field option (node) exists, node id #: " . $all_valid_nodes[$nodeidx]["ref"] . "\n";
$nodeinsert[] = "(?, ?, ?)";
$nodeinsertparams = array_merge($nodeinsertparams, array("i", $new_filter_rule, "i", $nodeid, "i", $node_condition));
if ($allowpartialmigration) {
$rulevalid = true;
} // Atleast one rule is valid so the filter can be created
} else {
$errors[] = "Invalid field option '" . $rulevalue . "' specified for rule: '" . $filtertext . "', skipping";
$logtext .= "FILTER MIGRATION: --- Invalid field option: '" . $rulevalue . "', skipping\n";
}
}
debug($logtext);
if (count($errors) > 0 && !$rulevalid) {
delete_filter($filterid);
return $errors;
}
// Insert associated filter_rules
$logtext .= "FILTER MIGRATION: -- Adding nodes to filter_rule\n";
$sql = "INSERT INTO filter_rule_node (filter_rule,node,node_condition) VALUES " . implode(',', $nodeinsert);
ps_query($sql, $nodeinsertparams);
}
debug("FILTER MIGRATION: filter migration completed for '" . $filtertext);
$logtext .= "FILTER MIGRATION: filter migration completed for '" . $filtertext . "\n";
return $filterid;
}
/**
* Utility function to generate a random UTF8 character
*
* @return string
*/
function random_char()
{
$hex_code = dechex(mt_rand(195, 202));
$hex_code .= dechex(mt_rand(128, 175));
return pack('H*', $hex_code);
}
/**
* Utility function to randomly alter date by offset
*
* @param string $fromdate - date string
* @param int $maxoffset - Maximum number of days to offset
* @return string
*/
function mix_date($fromdate, $maxoffset = 30)
{
global $mixcache;
if (isset($mixcache[md5($fromdate)])) {
return $mixcache[md5($fromdate)];
}
if (trim($fromdate == "")) {
$tstamp = time();
} else {
$tstamp = strtotime($fromdate);
}
$dateshift = 60 * 60 * 24 * $maxoffset; // How much should dates be moved
$newstamp = $tstamp + (mt_rand(-$dateshift, $dateshift));
$newdate = gmdate('Y-m-d H:i:s', $newstamp);
debug("Converted date " . $fromdate . " to " . $newdate);
// Update cache
$mixcache[md5($fromdate)] = $newdate;
return $newdate;
}
/**
* Utility function to randomly scramble string
*
* @param null|string $string Text string to scramble
* @param boolean $recurse Optionally prevent recursion (maybe called by another mix unction)
* @return string
*/
function mix_text(?string $string, bool $recurse = true): string
{
global $mixcache;
$string ??= '';
if (trim($string) === '') {
return '';
}
if (isset($mixcache[md5($string)])) {
return $mixcache[md5($string)];
}
debug("Converting string<br/>" . $string . ", recurse=" . ($recurse ? "TRUE" : "FALSE"));
// Check if another function is better
if (validateDatetime($string) && $recurse) {
debug("This is a date - calling mix_date()");
return mix_date($string);
} elseif (strpos($string, "http") === 0 && $recurse) {
debug("This is a URL - calling mix_url()");
return mix_url($string);
} elseif (get_mime_types_by_extension(parse_filename_extension($string)) !== [] && $recurse) {
debug("This is a filename - calling mix_filename()");
return mix_filename($string);
}
$numbers = '0123456789';
$uppercons = 'BCDFGHJKLMNPQRSTVWXZ';
$uppervowels = 'AEIOUY';
$lowercons = 'bcdfghjklmnpqrstvwxz';
$lowervowels = 'aeiouy';
$noreplace = "'\".,<>#-_&\$£:;^?!@+()*% \n";
$newstring = "";
$bytelength = strlen($string);
$mbytelength = mb_strlen($string);
// Simple conversion if numbers
if ($bytelength == $mbytelength && (string)(int)$string == $string) {
$newstring = mt_rand(0, (int)$string);
} else {
// Process each character
for ($i = 0; $i < $mbytelength; $i++) {
$oldchar = mb_substr($string, $i, 1);
if ($i > 3 && strpos($noreplace, $oldchar) === false) {
// Randomly add or remove character after first
$randaction = mt_rand(0, 10);
if ($randaction == 0) {
// Skip a character
$i++;
} elseif ($randaction == 1) {
// Add a character
$i--;
}
}
if ($i >= $mbytelength || $oldchar == "") {
$newstring .= substr(str_shuffle($lowervowels . $lowercons), 0, 1);
} elseif (strpos($noreplace, $oldchar) !== false) {
$newstring .= $oldchar;
} elseif (strlen($oldchar) == 1) {
// Non- multibyte
if (strpos($lowercons, $oldchar) !== false) {
$newchar = substr(str_shuffle($lowercons), 0, 1);
} elseif (strpos($uppercons, $oldchar) !== false) {
$newchar = substr(str_shuffle($uppercons), 0, 1);
} elseif (strpos($lowervowels, $oldchar) !== false) {
$newchar = substr(str_shuffle($lowervowels), 0, 1);
} elseif (strpos($uppervowels, $oldchar) !== false) {
$newchar = substr(str_shuffle($uppervowels), 0, 1);
} elseif (strpos($numbers, $oldchar) !== false) {
$newchar = substr(str_shuffle($numbers), 0, 1);
} else {
$newchar = substr(str_shuffle($noreplace), 0, 1);
}
$newstring .= $newchar;
} else {
$newchar = random_char();
$newstring .= $newchar;
} // End of multibyte conversion
}
}
// Update cache
$mixcache[md5($string)] = $newstring;
return $newstring;
}
/**
* Utility function to randomly scramble data array for exporting
*
* @param array $row - Array of data passed by reference
* @param boolean $scramblecolumns - Optional array of columns to scramble
* @return void
*/
function alter_data(&$row, $key, $scramblecolumns = array())
{
foreach ($scramblecolumns as $scramblecolumn => $scrambletype) {
$row[$scramblecolumn] = call_user_func($scrambletype, $row[$scramblecolumn]);
}
}
/**
* Utility function to scramble a URL
*
* @param string $string - URL to scramble
*
* @return string
*/
function mix_url($string)
{
global $mixcache, $baseurl;
if (trim($string) == "") {
return "";
}
if (isset($mixcache[md5($string)])) {
return $mixcache[md5($string)];
}
if (strpos($string, "pages") === 0 || strpos($string, "/pages") === 0 || strpos($string, $baseurl) === 0) {
// URL is a relative path within the system, don't scramble
return $string;
}
if (strpos($string, "://") !== false) {
$urlparts = explode("://", $string);
return $urlparts[0] . "://" . mix_text($urlparts[1], false);
}
return mix_text($string);
}
/**
* Utility function to scramble a filename
*
* @param string $string - filename to scramble
*
* @return string
*/
function mix_filename($string)
{
global $mixcache;
if (trim($string) == "") {
return "";
}
if (isset($mixcache[md5($string)])) {
return $mixcache[md5($string)];
}
debug("filename: " . $string);
if (strpos($string, ".") === false) {
return mix_text($string, false);
}
$fileparts = pathinfo($string);
$newfilename = mix_text($fileparts["filename"], false) . "." . $fileparts["extension"];
debug("New filename: " . $newfilename);
return $newfilename;
}
/**
* Utility function to scramble an email address
*
* @param string $string - email to scramble
*
* @return string
*/
function mix_email($string)
{
global $mixcache;
if (isset($mixcache[md5($string)])) {
return $mixcache[md5($string)];
}
$emailparts = explode("@", $string);
if (count($emailparts) < 2) {
return mix_text($string);
}
$newemail = implode("@", array_map("mix_text", $emailparts));
// Update cache
$mixcache[md5($string)] = $newemail;
return $newemail;
}
/**
* Utility function to escape and replace any empty strings with NULLS for exported SQL scripts
*
* @param null|string $value Value to check
* @return string
*/
function safe_export(?string $value): string
{
return trim($value ?? '') == "" ? "NULL" : "'" . escape_check($value) . "'";
}
/**
* Get array of tables to export when exporting system config and data
*
* @param int $exportcollection - Optional collection id to include resources and data from
*
* @return array
*/
function get_export_tables($exportcollection = 0)
{
global $plugins;
if ((string)(int)$exportcollection !== (string)$exportcollection) {
$exportcollection = 0;
}
// Create array of tables to export
$exporttables = array();
$exporttables["sysvars"] = array();
$exporttables["preview_size"] = array();
$exporttables["workflow_actions"] = array();
if (in_array("rse_workflow", $plugins)) {
$exporttables["archive_states"] = array();
}
$exporttables["user"] = array();
$exporttables["user"]["scramble"] = array("username" => "mix_text","email" => "mix_email","fullname" => "mix_text","comments" => "mix_text","created" => "mix_date");
$exporttables["user_preferences"] = array();
$exporttables["usergroup"] = array();
$exporttables["usergroup"]["scramble"] = array("name" => "mix_text","welcome_message" => "mix_text","search_filter" => "mix_text","edit_filter" => "mix_text");
$exporttables["dash_tile"] = array();
$exporttables["dash_tile"]["scramble"] = array("title" => "mix_text","txt" => "mix_text","url" => "mix_url");
$exporttables["user_dash_tile"] = array();
$exporttables["usergroup_dash_tile"] = array();
$exporttables["resource_type"] = array();
$exporttables["resource_type_field"] = array();
$exporttables["resource_type_field"]["scramble"] = array("title" => "mix_text","name" => "mix_text");
$exporttables["node"] = array();
$exporttables["node"]["scramble"] = array("name" => "mix_text");
$exporttables["filter"] = array();
$exporttables["filter"]["scramble"] = array("name" => "mix_text");
$exporttables["filter_rule"] = array();
$exporttables["filter_rule_node"] = array();
// Optional tables
if ($exportcollection != 0) {
// Collections
$exporttables["collection"] = array();
$exporttables["collection"]["exportcondition"] = "WHERE ref = '$exportcollection'";
$exporttables["collection"]["scramble"] = array("name" => "mix_text","description" => "mix_text","keywords" => "mix_text","created" => "mix_date");
$exporttables["user_collection"] = array();
$exporttables["usergroup_collection"] = array();
$exporttables["collection_resource"] = array();
// Resources and resource metadata
$exporttables["resource"] = array();
$exporttables["resource"]["scramble"] = array("field8" => "mix_text","creation_date" => "mix_date");
$exporttables["resource"]["exportcondition"] = " WHERE ref IN (SELECT resource FROM collection_resource WHERE collection='$exportcollection')";
$exporttables["resource_node"] = array();
$exporttables["resource_custom_access"] = array();
$exporttables["resource_dimensions"] = array();
$exporttables["resource_related"] = array();
$exporttables["resource_alt_files"] = array();
$exporttables["resource_alt_files"]["scramble"] = array("name" => "mix_text","description" => "mix_text","file_name" => "mix_filename");
$exporttables["annotation"] = array();
$exporttables["annotation_node"] = array();
}
$extra_tables = hook("export_add_tables");
if (is_array($extra_tables)) {
$exporttables = array_merge($exporttables, $extra_tables);
}
return $exporttables;
}
function edit_filter_to_restype_permission($filtertext, $usergroup, $existingperms, $updatecurrent = false)
{
global $userpermissions;
$addpermissions = array();
// Replace any resource type edit filter sections with new XE/XE-?/XE? permissions
$filterrules = explode(";", $filtertext);
$cleanedrules = array();
foreach ($filterrules as $filterrule) {
$filterparts = explode("=", $filterrule);
$checkattr = trim(strtolower($filterparts[0]));
if (substr($checkattr, 0, 13) == "resource_type") {
$filternot = false;
if (substr($checkattr, -1) == "!") {
$filternot = true;
} else {
// Only allowing certain resource types. Add permission to block all resource types
// and then add a permission for each permitted type
$addpermissions[] = "XE";
}
$checkrestypes = explode("|", $filterparts[1]);
foreach ($checkrestypes as $checkrestype) {
// Add either XE-? or XE? permission, depending on whether group is only allowed to edit the specified types or everything except these types
$addpermissions[] = "XE" . ($filternot ? "" : "-") . (int)trim($checkrestype);
}
} else {
$cleanedrules[] = trim($filterrule);
}
}
$currentgroup = get_usergroup($usergroup);
if (in_array("permissions", $currentgroup["inherit"])) {
$existingperms = explode(",", $currentgroup["permissions"]);
}
$newperms = array_diff($addpermissions, $existingperms);
if (count($newperms) > 0) {
save_usergroup($usergroup, array('permissions' => $currentgroup["permissions"] . ',' . implode(',', $newperms)));
clear_query_cache('usergroup');
}
if ($updatecurrent) {
$userpermissions = array_merge($userpermissions, $newperms);
}
// Reconstruct filter text without this to create new filter
return implode(";", $cleanedrules);
}

1010
include/mime_types.php Normal file

File diff suppressed because it is too large Load Diff

2997
include/node_functions.php Normal file

File diff suppressed because it is too large Load Diff

392
include/pdf_functions.php Normal file
View File

@@ -0,0 +1,392 @@
<?php
/**
* Returns the path to a pdf template
*
* @param string $resource_type ID of the resource type
* @param string $template_name Known template name already found in the array
*
* @return string
*/
function get_pdf_template_path($resource_type, $template_name = '')
{
global $storagedir, $pdf_resource_type_templates;
$template = '';
if (!array_key_exists($resource_type, $pdf_resource_type_templates)) {
debug('There are no PDF templates set for resource type "' . $resource_type . '"');
return false;
}
$templates = $pdf_resource_type_templates[$resource_type];
if (array_key_exists($resource_type, $pdf_resource_type_templates) && empty($templates)) {
debug('There are no PDF templates set for resource type "' . $resource_type . '"');
return false;
}
// Client code wants a specific template name but there isn't one
if ('' !== $template_name && !in_array($template_name, $templates)) {
debug('PDF template "' . $template_name . '" could not be found in $pdf_resource_type_templates');
return false;
}
// Client code wants a specific template name
if ('' !== $template_name && in_array($template_name, $templates)) {
$template_array_key = array_search($template_name, $templates);
if (false !== $template_array_key) {
$template = $templates[$template_array_key];
}
}
// Provide a default one if template name is empty
if ('' === $template && '' === $template_name) {
$template = $templates[0];
}
return $storagedir . '/system/pdf_templates/' . $template . '.html';
}
/**
* Takes an HTML template suitable for HTML2PDF library and generates a PDF file if successfull
*
* @param string $html_template_path HTML template path
* @param string $filename The file name of the generated PDF file. If this is an actual path,
* and $save_on_server = true, it will be save on the server
* @param array $bind_placeholders A map of all the values that are meant to replace any
* placeholders found in the HTML template
* @param boolean $save_on_server If true, PDF file will be saved to the filename path
* @param array $pdf_properties Properties of the PDF file (e.g. author, title, font, margins)
*
* @return boolean
*/
function generate_pdf($html_template_path, $filename, array $bind_placeholders = array(), $save_on_server = false, array $pdf_properties = array())
{
global $applicationname, $baseurl, $baseurl_short, $storagedir, $linkedheaderimgsrc, $language, $contact_sheet_date_include_time, $contact_sheet_date_wordy;
$html2pdf_path = __DIR__ . '/../lib/html2pdf/vendor/autoload.php';
if (!file_exists($html2pdf_path)) {
trigger_error('html2pdf class file is missing. Please make sure you have it under lib folder!');
}
require_once $html2pdf_path;
// Do we have a physical HTML template
if (!file_exists($html_template_path)) {
trigger_error('File "' . $html_template_path . '" does not exist!');
}
$html = file_get_contents($html_template_path);
if (false === $html) {
return false;
}
// General placeholders available to HTML templates
$general_params = array(
'applicationname' => $applicationname,
'baseurl' => $baseurl,
'baseurl_short' => $baseurl_short,
'filestore' => $storagedir,
'filename' => (!$save_on_server ? $filename : basename($filename)),
'date' => nicedate(date('Y-m-d H:i:s'), $contact_sheet_date_include_time, $contact_sheet_date_wordy),
);
if ('' != $linkedheaderimgsrc) {
$general_params['linkedheaderimgsrc'] = $linkedheaderimgsrc;
}
$bind_params = array_merge($general_params, $bind_placeholders);
foreach ($bind_params as $param => $param_value) {
// Bind [%param%] placeholders to their values
$html = str_replace('[%' . $param . '%]', escape($param_value), $html);
// replace \r\n with <br />. This is how they do it at the moment at html2pdf.fr
$html = str_replace("\r\n", '<br />', $html);
}
$html = process_if_statements($html, $bind_params);
// Last resort to clean up PDF templates by searching for all remaining placeholders
$html = preg_replace('/\[%.*%\]/', '', $html);
// Setup PDF
$pdf_orientation = 'P';
$pdf_format = 'A4';
$pdf_language = resolve_pdf_language();
$pdf_unicode = true;
$pdf_encoding = 'UTF-8';
$pdf_margins = array(5, 5, 5, 8);
if (array_key_exists('orientation', $pdf_properties) && '' != trim($pdf_properties['orientation'])) {
$pdf_orientation = $pdf_properties['orientation'];
}
if (array_key_exists('format', $pdf_properties) && '' != trim($pdf_properties['format'])) {
$pdf_format = $pdf_properties['format'];
}
if (array_key_exists('language', $pdf_properties) && '' != trim($pdf_properties['language'])) {
$pdf_language = $pdf_properties['language'];
}
if (array_key_exists('margins', $pdf_properties) && is_array($pdf_properties['margins']) && 0 !== count($pdf_properties['margins'])) {
$pdf_margins = $pdf_properties['margins'];
}
$html2pdf = new Spipu\Html2Pdf\Html2Pdf($pdf_orientation, $pdf_format, $pdf_language, $pdf_unicode, $pdf_encoding, $pdf_margins);
// Set PDF title
if (array_key_exists('title', $pdf_properties) && '' != trim($pdf_properties['title'])) {
$html2pdf->pdf->SetTitle($pdf_properties['title']);
}
// Set PDF author
if (array_key_exists('author', $pdf_properties) && '' != trim($pdf_properties['author'])) {
$html2pdf->pdf->SetAuthor($pdf_properties['author']);
}
// Set PDF subject
if (array_key_exists('subject', $pdf_properties) && '' != trim($pdf_properties['subject'])) {
$html2pdf->pdf->SetSubject($pdf_properties['subject']);
}
// Set PDF font family
if (array_key_exists('font', $pdf_properties) && '' != trim($pdf_properties['font'])) {
$html2pdf->setDefaultFont($pdf_properties['font']);
}
$html2pdf->WriteHTML($html);
if ($save_on_server) {
$html2pdf->Output($filename, 'F');
} else {
$html2pdf->Output($filename);
}
return true;
}
/**
* Returns the path to any template in the system.
*
* Returns the path to any templates in the system as long as they are saved in the correct place:
* - /templates (default - base templates)
* - /filestore/system/templates (custom templates)
* Templates will be structured in folders based on features (e.g templates/contact_sheet/
* will be used for any templates used for contact sheets)
*
* @param string $template_name Template names should contain the extension as well (e.g. template_1.php / template_1.html)
* @param string $template_namespace The name by which multiple templates are grouped together
*
* @return string
*/
function get_template_path($template_name, $template_namespace)
{
global $storagedir;
$template_path = '';
$remove_directory_listings = array('.', '..');
// Directories that may contain these files
$default_tpl_dir = __DIR__ . "/../templates/{$template_namespace}";
$filestore_tpl_dir = "{$storagedir}/system/templates/{$template_namespace}";
if (!file_exists($default_tpl_dir)) {
trigger_error("ResourceSpace could not find templates folder '{$template_namespace}'");
}
// Get default path
$default_tpl_files = array_diff(scandir($default_tpl_dir), $remove_directory_listings);
if (in_array($template_name, $default_tpl_files)) {
$template_path = "$default_tpl_dir/$template_name";
}
// Get custom template (if any)
if (file_exists($filestore_tpl_dir)) {
$filestore_tpl_files = array_diff(scandir($filestore_tpl_dir), $remove_directory_listings);
if (in_array($template_name, $filestore_tpl_files)) {
$template_path = "$filestore_tpl_dir/$template_name";
}
}
if ('' == $template_path) {
trigger_error("ResourceSpace could not find template '{$template_name}'");
}
return $template_path;
}
/**
* Function used to process a template
*
* Process template and bind any placeholders. The template should contain (if needed) PHP statements which will
* will be processed through this function.
*
* @param string $template_path The full path to the location of the template (as returned by get_template_path())
* @param array $bind_placeholders A map of all the values that are meant to replace any placeholders found in the template
*
* @return string
*/
function process_template($template_path, array $bind_placeholders = array())
{
global $applicationname, $baseurl, $baseurl_short, $storagedir, $lang, $linkedheaderimgsrc;
global $contact_sheet_date_include_time, $contact_sheet_date_wordy, $pdf_properties;
// General placeholders available to templates
$applicationname = $bind_placeholders['applicationname'] ?? $applicationname;
$baseurl = $bind_placeholders['baseurl'] ?? $baseurl;
$baseurl_short = $bind_placeholders['baseurl_short'] ?? $baseurl_short;
$filestore = $bind_placeholders['filestore'] ?? $storagedir;
$lang = $bind_placeholders['lang'] ?? $lang;
$date = $bind_placeholders['date'] ?? nicedate(date('Y-m-d H:i:s'), $contact_sheet_date_include_time, $contact_sheet_date_wordy);
// Sometimes, HTML2PDF complains about headers being already sent
ob_end_clean();
// At this point we shoud have all the placeholders we need to render the template nicely
ob_start();
include $template_path;
return ob_get_clean();
}
/**
* Process a string (mainly HTML) which contains if statement placeholders and return the processed string
*
* Handles [%if var is set%] [%endif%] type of placeholders
*
* @param string $original_string Full string containing placeholders
* @param array $bind_params A map of all the values that are meant to replace any
* placeholders found in the HTML template
*
* @return string
*/
function process_if_statements($original_string, array $bind_params)
{
$remove_placeholder_elements = array('[%if ', ' is set%]');
preg_match_all('/\[%if (.*?) is set%\]/', $original_string, $if_isset_matches);
foreach ($if_isset_matches[0] as $if_isset_match) {
$var_name = str_replace($remove_placeholder_elements, '', $if_isset_match);
$if_isset_match_position = strpos($original_string, $if_isset_match);
$endif_position = strpos($original_string, '[%endif%]', $if_isset_match_position);
$substr_lenght = $endif_position - $if_isset_match_position;
$substr_html_one = substr($original_string, 0, $if_isset_match_position);
$substr_html_two = substr($original_string, $if_isset_match_position, $substr_lenght + 9);
$substr_html_three = substr($original_string, $endif_position + 9);
/*
Make sure we have the correct subset (html2) for our if statement. This means we don't
stop at the first endif we found unless there are no if statements inside the subset.
If there are, move passed it and continue looking for other endifs until we reach the
same number of endifs as we had ifs
*/
$endif_count = 0;
do {
$if_count = preg_match_all('/\[%if (.*?) is set%\]/', $substr_html_two, $if_isset_matches_subset_two) - 1;
if (0 < $if_count) {
// Move the end of the second subset of HTML to the next endif and then increase endif counter
$next_endif_position = strpos($substr_html_three, '[%endif%]') + 9;
$substr_html_two .= substr($substr_html_three, 0, $next_endif_position);
$substr_html_three = substr($substr_html_three, $next_endif_position);
$endif_count++;
}
} while ($if_count !== $endif_count);
// Variable is not set, remove that section from the template
if (!array_key_exists($var_name, $bind_params)) {
$original_string = $substr_html_one . $substr_html_three;
continue;
}
// The section stays in, clean it up of the top level if - endif placeholders
$substr_html_two = substr($substr_html_two, strlen($if_isset_match));
$substr_html_two = substr($substr_html_two, 0, -9);
$original_string = $substr_html_one . $substr_html_two . $substr_html_three;
}
return $original_string;
}
/**
* Function to convert the user's language into an HTML2PDF supported language.
*
* Scans the HTML2PDF locale folder to create a list of supported languages to compare against
* the set user language. Also resolves dialects when possible. Fallback set to 'en'.
*
* @return string
*/
function resolve_pdf_language()
{
global $language;
$asdefaultlanguage = 'en';
$supported_lang_files = scandir(__DIR__ . '/../lib/html2pdf/src/locale');
$supported_langs = array();
foreach ($supported_lang_files as $file) {
$sl = pathinfo($file, PATHINFO_FILENAME);
if (!in_array($sl, array("",".",".."))) {
$supported_langs[] = $sl;
}
}
if (in_array($language, $supported_langs)) {
return $language;
} else {
switch ($language) {
case "es-AR":
return "es";
break;
case "pt-BR":
return "pt";
break;
default:
// this includes en-US
return $asdefaultlanguage;
}
}
}
/**
* Returns an array of available PDF template names
* *
* @param string $template_namespace The name by which multiple templates are grouped together e.g. contact_sheet
*
* @return array()
*/
function get_pdf_templates($template_namespace)
{
global $storagedir;
$templates = array();
$remove_directory_listings = array('.', '..');
// Directories that may contain these files
$default_tpl_dir = __DIR__ . "/../templates/{$template_namespace}";
$filestore_tpl_dir = "{$storagedir}/system/templates/{$template_namespace}";
if (!file_exists($default_tpl_dir)) {
trigger_error("ResourceSpace could not find templates folder '{$template_namespace}'");
}
// Get default path
$templates = array_diff(scandir($default_tpl_dir), $remove_directory_listings);
// Get custom template (if any)
if (file_exists($filestore_tpl_dir)) {
$filestore_templates = array_diff(scandir($filestore_tpl_dir), $remove_directory_listings);
$templates = array_merge($templates, $filestore_templates);
}
$templates = array_map(function ($e) {
return pathinfo($e, PATHINFO_FILENAME);
}, $templates);
return $templates;
}

1803
include/plugin_functions.php Normal file

File diff suppressed because it is too large Load Diff

1086
include/preview_preprocessing.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
<?php
global $baseurl,$baseurl_short,$enable_related_resources, $edit_access, $title_field;
$view_title_field = $title_field;
if ($enable_related_resources) {
$use_watermark = check_use_watermark();
$relatedresources = do_search('!related' . $ref);
$related_restypes = array();
for ($n = 0; $n < count($relatedresources); $n++) {
$related_restypes[] = $relatedresources[$n]['resource_type'];
}
$related_restypes = array_unique($related_restypes);
$relatedtypes_shown = array();
$related_resources_shown = 0;
global $related_type_show_with_data, $related_type_upload_link, $userref;
if (isset($related_type_show_with_data)) {
foreach ($related_type_show_with_data as $rtype) {
# Is this a resource type that needs to be displayed? Don't show resources of the same type as this is not the standard configuration
if ($resource['resource_type'] == $rtype || !in_array($rtype, $related_type_show_with_data) || (!in_array($rtype, $related_restypes) && !$related_type_upload_link)) {
continue;
}
// Show only related resource types that match this tab:
$resource_type_tab_ref = ps_value('SELECT tab AS value FROM resource_type WHERE ref = ?', ['i', $rtype], '', 'schema');
if ($tab_ref !== $resource_type_tab_ref) {
continue;
}
$restypename = ps_value("select name as value from resource_type where ref = ?", array("i",$rtype), "", "schema");
$restypename = lang_or_i18n_get_translated($restypename, "resourcetype-", "-2");
if (isset($related_type_thumbnail_view) && in_array($rtype, $related_type_thumbnail_view)) {
foreach ($relatedresources as $relatedresource) {
if ($relatedresource['resource_type'] == $rtype) {
?>
<div class="ResourcePanelShellSmall" id="RelatedResource_<?php echo $relatedresource["ref"]; ?>">
<a class="ImageWrapperSmall" href="<?php echo $baseurl_short ?>pages/view.php?ref=<?php echo $relatedresource["ref"]; ?>" title="<?php echo escape(i18n_get_translated(($relatedresource["field" . $view_title_field]))) ?>" onClick="return ModalLoad(this,true);">
<?php if ((int) $relatedresource["has_image"] !== RESOURCE_PREVIEWS_NONE) {
$thm_url = get_resource_path($relatedresource["ref"], false, "col", false, $relatedresource["preview_extension"], -1, 1, $use_watermark, $relatedresource["file_modified"]);
render_resource_image($relatedresource, $thm_url, "collection");
} else {
echo get_nopreview_html((string) $relatedresource["file_extension"]);
}
?>
</a>
<?php
if ($edit_access) {
?>
<div class="ResourcePanelInfo" >
<a href="#"
onClick="if(confirm('<?php echo escape($lang["related_resource_confirm_delete"])?>'))
{
relateresources(<?php echo (int) $ref . "," . (int) $relatedresource["ref"] ;?>,'remove',
<?php echo escape(generate_csrf_js_object('update_related_resource')); ?>);
jQuery('#RelatedResource_<?php echo (int) $relatedresource["ref"]; ?>').remove();
}
return false;" >
<?php echo LINK_CARET . escape($lang["action-remove"]) ?></a></div>
<?php
}?>
</div>
<?php
}
}
if ($related_type_upload_link && $edit_access) {
if ($upload_then_edit) {
$uploadurl = generateURL($baseurl . "/pages/upload_batch.php", ["redirecturl" => generateURL($baseurl . "/pages/view.php", $urlparams)], ["relateto" => $ref]);
} else {
$uploadurl = generateURL($baseurl . "/pages/edit.php", ["redirecturl" => generateURL($baseurl . "/pages/view.php", $urlparams) . "#RelatedResources","ref" => -$userref], ["relateto" => $ref]);
}
echo "<div class=\"clearerleft\" ></div>";
echo "<a class=\"ResourcePanelSmallIcons\" href=\"" . $uploadurl . "\" onclick=\"return CentralSpaceLoad(this, true);\">" . LINK_CARET . escape($lang["upload"]) . "</a>";
}
} else {
// Standard table view
?>
<div class="clearerleft"></div>
<div class="item" id="RelatedResourceData">
<?php
if (in_array($rtype, $related_restypes) || ($related_type_upload_link && $edit_access)) {
?>
<div class="Listview ListviewTight" >
<table class="ListviewStyle">
<tbody>
<tr class="ListviewTitleStyle">
<th><h3><?php echo escape($restypename); ?></h3></th>
<th><div class="ListTools"></div></th>
</tr>
<?php
foreach ($relatedresources as $relatedresource) {
$related_resource_ref = (int) $relatedresource['ref'];
if ($relatedresource['resource_type'] == $rtype) {
$relatedtitle = (string) $relatedresource["field{$view_title_field}"];
echo "<tr id=\"relatedresource{$related_resource_ref}\" class=\"RelatedResourceRow\">";
echo "<td class=\"link\"><a href=\"{$baseurl_short}pages/view.php?ref={$related_resource_ref}\" onClick=\"return ModalLoad(this,true);\" >" . escape($relatedtitle) . "</a></td>";
echo "<td>";
if ($edit_access) {
?>
<div class="ListTools" >
<a href="#"
onClick="if(confirm('<?php echo escape($lang["related_resource_confirm_delete"])?>'))
{
relateresources(<?php echo (int) $ref . "," . (int) $relatedresource["ref"] ;?>,'remove',
<?php echo escape(generate_csrf_js_object('update_related_resource')); ?>);
jQuery('#RelatedResource_<?php echo (int) $relatedresource["ref"]; ?>').remove();
}
return false;" >
<?php echo LINK_CARET . escape($lang["action-remove"]) ?></a></div>
<?php
}
echo "</td>";
echo "</tr>";
$related_resources_shown++;
}
}
if ($related_type_upload_link && $edit_access) {
if ($upload_then_edit) {
$uploadurl = generateURL($baseurl . "/pages/upload_batch.php", ["redirecturl" => generateURL($baseurl . "/pages/view.php", $urlparams)], ["relateto" => $ref]);
} else {
$uploadurl = generateURL($baseurl . "/pages/edit.php", ["redirecturl" => generateURL($baseurl . "/pages/view.php", $urlparams) . "#RelatedResources","ref" => -$userref], ["relateto" => $ref]);
}
echo "<tr><td></td><td><div class=\"ListTools\"><a href=\"" . $uploadurl . "\">" . LINK_CARET . escape($lang["upload"]) . "</a></div></td>";
}
?>
</tbody>
</table>
</div>
<?php
} ?>
</div><!-- end of RelatedResourceData -->
<?php
}
// We have displayed these, don't show them again later
$relatedtypes_shown[] = $rtype;
}
}
}

7614
include/render_functions.php Normal file

File diff suppressed because it is too large Load Diff

823
include/reporting_functions.php Executable file
View File

@@ -0,0 +1,823 @@
<?php
# Reporting functions
/**
* Retrieves the name of a report
*
* @param array $report The report array containing at least a 'name' key.
* @return string The translated report name.
*/
function get_report_name($report)
{
# Translates or customizes the report name.
$customName = hook('customreportname', '', array($report));
if ($customName) {
return $customName;
}
return lang_or_i18n_get_translated($report["name"], "report-");
}
/**
* Retrieves an array of reports from the database.
*
* This function queries the database for all reports and processes them by:
* 1. Translating the report names using the `get_report_name` function.
* 2. Checking if the reports contain date fields using `report_has_date`.
* 3. Verifying if the reports have associated thumbnails using `report_has_thumbnail`.
*
* The reports are always listed in the same order, regardless of the language used.
*
* @return array An array of processed reports, each containing 'ref', 'name',
* 'contains_date', and 'has_thumbnail' keys.
*/
function get_reports()
{
# Executes query.
$r = ps_query("SELECT ref, `name`, `query`, support_non_correlated_sql FROM report ORDER BY name");
# Translates report names in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
if (!hook('ignorereport', '', array($r[$n]))) {
$r[$n]["name"] = get_report_name($r[$n]);
$r[$n]["contains_date"] = report_has_date((string) $r[$n]["query"]);
$r[$n]['has_thumbnail'] = report_has_thumbnail((string) $r[$n]["query"]);
$return[] = $r[$n];
}
}
return $return;
}
/**
* do_report - Runs the specified report. This is used in a number of ways:-
* 1) Outputs an HTML table to screen ($download = false)
* 2) Produces a CSV
* - for direct download from team_report.php
* - captured and saved as a CSV file if called by send_periodic_report_emails() and over 100 rows are returned
*
*
* @param int $ref Report ID
* @param mixed $from_y Start year (used for reprts with date placholders)
* @param mixed $from_m Start month
* @param mixed $from_d Start day
* @param mixed $to_y End year
* @param mixed $to_m To month
* @param mixed $to_d To day
* @param mixed $download Output as CSV attachment (default)/output directly to client
* @param mixed $add_border Optional table border (not for download)
* @param mixed $foremail Sending as email?
* @param array $search_params Search parameters - {@see get_search_params()} - will run the report on the search
* results and replace the '[non_correlated_sql]' placeholder with the search query.
*
* @return void | string | array Outputs CSV file, returns HTML table or returns an array with path to the CSV file, rows and filename
*/
function do_report($ref, $from_y, $from_m, $from_d, $to_y, $to_m, $to_d, $download = true, $add_border = false, $foremail = false, array $search_params = array())
{
# Run report with id $ref for the date range specified. Returns a result array.
global $lang, $baseurl, $report_rows_attachment_limit;
$report = ps_query("SELECT ref, `name`, `query`, support_non_correlated_sql FROM report WHERE ref = ?", array("i",$ref));
if (count($report) < 1) {
return $lang['error_generic'];
}
$has_date_range = report_has_date($report[0]["query"]);
$report = $report[0];
$report['name'] = get_report_name($report);
if ($download || $foremail) {
if ($has_date_range) {
$filename = str_replace(array(" ","(",")","-","/",","), "_", $report["name"]) . "_" . $from_y . "_" . $from_m . "_" . $from_d . "_" . $lang["to"] . "_" . $to_y . "_" . $to_m . "_" . $to_d . ".csv";
} else {
$filename = str_replace(array(" ","(",")","-","/",","), "_", $report["name"]) . ".csv";
}
}
if ($results = hook("customreport", "", array($ref,$from_y,$from_m,$from_d,$to_y,$to_m,$to_d,$download,$add_border, $report))) {
// Hook has created the $results array
} else {
// Generate report results normally
$sql_parameters = array();
$report_placeholders = [
'[from-y]' => $from_y,
'[from-m]' => $from_m,
'[from-d]' => $from_d,
'[to-y]' => $to_y,
'[to-m]' => $to_m,
'[to-d]' => $to_d,
];
if ((bool)$report['support_non_correlated_sql'] === true && !empty($search_params)) {
// If report supports being run on search results, embed the non correlated sql necessary to feed the report
$returned_search = do_search(
$search_params['search'],
$search_params['restypes'],
$search_params['order_by'],
$search_params['archive'],
-1, # fetchrows
$search_params['sort'],
false, # access_override
DEPRECATED_STARSEARCH,
false, # ignore_filters
false, # return_disk_usage
$search_params['recentdaylimit'],
false, # go
false, # stats_logging
true, # return_refs_only
false, # editable_only
true # returnsql
);
if (!is_a($returned_search, "PreparedStatementQuery") || !is_string($returned_search->sql)) {
debug("Invalid SQL returned by do_search(). Report cannot be generated");
return "";
}
$sql_parameters = array_merge($sql_parameters, $returned_search->parameters);
$report_placeholders[REPORT_PLACEHOLDER_NON_CORRELATED_SQL] = "(SELECT ncsql.ref FROM ({$returned_search->sql}) AS ncsql)";
}
$sql = report_process_query_placeholders($report['query'], $report_placeholders);
db_set_connection_mode("read_only");
$results = ps_query($sql, $sql_parameters);
db_clear_connection_mode();
}
$resultcount = count($results);
if ($resultcount == 0) {
// No point downloading as the resultant file will be empty
$download = false;
}
foreach ($results as &$result) {
foreach ($result as $key => &$value) {
# Merge translation strings if multiple in a single column
if (substr($key, 0, 4) == "i18n") {
$delimiter = substr($key, 4, strpos($key, "_") - 4);
$value = implode("", i18n_merge_translations(explode($delimiter, (string)$value)));
}
}
unset($value);
}
unset($result);
if ($download) {
header("Content-type: application/octet-stream");
header("Content-disposition: attachment; filename=\"" . $filename . "\"");
}
if ($download || ($foremail && $resultcount > $report_rows_attachment_limit)) {
if ($foremail) {
ob_clean();
ob_start();
}
for ($n = 0; $n < $resultcount; $n++) {
$result = $results[$n];
if ($n == 0) {
$f = 0;
foreach ($result as $key => $value) {
$f++;
if ($f > 1) {
echo ",";
}
if (substr($key, 0, 4) == "i18n") {
$key = substr($key, strpos($key, "_") + 1);
}
if ($key != "thumbnail") {
echo "\"" . lang_or_i18n_get_translated($key, "columnheader-") . "\"";
}
}
echo "\n";
}
$f = 0;
foreach ($result as $key => $value) {
$f++;
if ($f > 1) {
echo ",";
}
$custom = hook('customreportfield', '', array($result, $key, $value, $download));
if ($custom !== false) {
echo $custom;
} elseif ($key != "thumbnail") {
$value = lang_or_i18n_get_translated($value, "usergroup-");
$value = str_replace('"', '""', $value); # escape double quotes
if (substr($value, 0, 1) == ",") {
$value = substr($value, 1);
} # Remove comma prefix on dropdown / checkbox values
echo "\"" . $value . "\"";
}
}
echo "\n";
}
if ($foremail) {
$output = ob_get_contents();
ob_end_clean();
$unique_id = uniqid();
$reportfile = get_temp_dir(false, "Reports") . "/Report_" . $unique_id . ".csv";
file_put_contents($reportfile, $output);
return array("file" => $reportfile,"filename" => $filename, "rows" => $resultcount);
}
} else {
# Not downloading - output a table
// If report results are too big, display the first rows and notify user they should download it instead
$output = '';
if ($resultcount > $report_rows_attachment_limit) {
$results = array_slice($results, 0, $report_rows_attachment_limit);
// Catch the error now and place it above the table in the output
render_top_page_error_style($lang['team_report__err_report_too_long']);
$output = ob_get_contents();
ob_clean();
ob_start();
}
// Pre-render process: Process nodes search syntax (e.g @@228 or @@!223) and add a new column that contains the node list and their names
if (isset($results[0]['search_string'])) {
$results = process_node_search_syntax_to_names($results, 'search_string');
}
$border = "";
if ($add_border) {
$border = "border=\"1\"";
}
$output .= "<br /><h2>" . $report['name'] . "</h2><style>.InfoTable td {padding:5px;}</style><table $border class=\"InfoTable\">";
for ($n = 0; $n < count($results); $n++) {
$result = $results[$n];
if ($n == 0) {
$f = 0;
$output .= "<tr>\r\n";
foreach ($result as $key => $value) {
$f++;
if ($key == "thumbnail") {
$output .= "<td><strong>Link</strong></td>\r\n";
} else {
if (substr($key, 0, 4) == "i18n") {
$key = substr($key, strpos($key, "_") + 1);
}
$output .= "<td><strong>" . lang_or_i18n_get_translated($key, "columnheader-") . "</strong></td>\r\n";
}
}
$output .= "</tr>\r\n";
}
$f = 0;
$output .= "<tr>\r\n";
foreach ($result as $key => $value) {
$f++;
if ($key == "thumbnail") {
$thm_path = get_resource_path($value, true, "thm", false, "", -1, 1, false);
if (!file_exists($thm_path)) {
$thm_path = dirname(__DIR__) . "/gfx/no_preview/default_thm.png";
} else {
$thm_path = get_resource_path($value, true, "col", false, "", -1, 1, false);
}
$output .= sprintf(
"<td><a href=\"%s/?r=%s\" target=\"_blank\"><img src=\"data:image/%s;base64,%s\"></a></td>\r\n",
$baseurl,
$value,
pathinfo($thm_path, PATHINFO_EXTENSION),
base64_encode(file_get_contents($thm_path))
);
} else {
$custom = hook('customreportfield', '', array($result, $key, $value, $download));
if ($custom !== false) {
$output .= $custom;
} else {
$output .= "<td>" . strip_tags_and_attributes(lang_or_i18n_get_translated($value, "usergroup-"), array("a"), ['href', 'target', 'rel', 'title']) . "</td>\r\n";
}
}
}
$output .= "</tr>\r\n";
}
$output .= "</table>\r\n";
if (count($results) == 0) {
$output .= $lang["reportempty"];
}
return $output;
}
exit();
}
/**
* Creates a new automatic periodic e-mail report
*
*/
function create_periodic_email($user, $report, $period, $email_days, array $user_groups, array $search_params)
{
if ($email_days < 1) {
$email_days = 1; # Minimum email frequency is daily.
}
# Delete any matching rows for this report/period.
$query = "DELETE FROM report_periodic_emails
WHERE user = ?
AND report = ?
AND period = ?";
$parameters = array("i",$user, "i",$report, "i",$period);
ps_query($query, $parameters);
# Insert a new row.
$query = "INSERT INTO report_periodic_emails
(user, report, period, email_days, search_params)
VALUES (?,?,?,?,?)";
$parameters = array("i",$user, "i",$report, "i",$period, "i",$email_days,
"s",json_encode($search_params, JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK));
ps_query($query, $parameters);
$ref = sql_insert_id();
# Send to all users?
if (
checkperm('m')
&& !empty($user_groups)
) {
$ugstring = implode(",", $user_groups);
ps_query("UPDATE report_periodic_emails SET user_groups = ? WHERE ref = ?", array("s",$ugstring, "i",$ref));
}
# Return
return true;
}
/**
* Sends periodic report emails to users based on configured schedules.
*
* This function checks for any scheduled reports that need to be sent, either because they are
* pending or overdue. It gathers the necessary user email addresses, generates the reports,
* and sends them as emails with attachments if applicable.
*
* @param bool $echo_out Determines whether to output progress messages during processing.
* @param bool $toemail Determines whether to send the reports via email.
*
* @return void
*/
function send_periodic_report_emails($echo_out = true, $toemail = true)
{
# For all configured periodic reports, send a mail if necessary.
global $lang,$baseurl, $report_rows_zip_limit, $email_notify_usergroups, $userref;
if (is_process_lock("periodic_report_emails")) {
echo " - periodic_report_emails process lock is in place. Skipping.\n";
return;
}
set_process_lock("periodic_report_emails");
// Keep record of temporary CSV/ZIP files to delete after emails have been sent
$deletefiles = array();
$users = [];
# Query to return all 'pending' report e-mails, i.e. where we haven't sent one before OR one is now overdue.
$query = "
SELECT pe.ref,
pe.user,
pe.send_all_users,
pe.user_groups,
pe.report,
pe.period,
pe.email_days,
pe.last_sent,
pe.search_params,
u.email,
r.name
FROM report_periodic_emails pe
JOIN user u ON pe.user = u.ref
JOIN report r ON pe.report = r.ref
WHERE pe.last_sent IS NULL
OR (date_add(date(pe.last_sent), INTERVAL pe.email_days DAY) <= date(now()) AND pe.email_days > 0);
";
$reports = ps_query($query);
foreach ($reports as $report) {
$start = time() - (60 * 60 * 24 * $report["period"]);
$from_y = date("Y", $start);
$from_m = date("m", $start);
$from_d = date("d", $start);
$to_y = date("Y");
$to_m = date("m");
$to_d = date("d");
// Send e-mail reports to users belonging to the specific user groups
if (empty($report['user_groups'])) {
if ($report['send_all_users']) {
// Send to all users is deprecated. Send to $email_notify_usergroups or Super Admin if not set
if (!empty($email_notify_usergroups)) {
foreach ($email_notify_usergroups as $usergroup) {
if (get_usergroup($usergroup) !== false) {
$addusers = get_users($usergroup, "", "u.username", false, -1, 1);
$users = array_merge($users, $addusers);
}
}
} else {
$users = get_notification_users("SYSTEM_ADMIN");
}
}
} else {
$users = get_users($report['user_groups'], "", "u.username", false, -1, 1);
}
// Always add original report creator
$creator = get_user($report['user']);
$users[] = $creator;
$sentousers = [];
if (isset($userref)) {
// Store current user before emulating each to get report
$saveduserref = $userref;
}
// Get unsubscribed users
$unsubscribed = ps_array(
'SELECT user_id as `value`
FROM report_periodic_emails_unsubscribe
WHERE periodic_email_id = ?',
["i",$report['ref']]
);
$reportcache = null;
foreach ($users as $user) {
if (in_array($user["ref"], $unsubscribed) || in_array($user["ref"], $sentousers)) {
// User has unsubscribed from this report or already been sent it
continue;
}
// Check valid email
$email = $user['email'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
continue;
}
// Construct and run the report
// Emulate the receiving user so language text strings are translated and search results take into account any permissions and filters
emulate_user($user["ref"]);
$userref = $user["ref"];
// Translates the report name.
$report["name"] = lang_or_i18n_get_translated($report["name"], "report-");
$search_params = (trim($report['search_params'] ?? "") !== '' ? json_decode($report['search_params'], true) : []);
$static_report = true; // If no dynamic search results are included then the same report results can be used for all recipients
if (!empty($search_params)) {
$static_report = false; // Report may vary so cannot be cached
}
# Generate report (table or CSV)
if ($static_report && isset($reportcache)) {
$output = $reportcache["output"];
$reportfiles = $reportcache["reportfiles"];
} else {
$output = do_report($report["report"], $from_y, $from_m, $from_d, $to_y, $to_m, $to_d, false, true, $toemail, $search_params);
if (empty($output)) {
// No data, maybe no access to search results
$output = "<br/>" . $lang["reportempty"] . "<br/>";
}
$reportfiles = [];
// If report is large, make it an attachment (requires $use_phpmailer=true)
if (is_array($output) && isset($output["file"])) {
$deletefiles[] = $output["file"];
// Include the file as an attachment
if ($output["rows"] > $report_rows_zip_limit) {
// Convert to zip file
$unique_id = uniqid();
$zipfile = get_temp_dir(false, "Reports") . "/Report_" . $unique_id . ".zip";
$zip = new ZipArchive();
$zip->open($zipfile, ZIPARCHIVE::CREATE);
$zip->addFile($output["file"], $output["filename"]);
$zip->close();
$deletefiles[] = $zipfile;
$zipname = str_replace(".csv", ".zip", $output["filename"]);
$reportfiles[$zipname] = $zipfile;
} else {
$reportfiles[$output["filename"]] = $output["file"];
}
}
if ($static_report) {
$reportcache["output"] = $output;
$reportcache["reportfiles"] = $reportfiles;
}
}
// Formulate a title
$title = $report["name"] . ": " . str_replace("?", $report["period"], $lang["lastndays"]);
if (!empty($reportfiles)) {
$output = str_replace("[report_title]", $title, $lang["report_periodic_email_report_attached"]);
}
$unsubscribe_url = generateURL($baseurl, ["ur" => $report["ref"],"u" => $user["ref"]]);
$unsubscribe_link = sprintf(
"<br />%s<br /><a href=\"%s\" target=\"_blank\">%s</a>",
$lang["unsubscribereport"],
$unsubscribe_url,
$unsubscribe_url
);
if ($echo_out) {
echo escape($lang["sendingreportto"]) . " " . $email . "<br />" . $output . $unsubscribe_link . "<br />";
}
$delete_link = "";
if ((int)$user['ref'] == (int)$report["user"]) {
// Add a delete link to the report
$delete_link = "<br />" . $lang["report_delete_periodic_email_link"] . "<br /><a href=\"" . $baseurl . "/?dr=" . $report["ref"] . "\" target=\"_blank\">" . $baseurl . "/?dr=" . $report["ref"] . "</a>";
}
send_mail($email, $title, $output . $delete_link . $unsubscribe_link, "", "", "", "", "", "", "", $reportfiles);
$sentousers[] = $user['ref'];
}
if (isset($saveduserref)) {
$userref = $saveduserref;
emulate_user($userref);
}
# Mark as done.
ps_query('UPDATE report_periodic_emails set last_sent = now() where ref = ?', array("i",$report['ref']));
}
$GLOBALS["use_error_exception"] = true;
foreach ($deletefiles as $deletefile) {
try {
unlink($deletefile);
} catch (Exception $e) {
debug("Unable to delete - file not found: " . $deletefile);
}
}
unset($GLOBALS["use_error_exception"]);
clear_process_lock("periodic_report_emails");
}
/**
* Deletes a periodic report for the current user.
*
* This function removes the specified periodic report from the database for the user
* and also clears any associated unsubscribe (opt out) records.
*
* @param int $ref The reference ID of the periodic report to delete.
* @return bool Returns true upon successful deletion.
*/
function delete_periodic_report($ref)
{
global $userref;
ps_query('DELETE FROM report_periodic_emails WHERE user = ? AND ref = ?', array("i",$userref, "i",$ref));
ps_query('DELETE FROM report_periodic_emails_unsubscribe WHERE periodic_email_id = ?', array("i", $ref));
return true;
}
/**
* Unsubscribes a user from a specified periodic report.
*
* This function inserts a record into the unsubscribe table, preventing the specified user
* from receiving future emails related to the given periodic report.
*
* @param int $user_id The ID of the user to unsubscribe.
* @param int $periodic_email_id The ID of the periodic email report to unsubscribe from.
* @return bool Returns true upon successful unsubscription.
*/
function unsubscribe_user_from_periodic_report($user_id, $periodic_email_id)
{
$query = 'INSERT INTO report_periodic_emails_unsubscribe
(user_id, periodic_email_id)
VALUES (?, ?)';
ps_query($query, array("i",$user_id, "i",$periodic_email_id));
return true;
}
/**
* Retrieves the translated version of an activity type.
*
* This function takes an activity type string, checks if a corresponding
* translation exists in the global language array, and returns the
* translated string if available. If no translation is found, it returns
* the original activity type.
*
* @param string $activity_type The activity type in plain text English.
* @return string The translated activity type if available; otherwise, the original activity type.
*/
function get_translated_activity_type($activity_type)
{
# Activity types are stored in plain text english in daily_stat. This function will use language strings to resolve a translated value where one is set.
global $lang;
$key = "stat-" . strtolower(str_replace(" ", "", $activity_type));
if (!isset($lang[$key])) {
return $activity_type;
} else {
return $lang[$key];
}
}
/**
* Checks for the presence of date placeholders in a report's SQL query.
*
* @param string $query The report's SQL query.
*
* @return boolean Returns true if a date placeholder was found else false.
*/
function report_has_date(string $query)
{
$date_placeholders = array('[from-y]','[from-m]','[from-d]','[to-y]','[to-m]','[to-d]');
$date_present = false;
foreach ($date_placeholders as $placeholder) {
$position = strpos($query, $placeholder);
if ($position !== false) {
$date_present = true;
break;
}
}
return $date_present;
}
/**
* Checks for the presence of date placeholders in a report's sql query using the report's id.
*
* @param int $report Report id of the report to retrieve the query data from the report table.
*
* @return boolean Returns true if a date placeholder was found else false.
*/
function report_has_date_by_id(int $report)
{
$query = ps_value("SELECT `query` as value FROM report WHERE ref = ?", array("i",$report), 0);
return report_has_date($query);
}
/**
* Check if report has a "thumbnail" column in its SQL query.
*
* @param ?string $query The reports' SQL query.
*/
function report_has_thumbnail(?string $query): bool
{
return preg_match('/(AS )*\'thumbnail\'/mi', (string) $query);
}
/**
* Get report date range based on user input
*
* @param array $info Information about the period selection. See unit test for example input
*/
function report_process_period(array $info): array
{
$available_periods = array_merge($GLOBALS['reporting_periods_default'], [0, -1]);
$period = isset($info['period']) && in_array($info['period'], $available_periods) ? $info['period'] : $available_periods[0];
// Specific number of days specified.
if ($period == 0) {
$period = (int) $info['period_days'] ?? 0;
if ($period < 1) {
$period = 1;
}
}
// Specific date range specified.
if ($period == -1) {
$from_y = $info['from-y'] ?? '';
$from_m = $info['from-m'] ?? '';
$from_d = $info['from-d'] ?? '';
$to_y = $info['to-y'] ?? '';
$to_m = $info['to-m'] ?? '';
$to_d = $info['to-d'] ?? '';
}
// Work out the FROM and TO range based on the provided period in days.
else {
$start = time() - (60 * 60 * 24 * $period);
$from_y = date('Y', $start);
$from_m = date('m', $start);
$from_d = date('d', $start);
$to_y = date('Y');
$to_m = date('m');
$to_d = date('d');
}
return [
'from_year' => $from_y,
'from_month' => $from_m,
'from_day' => $from_d,
'to_year' => $to_y,
'to_month' => $to_m,
'to_day' => $to_d,
];
}
/**
* Find and replace a reports' query placeholders with their values.
*
* @param string $query Reports' SQL query
* @param array $placeholders Map between a placeholder and its actual value
*/
function report_process_query_placeholders(string $query, array $placeholders): string
{
$default_placeholders = [
'[title_field]' => $GLOBALS['view_title_field'],
];
$all_placeholders = array_merge($default_placeholders, $placeholders);
$sql = $query;
foreach ($all_placeholders as $placeholder => $value) {
$sql = str_replace($placeholder, $value, $sql);
}
return $sql;
}
/**
* Output the Javascript to build a pie chart in the canvas denoted by $id
* $data must be in the following format
* $data = array(
* "slice_a label" => "slice_a value",
* "slice_b label" => "slice_b value",
* );
*
* @param string $id identifier for the canvas to render the chart in
* @param array $data data to be rendered in the chart
* @param string|null $total null will mean that the data is complete and an extra field is not required
* a string can be used to denote the total value to pad the data to
* @return void
*/
function render_pie_graph($id, $data, $total = null)
{
global $home_colour_style_override,$header_link_style_override;
$rt = 0;
$labels = [];
$values = [];
foreach ($data as $row) {
$rt += $row["c"];
$values[ ] = $row["c"];
$labels[] = $row["name"];
}
if (!is_null($total) && $total > $rt) {
# The total doesn't match, some rows were truncated, add an "Other".
$values[] = $total - $rt;
$labels[] = "Other";
}
$labels = array_map(fn($v) => substr(json_encode($v), 1, -1), $labels);
?>
<script type="text/javascript">
// Setup Styling
new Chart(document.getElementById('<?php echo escape($id) ?>'), {
type: 'pie',
data: {
labels: ['<?php echo implode("', '", $labels) ?>'],
datasets: [
{
data: [<?php echo escape(implode(", ", $values)) ?>]
}
]
},
options: chartstyling<?php echo escape($id)?>,
});
</script>
<?php
}
/**
* Output the Javascript to build a bar chart in the canvas denoted by $id
* $data must be in the following format
* $data = array(
* "point_a x value" => "point_a y value",
* "point_b x value" => "point_b y value",
*
* @param string $id identifier for the canvas to render the chart in
* @param array $data data to be rendered in the chart
* @return void
*/
function render_bar_graph(string $id, array $data)
{
$values = "";
foreach ($data as $t => $c) {
$values .= "{x: $t, y: $c },\n";
}
?>
<script type="text/javascript">
new Chart(
document.getElementById('<?php echo escape($id) ?>'),
{
type: 'line',
data: {
datasets: [
{
data: [<?php echo escape($values) ?>]
}
]
},
options: chartstyling<?php echo escape($id)?>,
}
);
</script>
<?php
}

1177
include/request_functions.php Executable file

File diff suppressed because it is too large Load Diff

264
include/research_functions.php Executable file
View File

@@ -0,0 +1,264 @@
<?php
# Research functions
# Functions to accomodate research requests
/**
* Sends a research request by inserting it into the requests table and notifying the relevant users.
*
* This function takes an array of custom fields related to the research request,
* processes the input data, and sends an email notification to the designated research admins.
* It gathers resource types, deadlines, contact information, and custom fields, and stores them
* in the database. It also constructs and sends a notification message with the request details.
*
* @param array $rr_cfields An array of custom fields associated with the research request.
* @return void This function does not return any value but performs database operations and sends notifications.
* @throws Exception If there is an error during JSON encoding of custom fields.
*/
function send_research_request(array $rr_cfields)
{
# Insert a search request into the requests table.
global $baseurl,$username,$userfullname,$useremail, $userref;
# Resolve resource types
$rt = "";
$types = get_resource_types();
for ($n = 0; $n < count($types); $n++) {
if (getval("resource" . $types[$n]["ref"], "") != "") {
if ($rt != "") {
$rt .= ", ";
}
$rt .= $types[$n]["ref"];
}
}
$as_user = getval("as_user", $userref, true); # If userref submitted, use that, else use this user
$rr_name = getval("name", "");
$rr_description = getval("description", "");
$parameters = array("i",$as_user, "s",$rr_name, "s",$rr_description);
$rr_deadline = getval("deadline", "");
if ($rr_deadline == "") {
$rr_deadline = null;
}
$rr_contact = mb_strcut(getval("contact", ""), 0, 100);
$rr_email = mb_strcut(getval("email", ""), 0, 200);
$rr_finaluse = getval("finaluse", "");
$parameters = array_merge($parameters, array("s",$rr_deadline, "s",$rr_contact, "s",$rr_email, "s",$rr_finaluse));
# $rt
$rr_noresources = getval("noresources", "");
if ($rr_noresources == "") {
$rr_noresources = null;
}
$rr_shape = mb_strcut(getval("shape", ""), 0, 50);
$parameters = array_merge($parameters, array("s",$rt, "i",$rr_noresources, "s",$rr_shape));
/**
* @var string JSON representation of custom research request fields after removing the generated HTML properties we
* needed during form processing
* @see gen_custom_fields_html_props()
*/
$rr_cfields_json = json_encode(array_map(function ($v) {
unset($v["html_properties"]);
return $v;
}, $rr_cfields), JSON_UNESCAPED_UNICODE);
if (json_last_error() !== JSON_ERROR_NONE) {
trigger_error(json_last_error_msg());
}
$rr_cfields_json_sql = ($rr_cfields_json == "" ? "" : $rr_cfields_json);
$parameters = array_merge($parameters, array("s",$rr_cfields_json_sql));
ps_query("insert into research_request(created,user,name,description,deadline,contact,email,finaluse,resource_types,noresources,shape, custom_fields_json)
values (now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $parameters);
# Send request
$templatevars['ref'] = sql_insert_id();
$templatevars['teamresearchurl'] = $baseurl . "/pages/team/team_research_edit.php?ref=" . $templatevars['ref'];
$templatevars['username'] = $username;
$templatevars['userfullname'] = $userfullname;
$templatevars['useremail'] = getval("email", $useremail); # Use provided e-mail (for anonymous access) or drop back to user email.
$templatevars['url'] = $baseurl . "/pages/team/team_research_edit.php?ref=" . $templatevars['ref'];
$research_notify_users = get_notification_users("RESEARCH_ADMIN");
$userconfirmmessage = new ResourceSpaceUserNotification();
$userconfirmmessage->set_subject("lang_newresearchrequestwaiting");
$userconfirmmessage->set_text("'$username' ($userfullname - $useremail) ");
$userconfirmmessage->append_text("lang_haspostedresearchrequest");
$userconfirmmessage->append_text(".\n\n");
$userconfirmmessage->user_preference = "user_pref_resource_access_notifications";
$userconfirmmessage->template = "emailnewresearchrequestwaiting";
$userconfirmmessage->templatevars = $templatevars;
$userconfirmmessage->url = $templatevars["teamresearchurl"];
// Hook needs to update the ResourceSpaceUserNotification object
hook("modifyresearchrequestemail", "", array($userconfirmmessage));
send_user_notification($research_notify_users, $userconfirmmessage);
}
/**
* Retrieves research requests from the database, optionally filtering by a search term
* and sorting the results by a specified field.
*
* @param string $find Optional search term to filter research requests by name, description, contact, or reference number.
* @param string $order_by The field to sort the results by. Valid options are 'ref', 'name', 'created', 'status', or 'assigned_to'.
* @param string $sort The sort direction, either 'ASC' or 'DESC'. Defaults to 'ASC'.
*
* @return array An array of research requests that match the search criteria.
*/
function get_research_requests($find = "", $order_by = "name", $sort = "ASC")
{
$searchsql = "";
$use_order_by = "";
$use_sort = validate_sort_value($sort) ? $sort : 'ASC';
$parameters = array();
if ($find != "") {
$searchsql = "WHERE name like ? or description like ? or contact like ? or ref=?";
$parameters = array("s","%{$find}%", "s","%{$find}%", "s","%{$find}%", "i",(int)$find);
}
if (in_array($order_by, array("ref","name","created","status","assigned_to","collection"))) {
$use_order_by = $order_by;
}
return ps_query("select " . columns_in("research_request", "r") . ",(select username from user u where u.ref=r.user) username,
(select username from user u where u.ref=r.assigned_to) assigned_username from research_request r
$searchsql
order by $use_order_by $use_sort", $parameters);
}
/**
* Retrieves a research request by its reference number, returning its details including name, description, deadline, contact information, user assignment, status, and custom fields.
*
* @param int $ref The reference number of the research request to retrieve.
* @return array|false An associative array with the research request details if found, or false if no request exists.
*/
function get_research_request($ref)
{
$rr_sql = "SELECT rr.ref,rr.name,rr.description,rr.deadline,rr.email,rr.contact,rr.finaluse,rr.resource_types,rr.noresources,rr.shape,
rr.created,rr.user,rr.assigned_to,rr.status,rr.collection,rr.custom_fields_json,
(select u.username from user u where u.ref=rr.user) username,
(select u.username from user u where u.ref=rr.assigned_to) assigned_username from research_request rr where rr.ref=?";
$rr_parameters = array("i",$ref);
$return = ps_query($rr_sql, $rr_parameters);
if (count($return) == 0) {
return false;
}
return $return[0];
}
/**
* Saves a research request by updating its status and assigned user, sending notifications to the originator if the status changes, and optionally deleting the request or copying existing collection resources.
*
* @param int $ref The reference number of the research request to be saved.
* @return bool True if the operation was successful, false otherwise.
*/
function save_research_request($ref)
{
# Save
global $baseurl,$email_from,$applicationname,$lang;
$parameters = array("i",$ref);
if (getval("delete", "") != "") {
# Delete this request.
ps_query("delete from research_request where ref=? limit 1", $parameters);
return true;
}
# Check the status, if changed e-mail the originator
$currentrequest = ps_query("select status, assigned_to, collection from research_request where ref=?", $parameters);
$oldstatus = (count($currentrequest) > 0) ? $currentrequest[0]["status"] : 0;
$newstatus = getval("status", 0);
$collection = (count($currentrequest) > 0) ? $currentrequest[0]["collection"] : 0;
$oldassigned_to = (count($currentrequest) > 0) ? $currentrequest[0]["assigned_to"] : 0;
$assigned_to = getval("assigned_to", 0);
$templatevars['url'] = $baseurl . "/?c=" . $collection;
$templatevars['teamresearchurl'] = $baseurl . "/pages/team/team_research_edit.php?ref=" . $ref;
if ($oldstatus != $newstatus) {
$requesting_user = ps_query("SELECT u.email, u.ref FROM user u,research_request r WHERE u.ref=r.user AND r.ref = ?", $parameters);
$requesting_user = $requesting_user[0];
if ($newstatus == 1) {
$assignedmessage = new ResourceSpaceUserNotification();
$assignedmessage->set_subject("lang_researchrequestassigned");
$assignedmessage->set_text("lang_researchrequestassignedmessage");
$assignedmessage->template = "emailresearchrequestassigned";
$assignedmessage->templatevars = $templatevars;
$assignedmessage->url = $templatevars["teamresearchurl"];
send_user_notification([$requesting_user['ref']], $assignedmessage);
# Log this
daily_stat("Assigned research request", 0);
}
if ($newstatus == 2) {
$completemessage = new ResourceSpaceUserNotification();
$completemessage->set_subject("lang_researchrequestcomplete");
$completemessage->set_text("lang_researchrequestcompletemessage");
$completemessage->append_text("\n\n");
$completemessage->append_text("lang_clicklinkviewcollection");
$completemessage->template = "emailresearchrequestcomplete";
$completemessage->templatevars = $templatevars;
$completemessage->url = $templatevars["teamresearchurl"];
send_user_notification([$requesting_user['ref']], $completemessage);
# Log this
daily_stat("Processed research request", 0);
}
}
if ($oldassigned_to != $assigned_to) {
$assignedmessage = new ResourceSpaceUserNotification();
$assignedmessage->set_subject("lang_researchrequestassigned");
$assignedmessage->set_text("lang_researchrequestassignedmessage");
$assignedmessage->template = "emailresearchrequestassigned";
$assignedmessage->templatevars = $templatevars;
$assignedmessage->url = $templatevars["teamresearchurl"];
send_user_notification([$assigned_to], $assignedmessage);
}
$parameters = array("i",$newstatus, "i",$assigned_to, "i",$ref);
ps_query("UPDATE research_request SET status = ?, assigned_to = ? WHERE ref= ?", $parameters);
# Copy existing collection
$rr_copyexisting = getval("copyexisting", "");
$rr_copyexistingref = getval("copyexistingref", "");
if ($rr_copyexisting != "" && is_numeric($collection)) {
$parameters = array("i",$collection, "i",$rr_copyexistingref, "i",$collection);
ps_query("INSERT INTO collection_resource(collection,resource)
SELECT ?, resource FROM collection_resource
WHERE collection = ? AND resource NOT IN (SELECT resource FROM collection_resource WHERE collection = ?)", $parameters);
}
return true;
}
/**
* Retrieves the collection reference associated with a given research request.
*
* @param int $ref The reference number of the research request.
* @return int|false The collection reference if found, or false if not.
*/
function get_research_request_collection($ref)
{
$parameters = array("i",$ref);
$return = ps_value("select collection value from research_request where ref=?", $parameters, 0);
if (($return == 0) || (strlen($return) == 0)) {
return false;
} else {
return $return;
}
}
/**
* Updates the collection reference associated with a specified research request.
*
* @param int $research The reference number of the research request.
* @param int $collection The reference number of the collection to associate.
* @return void
*/
function set_research_collection($research, $collection)
{
$parameters = array("i",$collection, "i",$research);
ps_query("update research_request set collection=? where ref=?", $parameters);
}

9032
include/resource_functions.php Executable file

File diff suppressed because it is too large Load Diff

3475
include/search_functions.php Executable file

File diff suppressed because it is too large Load Diff

144
include/search_public.php Executable file
View File

@@ -0,0 +1,144 @@
<?php
if ((substr($search, 0, 11) != "!collection") && ($collections != "") && is_array($collections)) {
$shownresults = false;
for ($n = $offset; (($n < $result_count && $n < $colcount) && ($n < ($rowstoretrieve))); $n++) {
$resources = do_search("!collection" . $collections[$n]['ref'], "", "relevance", "", 5);
$pub_url = "search.php?search=" . urlencode("!collection" . $collections[$n]["ref"]);
if ($display == "thumbs" || $display == "xlthumbs") {
$shownresults = true;
?>
<div class="ResourcePanel">
<div class="ImageWrapper" style="position: relative;height:150px;">
<a onClick="return CentralSpaceLoad(this,true);" href="<?php echo $pub_url?>" title="<?php echo escape(str_replace(array("\"","'"), "", i18n_get_collection_name($collections[$n]))) ?>">
<?php
$images = 0;
for ($m = 0; $m < count($resources) && $images <= 4; $m++) {
$border = true;
$ref = $resources[$m]['ref'];
if ((int) $resources[$m]['has_image'] !== 0 && !resource_has_access_denied_by_RT_size($resources[$m]['resource_type'], 'col')) {
$previewpath = get_resource_path($ref, false, "col", false, "jpg", -1, 1, false, $resources[$m]["file_modified"]);
} else {
$previewpath = "{$baseurl_short}gfx/no_preview/default.png";
$border = false;
}
$modifiedurl = hook('searchpublicmodifyurl');
if ($modifiedurl) {
$previewpath = $modifiedurl;
$border = true;
}
$images++;
$space = 10 + ($images - 1) * 18;
?>
<img
alt="<?php echo escape(i18n_get_translated(($resources[$m]["field" . $view_title_field] ?? ""))); ?>"
style="position: absolute; top:<?php echo $space ?>px;left:<?php echo $space ?>px"
src="<?php echo $previewpath?>"
<?php echo $border ? 'class="ImageBorder"' : ''; ?>
>
<?php
}
?>
</a>
</div><!-- End of ImageWrapper -->
<?php hook("icons", "search", array("collections" => true)); //for spacing
//add spacing for display fields to even out the box size
for ($x = 0; $x < count($df); $x++) { ?>
<div class="ResourcePanelInfo">
<?php if (in_array($df[$x]['ref'], $thumbs_display_extended_fields)) { ?>
<div class="extended">
<?php
}
if ($x == count($df) - 1) { ?>
<a onClick="return CentralSpaceLoad(this, true);"
href="<?php echo $pub_url?>"
title="<?php echo escape(str_replace(array("\"", "'"), "", i18n_get_collection_name($collections[$n]))) ?>">
<?php echo highlightkeywords(tidy_trim(i18n_get_collection_name($collections[$n]), 32), $search)?>
</a>
<?php } ?>
&nbsp;
<?php if (in_array($df[$x]['ref'], $thumbs_display_extended_fields)) { ?>
</div>
<?php } ?>
</div><!-- End of ResourcePanelInfo -->
<?php
}
?>
<div class="ResourcePanelIcons" style="float:right;">
<a href="<?php echo $baseurl_short?>pages/collections.php?collection=<?php echo escape($collections[$n]["ref"]); ?>" onClick="return CollectionDivLoad(this);">
<?php echo LINK_CARET . escape($lang["action-select"]); ?>
</a>&nbsp;&nbsp;&nbsp;
<a onClick="return CentralSpaceLoad(this,true);" href="<?php echo $pub_url?>">
<?php echo LINK_CARET . escape($lang["view"]); ?>
</a>
</div>
<div class="clearer"></div>
</div><!-- End of ResourcePanel -->
<?php
}
if ($display == "list") { ?>
<tr>
<?php
if ($use_selection_collection) {
echo "<td></td>";
}
if (!isset($collections[$n]['savedsearch']) || (isset($collections[$n]['savedsearch']) && $collections[$n]['savedsearch'] == null)) {
$collection_prefix = $lang["collection"] . ": ";
$collection_tag = $lang['collection'];
} else {
$collection_prefix = ""; # The prefix $lang['smartcollection'] . ": " is added in i18n_get_collection_name()
$collection_tag = $lang['smartcollection'];
}
?>
<td nowrap>
<div class="ListTitle">
<a onClick="return CentralSpaceLoad(this,true);" href="<?php echo $pub_url?>" title="<?php echo str_replace(array("\"","'"), "", $collection_prefix . i18n_get_collection_name($collections[$n]))?>">
<?php echo $collection_prefix . highlightkeywords(tidy_trim(i18n_get_collection_name($collections[$n]), 45), $search)?>
</a>
</div>
</td>
<?php for ($x = 0; $x < count($df) - 1; $x++) { ?>
<td>-</td>
<?php } ?>
<?php if ($id_column) { ?>
<td><?php echo $collections[$n]['ref']; ?></td>
<?php } ?>
<?php if ($resource_type_column) { ?>
<td><?php echo $collection_tag?></td>
<?php } ?>
<?php if ($date_column) { ?>
<td><?php echo nicedate($collections[$n]["created"], false, true)?></td>
<?php } ?>
<td>
<div class="ListTools">
<a href="<?php echo $baseurl_short?>pages/collections.php?collection=<?php echo escape($collections[$n]["ref"]); ?>" onClick="return CollectionDivLoad(this);">
<?php echo LINK_CARET . escape($lang["action-select"]); ?>
</a>&nbsp;&nbsp;
<a onClick="return CentralSpaceLoad(this,true);" href="<?php echo $pub_url?>">
<?php echo LINK_CARET . escape($lang["viewall"]); ?>
</a>
</div>
</td>
</tr>
<?php
}
}
if (($display == "thumbs" || $display == "xlthumbs") && $shownresults) {
# If any collection results were displayed, carriage return before the results so the collections are visibly separate.
?>
<br style="clear:both;" />
<?php
}
} /* end if not a collection search */ ?>

View File

@@ -0,0 +1,327 @@
<?php
# display collection title if option set.
$search_title = "";
$search_title_links = "";
global $baseurl_short, $filename_field, $archive_standard,$related_search_searchcrumb_field;
# Display a title of the search (if there is a title)
$searchcrumbs = "";
// Get search URL, resetting pager
$search_params = get_search_params();
$search_url = generateURL($baseurl_short . 'pages/search.php', $search_params, array("offset" => 0, "go" => ""));
if ($search_titles_searchcrumbs && $use_refine_searchstring) {
$refinements = str_replace(" -", ",-", rawurldecode($search));
$refinements = explode(",", $search);
if (substr($search, 0, 1) == "!" && substr($search, 0, 6) != "!empty") {
$startsearchcrumbs = 1;
} else {
$startsearchcrumbs = 0;
}
if ($refinements[0] != "") {
for ($n = $startsearchcrumbs; $n < count($refinements); $n++) {
# strip the first semi-colon so it's not swapped with an " OR "
$semi_pos = strpos($refinements[$n], ":;");
if ($semi_pos !== false) {
$refinements[$n] = substr_replace($refinements[$n], ": ", $semi_pos, strlen(":;"));
}
$search_title_element = str_replace(";", " OR ", $refinements[$n]);
$search_title_element = search_title_node_processing($search_title_element);
if ($n != 0 || !$archive_standard) {
$searchcrumbs .= " > </count> </count> </count> ";
}
$searchcrumbs .= "<a href=\"" . $baseurl_short . "pages/search.php?search=";
for ($x = 0; $x <= $n; $x++) {
$searchcrumbs .= urlencode($refinements[$x]);
if ($x != $n && substr($refinements[$x + 1], 0) != "-") {
$searchcrumbs .= ",";
}
}
$search_title_element = explode(":", search_title_node_processing($refinements[$n]));
if (isset($search_title_element[1])) {
$datefieldinfo = ps_query("select ref from resource_type_field where name=? and type IN (4,6,10)", array("s",trim($search_title_element[0])), "schema");
if (count($datefieldinfo)) {
$search_title_element[1] = str_replace("|", "-", $search_title_element[1]);
$search_title_element[1] = str_replace("nn", "??", $search_title_element[1]);
}
if (!isset($cattreefields)) {
$cattreefields = array();
}
if (in_array($search_title_element[0], $cattreefields)) {
$search_title_element = $lang['fieldtype-category_tree'];
} else {
$search_title_element = str_replace(";", " OR ", $search_title_element[1]);
}
} else {
$search_title_element = $search_title_element[0];
}
if (substr(trim($search_title_element), 0, 6) == "!empty") {// superspecial !empty search
$search_title_elementq = trim(str_replace("!empty", "", rawurldecode($search_title_element)));
if (is_numeric($search_title_elementq)) {
$fref = $search_title_elementq;
$ftitle = ps_value("select title value from resource_type_field where ref=?", array("i",$search_title_elementq), "", "schema");
} else {
$ftitleref = ps_query("select title,ref from resource_type_field where name=?", array("s",$search_title_elementq), "schema");
if (!isset($ftitleref[0])) {
exit("invalid !empty search. No such field: $search_title_elementq");
}
$ftitle = $ftitleref[0]['title'];
$fref = $ftitleref[0]['ref'];
}
if ($ftitle == "") {
exit("invalid !empty search");
}
$search_title_element = str_replace("%field", lang_or_i18n_get_translated($ftitle, "fieldtitle-"), $lang["untaggedresources"]);
}
$searchcrumbs .= "&amp;order_by=" . urlencode($order_by) . "&amp;sort=" . urlencode($sort) . "&amp;offset=" . urlencode($offset) . "&amp;archive=" . urlencode($archive) . "&amp;sort=" . urlencode($sort) . "\" onClick='return CentralSpaceLoad(this,true);'>" . $search_title_element . "</a>";
}
}
}
if ($search_titles) {
if (substr($search, 0, 11) == "!collection") {
$col_title_ua = "";
// add a tooltip to Smart Collection titles (which provides a more detailed view of the searchstring.
$alt_text = '';
if ($pagename == "search" && isset($collectiondata['savedsearch']) && $collectiondata['savedsearch'] != '') {
$smartsearch = ps_query("select " . columns_in("collection_savedsearch") . " from collection_savedsearch where ref=?", array("i",$collectiondata['savedsearch']));
if (isset($smartsearch[0])) {
$alt_text = "title='search=" . $smartsearch[0]['search'] . "&restypes=" . $smartsearch[0]['restypes'] . "&archive=" . $smartsearch[0]['archive'] . "'";
}
}
hook("collectionsearchtitlemod");
$collection_trail = array();
$branch_trail = array();
global $enable_themes;
if (
$enable_themes
&& isset($collectiondata) && $collectiondata !== false
&& $collectiondata["type"] == COLLECTION_TYPE_FEATURED
) {
$general_url_params = ($k == "" ? array() : array("k" => $k));
$collection_trail[] = array(
"title" => $lang["themes"],
"href" => generateURL("{$baseurl_short}pages/collections_featured.php", $general_url_params)
);
$fc_branch_path = move_featured_collection_branch_path_root(
// We ask for the branch up from the parent as we want to generate a different link for the actual collection.
// If we were to use the $collectiondata["ref"] then the generated link for the collection would've pointed at
// collections_featured.php which we don't want
get_featured_collection_category_branch_by_leaf((int) $collectiondata["parent"], [])
);
$branch_trail = array_map(function ($branch) use ($baseurl_short, $general_url_params) {
return array(
"title" => i18n_get_translated($branch["name"]),
"href" => generateURL("{$baseurl_short}pages/collections_featured.php", $general_url_params, array("parent" => $branch["ref"])));
}, $fc_branch_path);
}
$full_collection_trail = array_merge($collection_trail, $branch_trail);
$full_collection_trail[] = array(
"title" => i18n_get_collection_name($collectiondata) . $col_title_ua,
"href" => generateURL($baseurl_short . "pages/search.php", $search_params),
"attrs" => array($alt_text),
);
ob_start();
renderBreadcrumbs($full_collection_trail, '', 'BreadcrumbsBoxSlim BreadcrumbsBoxTheme');
$renderBreadcrumbs = ob_get_contents();
ob_end_clean();
$renderBreadcrumbs = str_replace("</div></div>", "{$searchcrumbs}</div></div>", $renderBreadcrumbs);
$search_title .= $renderBreadcrumbs;
} elseif ($search == "" && $archive_standard) {
# Which resource types (if any) are selected?
$searched_types_refs_array = explode(",", $restypes); # Searched resource types and collection types
$resource_types_array = get_resource_types("", false, false, true); # Get all resource types, untranslated
$searched_resource_types_names_array = array();
for ($n = 0; $n < count($resource_types_array); $n++) {
if (in_array($resource_types_array[$n]["ref"], $searched_types_refs_array)) {
$searched_resource_types_names_array[] = lang_or_i18n_get_translated($resource_types_array[$n]["name"], "resourcetype-", "-2");
}
}
if (count($searched_resource_types_names_array) == count($resource_types_array)) {
# All resource types are selected, don't list all of them
unset($searched_resource_types_names_array);
$searched_resource_types_names_array[0] = $lang["all-resourcetypes"];
}
# Which collection types (if any) are selected?
$searched_collection_types_names_array = array();
if (in_array("mycol", $searched_types_refs_array)) {
$searched_collection_types_names_array[] = $lang["mycollections"];
}
if (in_array("pubcol", $searched_types_refs_array)) {
$searched_collection_types_names_array[] = $lang["publiccollections"];
}
if (in_array("themes", $searched_types_refs_array)) {
$searched_collection_types_names_array[] = $lang["themes"];
}
if (count($searched_collection_types_names_array) == 3) {
# All collection types are selected, don't list all of them
unset($searched_collection_types_names_array);
$searched_collection_types_names_array[0] = $lang["all-collectiontypes"];
}
if (count($searched_resource_types_names_array) > 0 && count($searched_collection_types_names_array) == 0) {
# Only (one or more) resource types are selected
$searchtitle = $lang["all"] . " " . implode($lang["resourcetypes_separator"] . " ", $searched_resource_types_names_array);
} elseif (count($searched_resource_types_names_array) == 0 && count($searched_collection_types_names_array) > 0) {
# Only (one or more) collection types are selected
$searchtitle = str_replace_formatted_placeholder("%collectiontypes%", $searched_collection_types_names_array, $lang["no_resourcetypes-collections"], false, $lang["collectiontypes_separator"]);
} elseif (count($searched_resource_types_names_array) > 0 && count($searched_collection_types_names_array) > 0) {
# Both resource types and collection types are selected
# Step 1: Replace %resourcetypes%
$searchtitle = $lang["all"] . " " . implode($lang["resourcetypes_separator"] . " ", $searched_resource_types_names_array);
//$searchtitle = str_replace_formatted_placeholder("%resourcetypes%", $searched_resource_types_names_array, $lang["resourcetypes-collections"], false, //$lang["resourcetypes_separator"]);
# Step 2: Replace %collectiontypes%
$searchtitle = str_replace_formatted_placeholder("%collectiontypes%", $searched_collection_types_names_array, $searchtitle, false, $lang["collectiontypes_separator"]);
} else {
# No resource types and no collection types are selected <20> show all resource types and all collection types
# Step 1: Replace %resourcetypes%
$searchtitle = str_replace_formatted_placeholder("%resourcetypes%", $lang["all-resourcetypes"], $lang["resourcetypes-collections"], false, $lang["resourcetypes_separator"]);
# Step 2: Replace %collectiontypes%
$searchtitle = str_replace_formatted_placeholder("%collectiontypes%", $lang["all-collectiontypes"], $searchtitle, false, $lang["collectiontypes_separator"]);
}
$search_title = '<div class="BreadcrumbsBox BreadcrumbsBoxSlim BreadcrumbsBoxTheme"><div class="SearchBreadcrumbs"><a href="' . $baseurl_short . 'pages/search.php?search=" onClick="return CentralSpaceLoad(this,true);">' . escape($searchtitle) . '</a></div></div> ';
} elseif (substr($search, 0, 1) == "!") {
// Special searches
if (substr($search, 0, 5) == "!last") {
$searchq = substr($search, 5);
$searchq = explode(",", $searchq);
$searchq = $searchq[0];
if (!is_numeric($searchq)) {
$searchq = 1000;
} # 'Last' must be a number. SQL injection filter.
$title_string = str_replace('%qty', $searchq, $lang["n_recent"]);
} elseif (substr($search, 0, 8) == "!related") {
$resource = substr($search, 8);
$resource = explode(",", $resource);
$resource = $resource[0];
$displayfield = get_data_by_field($resource, $related_search_searchcrumb_field);
if ($displayfield == '') {
$displayfield = get_data_by_field($resource, $filename_field);
}
$title_string = str_replace('%id%', $displayfield, $lang["relatedresources-id"]);
} elseif (substr($search, 0, 7) == "!unused") {
$title_string = $lang["uncollectedresources"];
} elseif (substr($search, 0, 11) == "!duplicates") {
$ref = explode(" ", $search);
$ref = str_replace("!duplicates", "", $ref[0]);
$ref = explode(",", $ref);// just get the number
$ref = $ref[0];
$filename = get_data_by_field($ref, $filename_field);
if ($ref != "") {
$title_string = $lang["duplicateresourcesfor"] . $filename ;
} else {
$title_string = $lang["duplicateresources"];
}
} elseif (substr($search, 0, 5) == "!list") {
$resources = substr($search, 5);
$resources = explode(",", $resources);
$resources = $resources[0];
$title_string = $lang["listresources"] . " " . $resources;
} elseif (substr($search, 0, 15) == "!archivepending") {
$title_string = $lang["resourcespendingarchive"];
} elseif (substr($search, 0, 12) == "!userpending") {
$title_string = $lang["userpending"];
} elseif (substr($search, 0, 10) == "!nopreview") {
$title_string = $lang["nopreviewresources"];
} elseif (substr($search, 0, 4) == "!geo") {
$title_string = "";
} elseif (substr($search, 0, 14) == "!contributions") {
$cuser = substr($search, 14);
$cuser = explode(",", $cuser);
$cuser = $cuser[0];
if ($cuser == $userref) {
switch ($archive) {
case -2:
$title_string = $lang["contributedps"];
break;
case -1:
$title_string = $lang["contributedpr"];
break;
case 0:
$title_string = $lang["contributedsubittedl"];
break;
default:
$title_string = $lang["mycontributions"];
break;
}
} else {
$udata = get_user($cuser);
if ($udata) {
$udisplayname = trim((string)$udata["fullname"]) != "" ? $udata["fullname"] : $udata["username"];
$title_string = $lang["contributedby"] . " " . $udisplayname . ((strpos($archive, ",") === false && !$archive_standard) ? " - " . $lang["status" . intval($archive)] : "");
}
}
} elseif (substr($search, 0, 8) == "!hasdata") {
$fieldref = intval(trim(substr($search, 8)));
$fieldinfo = get_resource_type_field($fieldref);
$fdisplayname = trim((string)$fieldinfo["title"]) != "" ? $fieldinfo["title"] : $fieldref;
$title_string = $lang["search_title_hasdata"] . " " . $fdisplayname;
} elseif (substr($search, 0, 6) == "!empty") {
$fieldref = intval(trim(substr($search, 6)));
$fieldinfo = get_resource_type_field($fieldref);
$displayname = i18n_get_translated($fieldinfo["title"]);
if (trim($displayname) == "") {
$displayname = $fieldinfo["ref"];
}
$title_string = $lang["search_title_empty"] . ' ' . $displayname;
} elseif (substr($search, 0, 14) == "!integrityfail") {
$title_string = $lang["file_integrity_fail_search"];
} elseif (substr($search, 0, 7) == "!locked") {
$title_string = $lang["locked_resource_search"];
}
if (isset($title_string) && $title_string != "") {
$search_title = '<div class="BreadcrumbsBox BreadcrumbsBoxSlim BreadcrumbsBoxTheme"><div class="SearchBreadcrumbs"><a href="' . $search_url . '" onClick="return CentralSpaceLoad(this,true);">' . escape($title_string) . '</a> ' . $searchcrumbs . '</div></div> ';
}
} elseif (!$archive_standard) {
$title_strings = [];
$wfstates = explode(",", $archive);
foreach ($wfstates as $wfstate) {
$title_strings[] = $lang["status" . $wfstate] ?? $lang["archive"] . ": " . $wfstate;
}
$search_title = '<div class="BreadcrumbsBox BreadcrumbsBoxSlim BreadcrumbsBoxTheme"><div class="SearchBreadcrumbs"><a href="' . $search_url . '" onClick="return CentralSpaceLoad(this,true);">' . escape(implode(", ", $title_strings)) . '</a>' . escape($searchcrumbs) . '</div></div> ';
}
hook("addspecialsearchtitle");
}
hook('add_search_title_links');
if (
!hook("replacenoresourcesfoundsearchtitle")
&& !is_array($result)
&& empty($collections)
&& getval("addsmartcollection", "") == ''
) {
$search_title = '<div class="BreadcrumbsBox BreadcrumbsBoxSlim BreadcrumbsBoxTheme"><div class="SearchBreadcrumbs"><a href="' . $search_url . '">' . $lang["noresourcesfound"] . '</a></div></div>';
}

1098
include/searchbar.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
<?php
/**
* Create/ Update a slideshow image record. Use NULL for $ref to create new records.
*
* @param integer $ref ID of the slideshow image. Use NULL to create a new record
* @param integer $resource_ref ID of the resource this slideshow is related to. Use NULL if no link is required
* @param integer $homepage_show Set to 1 if slideshow image should appear on the home page
* @param integer $featured_collections_show Set to 1 if slideshow image should appear on the featured collections page
* @param integer $login_show Set to 1 if slideshow image should appear on the login page
*
* @return boolean|integer Returns ID of the slideshow image(new/ updated), FALSE otherwise
*/
function set_slideshow($ref, $resource_ref = null, $homepage_show = 1, $featured_collections_show = 1, $login_show = 0)
{
if (
(!is_null($ref) && !is_numeric($ref))
|| (!(is_null($resource_ref) || trim($resource_ref) == '') && !is_numeric($resource_ref))
|| !is_numeric($homepage_show)
|| !is_numeric($featured_collections_show)
|| !is_numeric($login_show)
) {
return false;
}
$ref = ((int) $ref > 0 ? $ref : null);
$resource_ref = ((int) $resource_ref > 0 ? $resource_ref : null);
$query = "
INSERT INTO slideshow (ref, resource_ref, homepage_show, featured_collections_show, login_show)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY
UPDATE resource_ref = ?,
homepage_show = ?,
featured_collections_show = ?,
login_show = ?";
$query_params = array(
"i",$ref,
"i",$resource_ref,
"i",$homepage_show,
"i",$featured_collections_show,
"i",$login_show,
"i",$resource_ref,
"i",$homepage_show,
"i",$featured_collections_show,
"i",$login_show,
);
ps_query($query, $query_params);
// Clear cache
clear_query_cache("slideshow");
$new_ref = sql_insert_id();
if (is_null($ref) && $new_ref != 0) {
log_activity("Added new slideshow image", LOG_CODE_CREATED, null, 'slideshow', 'ref', $new_ref);
return $new_ref;
} elseif (!is_null($ref) && $new_ref != 0 && $ref == $new_ref) {
log_activity("Updated slideshow image", LOG_CODE_EDITED, null, 'slideshow', 'ref', $ref);
return $new_ref;
}
return false;
}
/**
* Delete slideshow record
*
* @param integer $ref ID of the slideshow
*
* @return boolean
*/
function delete_slideshow($ref)
{
$file_path = get_slideshow_image_file_path($ref);
if ($file_path != '' && unlink($file_path) === false) {
return false;
}
$query = "DELETE FROM slideshow WHERE ref = ?";
$query_params = ["i",$ref];
ps_query($query, $query_params);
log_activity("Deleted slideshow image", LOG_CODE_DELETED, null, 'slideshow', 'ref', $ref);
// Clear cache
clear_query_cache("slideshow");
return true;
}
/**
* Function used to re-order slideshow images
*
* @param array $from Slideshow image data we move FROM
* @param array $to Slideshow image data we move TO
*
* @return boolean
*/
function reorder_slideshow_images(array $from, array $to)
{
if (!file_exists($from['file_path'])) {
trigger_error('File "' . $from['file_path'] . '" does not exist or could not be found/accessed!');
}
if (!file_exists($to['file_path'])) {
trigger_error('File "' . $to['file_path'] . '" does not exist or could not be found/accessed!');
}
// Calculate files to be moved around
$from_file = $from['file_path'];
$temp_file = "{$from['file_path']}.tmp";
$to_file = $to['file_path'];
// Swap the slideshow images
if (!copy($from_file, $temp_file)) {
trigger_error("Failed to copy '{$from_file}' to temp file '{$temp_file}'");
}
if (rename($to_file, $from_file) && rename($temp_file, $to_file)) {
set_slideshow(
$from['ref'],
$to['resource_ref'],
$to['homepage_show'],
$to['featured_collections_show'],
$to['login_show']
);
set_slideshow(
$to['ref'],
$from['resource_ref'],
$from['homepage_show'],
$from['featured_collections_show'],
$from['login_show']
);
// Clear cache
clear_query_cache("slideshow");
return true;
}
return false;
}
/**
* Get the full path for the slideshow image file
*
* @param integer $ref ID of the slideshow image
*
* @return string The full path to the slideshow image
*/
function get_slideshow_image_file_path($ref)
{
$homeanim_folder_path = dirname(__DIR__) . "/{$GLOBALS['homeanim_folder']}";
$image_file_path = "{$homeanim_folder_path}/{$ref}.jpg";
if (!file_exists($image_file_path) || !is_readable($image_file_path)) {
return '';
}
return $image_file_path;
}

191
include/tab_functions.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
/**
* Access control check if user is allowed to manage system tabs.
*
* @return bool
* */
function acl_can_manage_tabs()
{
return checkperm('a');
}
/**
* Get entire tab records for a list of IDs
*
* @param array $refs List of tab refs
*
* @return array
*/
function get_tabs_by_refs(array $refs)
{
$refs = array_filter($refs, 'is_int_loose');
$refs_count = count($refs);
if ($refs_count > 0) {
return ps_query(
'SELECT ref, `name`, order_by FROM tab WHERE ref IN (' . ps_param_insert($refs_count) . ') ORDER BY order_by',
ps_param_fill($refs, 'i')
);
}
return [];
}
/**
* Get tabs (paged) based on some criteria (currently only order by and limit).
*
* @param array $criteria Array holding criteria information (order_by and limit).
*
* @return array {@see sql_limit_with_total_count()}
* */
function get_tabs_with_usage_count(array $criteria)
{
$order_by = isset($criteria['order_by'][0]) && in_array($criteria['order_by'][0], ['order_by', 'ref'])
? $criteria['order_by'][0]
: 'order_by';
$sort = isset($criteria['order_by'][1]) && validate_sort_value($criteria['order_by'][1])
? $criteria['order_by'][1]
: 'ASC';
$per_page = $criteria['limit']['per_page'] ?? null;
$offset = $criteria['limit']['offset'] ?? null;
$query = new PreparedStatementQuery(
"SELECT t.ref,
t.`name`,
t.order_by,
(SELECT count(ref) FROM resource_type_field WHERE tab = t.ref) AS usage_rtf,
(SELECT count(ref) FROM resource_type WHERE tab = t.ref) AS usage_rt
FROM tab AS t
ORDER BY {$order_by} {$sort}"
);
return sql_limit_with_total_count($query, $per_page, $offset);
}
/**
* Get all tab records, sorted by the order_by column
* @return array
* */
function get_all_tabs()
{
return ps_query('SELECT ref, `name`, order_by FROM tab ORDER BY order_by');
}
/**
* Get list of all tabs sorted based on current configuration. This always adds a fake record (ref #0) to indicate no assignment.
*
* @return array Key is the tabs' ID and value its translated name.
*/
function get_tab_name_options()
{
// The no selection option is always first
$tabs = array_map('i18n_get_translated', [0 => ''] + array_column(get_all_tabs(), 'name', 'ref'));
return sort_tabs_as_configured($tabs);
}
/**
* Sort list of tab names (preserving their key ID)
*
* @param array $tabs List of tab ID and tab translated name pairs
*
* @return array
* */
function sort_tabs_as_configured(array $tabs)
{
if ($GLOBALS['sort_tabs'] ?? false) {
asort($tabs);
}
return $tabs;
}
/**
* Create a new system tab record
* NOTE: order_by should only be set when re-ordering the set by the user. {@see sql_reorder_records('tab', $refs)}
*
* @return bool|int Return new tab record ID or FALSE otherwise
*/
function create_tab(array $tab)
{
$name = trim($tab['name'] ?? '');
if ($name !== '' && acl_can_manage_tabs()) {
ps_query(
'INSERT INTO tab (`name`, order_by)
VALUES (
?,
(
SELECT * FROM (
(SELECT ifnull(tab.order_by, 0) + 10 FROM tab ORDER BY ref DESC LIMIT 1)
UNION (SELECT 10)
) AS nob
LIMIT 1
))',
['s', $name]
);
$ref = sql_insert_id();
log_activity(null, LOG_CODE_CREATED, $name, 'tab', 'name', $ref);
return $ref;
}
return false;
}
/**
* Delete system tabs.
*
* IMPORTANT: never allow the "Default" tab (ref #1) to be deleted because this is the fallback location for information
* that has no association with other tabs.
*
* @param array $refs List of tab IDs
*
* @return bool Returns TRUE if it executed the query, FALSE otherwise
*/
function delete_tabs(array $refs)
{
if (!acl_can_manage_tabs()) {
return false;
}
$batch_activity_logger = function ($ref) {
log_activity(null, LOG_CODE_DELETED, null, 'tab', 'name', $ref);
};
$refs_chunked = array_chunk(
// Sanitise list: only numbers and never allow the "Default" tab (ref #1) to be deleted
array_diff(array_filter($refs, 'is_int_loose'), [1]),
SYSTEM_DATABASE_IDS_CHUNK_SIZE
);
foreach ($refs_chunked as $refs_list) {
$return = ps_query(
'DELETE FROM tab WHERE ref IN (' . ps_param_insert(count($refs_list)) . ')',
ps_param_fill($refs_list, 'i')
);
array_walk($refs_list, $batch_activity_logger);
}
return isset($return);
}
/**
* Update an existing tab.
* NOTE: order_by should only be set when re-ordering the set by the user. {@see sql_reorder_records('tab', $refs)}
*
* @param array $tab A tab record (type)
*
* @return bool Returns TRUE if it executed the query, FALSE otherwise
*/
function save_tab(array $tab)
{
$ref = (int) $tab['ref'];
$name = trim($tab['name']);
if ($ref > 0 && $name !== '' && acl_can_manage_tabs()) {
log_activity(null, LOG_CODE_EDITED, $name, 'tab', 'name', $ref);
ps_query('UPDATE tab SET `name` = ? WHERE ref = ?', ['s', $name, 'i', $ref]);
return true;
}
return false;
}

176
include/test_functions.php Normal file
View File

@@ -0,0 +1,176 @@
<?php
use Montala\ResourceSpace\CommandPlaceholderArg;
/**
* Generates a random JPEG image for a given resource with random background color and text.
*
* This function creates an image of specified dimensions with a random background color
* and text indicating the resource reference. The image is then saved to the resource's
* designated path and also triggers the creation of previews for the resource.
*
* @param int $resource The reference ID of the resource for which the image is being created.
* @param int $width The width of the generated image in pixels.
* @param int $height The height of the generated image in pixels.
* @return bool Returns true on success, indicating that the image was created and saved successfully.
*/
function resource_random_jpg($resource, $width, $height)
{
// Set random colours
$bg_r = mt_rand(0, 255);
$bg_g = mt_rand(0, 255);
$bg_b = mt_rand(0, 255);
// Create image
$test_image = imagecreate($width, $height);
$text_r = $bg_r < 128 ? $bg_r + 100 : $bg_r - 100;
$text_g = $bg_g < 128 ? $bg_g + 100 : $bg_g - 100;
$text_b = $bg_b < 128 ? $bg_b + 100 : $bg_b - 100;
$text_col = imagecolorallocate($test_image, $text_r, $text_g, $text_b);
imagestring($test_image, 5, 20, 15, 'Image ' . $resource, $text_col);
$path = get_resource_path($resource, true, '', true, 'jpg');
imagejpeg($test_image, $path, 50);
create_previews($resource, false, 'jpg');
return true;
}
/**
* Generate a random image which can be used during testing (e.g to upload, or create previews for)
*
* @param array $info Set image parameters:
* - text -> Image content text
* - filename (default: random)
* - width (default: 150)
* - height (default: 50)
* - bg[red|green|blue] -> Background colour (RGB), e.g $info['bg']['red'] = 234;
*
* @return array Returns an "error" key if something went wrong, otherwise provides some useful info (e.g path)
*/
function create_random_image(array $info): array
{
$width = $info['width'] ?? 150;
$height = $info['height'] ?? 50;
$filename = $info['filename'] ?? generateSecureKey(32);
// Background colour
$bg_r = $info['bg']['red'] ?? mt_rand(0, 255);
$bg_g = $info['bg']['green'] ?? mt_rand(0, 255);
$bg_b = $info['bg']['blue'] ?? mt_rand(0, 255);
// Text colour
$text_r = $bg_r < 128 ? $bg_r + 100 : $bg_r - 100;
$text_g = $bg_g < 128 ? $bg_g + 100 : $bg_g - 100;
$text_b = $bg_b < 128 ? $bg_b + 100 : $bg_b - 100;
// Generate image
$img = imagecreate($width, $height);
imagecolorallocate($img, $bg_r, $bg_g, $bg_b);
imagestring($img, 5, 20, 15, $info['text'], imagecolorallocate($img, $text_r, $text_g, $text_b));
$path = get_temp_dir() . DIRECTORY_SEPARATOR . "{$filename}.jpg";
if (imagejpeg($img, $path, 70)) {
return [
'path' => $path,
];
}
return [
'error' => 'Failed to create image',
];
}
/**
* Generate a random video which can be used during testing (e.g to upload, or create previews for)
*
* @param array $info Set video parameters:
* - duration (default: 5 seconds)
* - width (default: 300)
* - height (default: 300)
* - filename (default: random)
* - extension (default: mp4)
* - text -> Video content text (optional)
*
* @return array Returns an "error" key if something went wrong, otherwise provides some useful info (e.g path)
*/
function create_random_video(array $info): array
{
$duration = $info['duration'] ?? 5;
$width = $info['width'] ?? 300;
$height = $info['height'] ?? 300;
$filename = $info['filename'] ?? generateSecureKey(32);
$extension = $info['extension'] ?? 'mp4';
if (is_banned_extension($extension)) {
$extension = 'mp4';
}
$ffmpeg = get_utility_path('ffmpeg');
if ($ffmpeg !== false && in_array($extension, $GLOBALS['ffmpeg_supported_extensions'])) {
// Add text to video only if supported
if (isset($info['text']) && mb_strpos(run_command($ffmpeg, true), '--enable-libfontconfig') !== false) {
$cmd_vf = '-vf drawtext=text=%info_text'
. ":font='Times New Roman':fontsize=10:fontcolor=black:box=1:boxcolor=white:boxborderw=5";
$cmd_vf_params = [
'%info_text' => new CommandPlaceholderArg(
$info['text'],
fn($val): bool => preg_match('/^[a-zA-Z0-9\#\s]*$/', $val) === 1
),
];
} else {
$cmd_vf = '';
$cmd_vf_params = [];
}
// Create video file
$path = get_temp_dir() . DIRECTORY_SEPARATOR . safe_file_name($filename) . ".{$extension}";
$cmd_output = run_command(
"$ffmpeg -i testsrc=duration=%duration:size=%wx%h:rate=30 $cmd_vf %outfile",
true,
array_merge(
[
'%duration' => (int) $duration,
'%w' => (int) $width,
'%h' => (int) $height,
'%outfile' => new CommandPlaceholderArg($path, fn(): bool => true),
],
$cmd_vf_params
)
);
if (mb_strpos($cmd_output, ' Error ') !== false) {
return [
'error' => $cmd_output,
];
}
return [
'path' => $path,
];
}
return [
'error' => 'FFMpeg missing',
];
}
/**
* Debug logs for ResourceSpace automated tests
* @uses RS_TEST_DEBUG constant {@see tests/test.php}
*/
function test_log(string $msg): void
{
if (!RS_TEST_DEBUG) {
return;
}
echo PHP_EOL . $msg;
}
/**
* Get the test files' ID (from its file name)
*/
function test_get_file_id(string $file): int
{
$matches = [];
return preg_match('/[1-9][0-9]{2,}/', basename($file), $matches) === 1 ? (int) $matches[0] : 0;
}

19
include/tusconfig.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
global $scramble_key, $usersession;
$cachepath = get_temp_dir() . DIRECTORY_SEPARATOR . "tus" . DIRECTORY_SEPARATOR . md5($scramble_key . $usersession) . DIRECTORY_SEPARATOR;
return [
/**
* File cache configs.
* IMPORTANT: not recommended for production! (source: https://github.com/ankitpokhrel/tus-php/blob/main/README.md)
*/
'file' => [
'dir' => $cachepath,
'name' => 'tus_php.server.cache',
],
/*
Redis - by default will assume a local instance.
If you need to change the default connection parameters please see lib/tus/vendor/ankitpokhrel/tus-php/src/Config/server.php
*/
];

3715
include/user_functions.php Normal file

File diff suppressed because it is too large Load Diff

135
include/user_rating.php Executable file
View File

@@ -0,0 +1,135 @@
<?php
#
#
# Provides the User Rating function on the resource view page (if enabled)
# ------------------------------------------------------------------------
#
$rating = $resource["user_rating"];
$modified_user_rating = hook("modifyuserrating");
if ($modified_user_rating) {
$result[$n]['user_rating'] = $modified_user_rating;
}
$rating_count = $resource["user_rating_count"];
if ($rating == "") {
$rating = 0;
}
if ($rating_count == "") {
$rating_count = 0;
}
// for 'remove rating' tool, determine if user has a rating for this resource
if ($user_rating_only_once) {
$ratings = array();
$ratings = ps_query("select user,rating from user_rating where ref=?", array("i",$ref));
$current = "";
for ($n = 0; $n < count($ratings); $n++) {
if ($ratings[$n]['user'] == $userref) {
$current = $ratings[$n]['rating'];
}
}
$removeratingvis = ($current != "") ? "inline" : "none";
}
?>
<br />
<script type="text/javascript">
var UserRatingDone=false;
function UserRatingDisplay(rating,hiclass)
{
if (UserRatingDone) {return false;}
for (var n=1;n<=5;n++)
{
jQuery('#RatingStar'+n).removeClass('StarEmpty');
jQuery('#RatingStar'+n).removeClass('StarCurrent');
jQuery('#RatingStar'+n).removeClass('StarSelect');
if (n<=rating)
{
jQuery('#RatingStar'+n).addClass(hiclass);
}
else
{
jQuery('#RatingStar'+n).addClass('StarEmpty');
}
}
}
function UserRatingSet(userref,ref,rating)
{
jQuery('#RatingStarLink'+rating).blur(); // removes the white focus box around the star.
if (UserRatingDone) {return false;}
jQuery.post(
baseurl_short + "pages/ajax/user_rating_save.php",
{
userref: userref,
ref: ref,
rating: rating,
<?php echo generateAjaxToken('UserRatingSet'); ?>
}
);
document.getElementById('RatingCount').style.visibility='hidden';
if (rating==0)
{
UserRatingDone=false;
UserRatingDisplay(0,'StarSelect');
UserRatingDone=true;
document.getElementById('UserRatingMessage').innerHTML="<?php echo escape($lang["ratingremoved"])?>";
document.getElementById('RatingStarLink0').style.display = 'none';
}
else
{
UserRatingDone=true;
document.getElementById('UserRatingMessage').innerHTML="<?php echo escape($lang["ratingthankyou"])?>";
}
}
</script>
<table cellpadding="0" cellspacing="0" width="100%">
<tr class="DownloadDBlend">
<td id="UserRatingMessage"><?php echo escape($lang["ratethisresource"])?></td>
<td width="33%" class="RatingStars" onMouseOut="UserRatingDisplay(<?php echo escape($rating) ?>,'StarCurrent');">
<div class="RatingStarsContainer">
<?php if ($user_rating_only_once) { ?>
<a
href="#"
onClick="UserRatingSet(<?php echo $userref?>,<?php echo escape($ref) ?>,0);return false;"
title="<?php echo escape($lang["ratingremovehover"])?>"
style="display:<?php echo $removeratingvis;?>">
<span id="RatingStarLink0">X&nbsp;&nbsp;</span>
</a>
<?php
}
for ($n = 1; $n <= 5; $n++) { ?>
<a
href="#"
onMouseOver="UserRatingDisplay(<?php echo $n?>,'StarSelect');"
onClick="UserRatingSet(<?php echo $userref?>,<?php echo escape($ref) ?>,<?php echo $n?>);return false;"
id="RatingStarLink<?php echo $n?>">
<span id="RatingStar<?php echo $n?>" class="Star<?php echo $n <= $rating ? "Current" : "Empty"; ?>">
<img alt="" src="<?php echo $baseurl?>/gfx/interface/sp.gif" width="15" height="15">
</span>
</a>
<?php
}
?>
</div>
<div class="RatingCount" id="RatingCount">
<?php if ($user_rating_stats && $user_rating_only_once) { ?>
<a
onClick="return CentralSpaceLoad(this,true);"
href="<?php echo $baseurl?>/pages/user_ratings.php?ref=<?php echo $ref?>&amp;search=<?php echo urlencode($search)?>&amp;offset=<?php echo urlencode($offset)?>&amp;order_by=<?php echo urlencode($order_by) ?>&amp;sort=<?php echo urlencode($sort) ?>&amp;archive=<?php echo urlencode($archive) ?>">
<?php
}
echo urlencode($rating_count) . " " . $rating_count == 1 ? escape($lang["rating_lowercase"]) : escape($lang["ratings"]);
if ($user_rating_stats && $user_rating_only_once) { ?>
</a>
<?php
} ?>
</div>
</td>
</tr>
</table>

278
include/user_select.php Executable file
View File

@@ -0,0 +1,278 @@
<?php
# AJAX user selection.
if (!isset($userstring)) {
$userstring = "";
}
// $autocomplete_user_scope needs to be set if we have more than one user select field on a page
if (!isset($autocomplete_user_scope)) {
$autocomplete_user_scope = "";
}
?>
<table class="user_select_table" cellpadding="0" cellspacing="0">
<!-- autocomplete -->
<?php if (isset($user_select_internal) && $user_select_internal) { ?>
<tr>
<td>
<input type="text" onpaste="return false;" class="stdwidth" value="<?php echo escape($lang["starttypingusername"]); ?>" id="<?php echo escape($autocomplete_user_scope); ?>autocomplete" name="autocomplete_parameter" onFocus="if(this.value == '<?php echo escape($lang['starttypingusername']); ?>') {this.value = ''}" onBlur="if(this.value == '') {this.value = '<?php echo escape($lang['starttypingusername']); ?>';}" />
</td>
</tr>
<?php } else { ?>
<tr>
<td>
<input type="text" onpaste="return false;" class="medwidth" value="<?php echo escape($lang["starttypingusername"]); ?>" id="<?php echo escape($autocomplete_user_scope); ?>autocomplete" name="autocomplete_parameter" onFocus="if(this.value == '<?php echo escape($lang['starttypingusername']); ?>') {this.value = ''}" onBlur="if(this.value == '') {this.value = '<?php echo escape($lang['starttypingusername']); ?>';}" />
</td>
<td>
<input id="<?php echo escape($autocomplete_user_scope); ?>adduserbutton" type=button value="+" class="medcomplementwidth" onClick="<?php echo escape($autocomplete_user_scope); ?>addUser();" />
</td>
</tr>
<?php
} ?>
<?php if (isset($single_user_select_field_id)) { ?>
<tr>
<td colspan="2" align="left">
<input type="text" readonly="readonly" class="stdwidth" name="<?php echo escape($autocomplete_user_scope); ?>users" id="<?php echo escape($autocomplete_user_scope); ?>users" value="<?php
if (isset($single_user_select_field_value)) {
$found_single_user_select_field_value = ps_value("select username as value from user where ref=?", array("s",$single_user_select_field_value), '');
echo escape($found_single_user_select_field_value);
}
?>" />
<?php if ($found_single_user_select_field_value != '') { ?>
<script>jQuery("#<?php echo escape($autocomplete_user_scope); ?>adduserbutton").attr('value', '<?php echo escape($lang["clearbutton"]); ?>');</script>
<?php } ?>
<input type="hidden" id="<?php echo escape($single_user_select_field_id); ?>" name="<?php echo escape($single_user_select_field_id); ?>" value="<?php
if (isset($single_user_select_field_value)) {
echo escape($single_user_select_field_value);
} ?>" />
</td>
</tr>
<?php } else { ?>
<!-- user string -->
<tr>
<td colspan="2" align="left">
<textarea
rows=6
class="stdwidth"
name="<?php echo escape($autocomplete_user_scope); ?>users"
id="<?php echo escape($autocomplete_user_scope); ?>users"
<?php if (!$sharing_userlists) {?>
onChange="this.value=this.value.replace(/[^,] /g,function replacespaces(str) {return str.substring(0,1) + ', ';});"
<?php } else { ?>
onChange="<?php echo escape($autocomplete_user_scope); ?>addUser();<?php echo escape($autocomplete_user_scope); ?>checkUserlist();<?php echo escape($autocomplete_user_scope);
echo escape($autocomplete_user_scope); ?>updateUserSelect();"
<?php } ?>><?php echo escape($userstring); ?></textarea>
</td>
</tr>
<?php
}
if ($sharing_userlists) { ?>
<tr>
<td>
<div id="<?php echo escape($autocomplete_user_scope); ?>userlist_name_div" style="display:none;">
<input type="text" class="medwidth" value="<?php echo escape($lang['typeauserlistname']); ?>" id="<?php echo escape($autocomplete_user_scope); ?>userlist_name_value" name="userlist_parameter" onClick="this.value='';" />
</div>
</td>
<td>
<div id="<?php echo escape($autocomplete_user_scope); ?>userlist_+" style="display:none;">
<input type=button value="<?php echo escape($lang['saveuserlist']); ?>" class="medcomplementwidth" onClick="<?php echo escape($autocomplete_user_scope); ?>saveUserList();" />
</div>
</td>
</tr>
<tr>
<td>
<select
id="<?php echo escape($autocomplete_user_scope); ?>userlist_select"
class="medwidth"
onchange="
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>users').value = document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').value;
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_name_div').style.display = 'none';
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_+').style.display = 'none';
if (document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').value == '') {
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_delete').style.display='none';
} else {
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_delete').style.display='inline';
}"
></select>
</td>
<td>
<input type=button id="<?php echo escape($autocomplete_user_scope); ?>userlist_delete" value="<?php echo escape($lang['deleteuserlist']); ?>" style="display:none;" class="medcomplementwidth" onClick="<?php echo escape($autocomplete_user_scope); ?>deleteUserList();" />
</td>
</tr>
<?php } ?>
</table>
<script type="text/javascript">
function <?php echo escape($autocomplete_user_scope); ?>addUser(event, ui) {
var username = document.getElementById("<?php echo escape($autocomplete_user_scope); ?>autocomplete").value;
var users = document.getElementById("<?php echo escape($autocomplete_user_scope); ?>users");
if (typeof ui !== 'undefined') {
username = ui.item.value;
}
if (username.indexOf("<?php echo escape($lang["group"]); ?>") != -1) {
if ((confirm("<?php echo escape($lang["confirmaddgroup"]); ?>")) == false) {
return false;
}
}
if (username.indexOf("<?php echo escape($lang["groupsmart"]); ?>") != -1) {
if ((confirm("<?php echo escape($lang["confirmaddgroupsmart"]); ?>")) == false) {
return false;
}
}
<?php if (isset($single_user_select_field_id)) { ?>
var user_ref = '';
jQuery.ajax({
url: '<?php echo $baseurl; ?>/pages/ajax/autocomplete_user.php?getuserref=' + username,
type: 'GET',
async: false,
success: function(ref, textStatus, xhr) {
if (xhr.status == 200 && ref > 0) {
user_ref=ref;
}
}
});
var single_user_field = document.getElementById("<?php echo escape($single_user_select_field_id); ?>");
single_user_field.value = user_ref;
users.value = '';
if (user_ref == '') {
username = '';
jQuery("#<?php echo escape($autocomplete_user_scope); ?>adduserbutton").attr('value', '+');
} else {
jQuery("#<?php echo escape($autocomplete_user_scope); ?>adduserbutton").attr('value', '<?php echo escape($lang["clearbutton"]); ?>');
}
<?php
if (isset($single_user_select_field_onchange)) {
echo $single_user_select_field_onchange;
}
}
?>
if (username != "") {
if (users.value.length != 0) {
users.value += ", ";}
users.value += username;
}
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>autocomplete").value = "";
<?php if ($sharing_userlists) { ?>
var parameters = 'userstring=' + users.value;
var newstring = jQuery.ajax("<?php echo $baseurl; ?>/pages/ajax/username_list_update.php",
{
data: parameters,
complete: function(modified) {users.value=modified.responseText; checkUserlist();}
}
);
<?php } ?>
return false;
}
<?php
# Limit results - default is users, groups and smart groups.
$limit_results = '';
if (isset($single_user_select_field_id)) {
$limit_results = '?nogroups=true';
} elseif (isset($no_smart_groups)) {
$limit_results = '?nosmartgroups=true';
}
?>
jQuery(document).ready(function () {
jQuery('#<?php echo escape($autocomplete_user_scope); ?>autocomplete').autocomplete({
source: "<?php echo $baseurl; ?>/pages/ajax/autocomplete_user.php<?php echo escape($limit_results); ?>",
select: <?php echo escape($autocomplete_user_scope); ?>addUser,
classes: {
"ui-autocomplete": "userselect"
}
});
});
<?php if ($sharing_userlists) { ?>
<?php echo escape($autocomplete_user_scope); ?>updateUserSelect();
jQuery("#userlist_name_value").autocomplete({
source:"<?php echo $baseurl; ?>/pages/ajax/autocomplete_userlist.php"
});
<?php } ?>
<?php if ($sharing_userlists) { ?>
function checkUserlist() {
// conditionally add option to save userlist if string is new
var userstring = document.getElementById("<?php echo escape($autocomplete_user_scope); ?>users").value;
var sel = document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').options;
var newstring = true;
for (n = 0; n <= sel.length-1; n++) {
//alert (document.getElementById('<?php echo escape($autocomplete_user_scope); ?>users').value+'='+sel[n].value);
if (document.getElementById('<?php echo escape($autocomplete_user_scope); ?>users').value==sel[n].value) {
sel[n].selected = true;
document.getElementById("userlist_delete").style.display = 'inline';
newstring = false;
break;
}
}
if (newstring) {
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_name_div").style.display = 'block';
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_+").style.display = 'block';
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').value = "";
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_name_value').value = '';
document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_name_value').placeholder = '<?php echo escape($lang['typeauserlistname'])?>';
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_delete").style.display = 'none';
} else {
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_name_div").style.display = 'none';
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_+").style.display = 'none';
}
}
function <?php echo escape($autocomplete_user_scope); ?>saveUserList() {
var parameters = 'userref=<?php echo escape($userref); ?>&userstring=' + document.getElementById("<?php echo escape($autocomplete_user_scope); ?>users").value+'&userlistname='+document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_name_value").value;
jQuery.ajax("<?php echo $baseurl; ?>/pages/ajax/userlist_save.php",
{
data: parameters,
complete: function(){
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_name_div").style.display='none';
document.getElementById("<?php echo escape($autocomplete_user_scope); ?>userlist_+").style.display='none';
<?php echo escape($autocomplete_user_scope); ?>updateUserSelect();
}
}
);
}
function <?php echo escape($autocomplete_user_scope); ?>deleteUserList() {
var parameters = 'delete=true&userlistref='+document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').options[document.getElementById('<?php echo escape($autocomplete_user_scope); ?>userlist_select').selectedIndex].id;
jQuery.ajax("<?php echo $baseurl; ?>/pages/ajax/userlist_save.php", {
data: parameters,
complete: function() {
<?php echo escape($autocomplete_user_scope); ?>updateUserSelect();
}
});
}
function <?php echo escape($autocomplete_user_scope); ?>updateUserSelect() {
var parameters = 'userref=<?php echo escape($userref); ?>&userstring='+document.getElementById("<?php echo escape($autocomplete_user_scope); ?>users").value;
jQuery("#userlist_select").load("<?php echo $baseurl; ?>/pages/ajax/userlist_select_update.php",
parameters,
function() {
<?php echo escape($autocomplete_user_scope); ?>checkUserlist();
}
);
}
<?php
} ?>
</script>

View File

@@ -0,0 +1,53 @@
<div class="Question">
<label for="groupselect"><?php echo escape($lang["group"]); ?></label>
<select
id="groupselect"
name="groupselect"
class="stdwidth"
onchange="
if (this.value=='viewall') {
document.getElementById('groupselector').style.display='none';
} else {
document.getElementById('groupselector').style.display='block';
}">
<?php if (!checkperm("U")) { ?>
<option
<?php echo ($groupselect == "viewall") ? "selected " : '';?>
value="viewall">
<?php echo escape($lang["allgroups"]); ?>
</option>
<?php } ?>
<option
<?php echo ($groupselect == "select") ? "selected" : ''; ?>
value="select">
<?php echo escape($lang["select"]); ?>
</option>
</select>
<div class="clearerleft"></div>
<table
id="groupselector"
cellpadding=3
cellspacing=3
style="padding-left:150px;<?php echo ($groupselect == "viewall") ? "display:none;" : ''; ?>">
<?php
$grouplist = get_usergroups(true);
for ($n = 0; $n < count($grouplist); $n++) { ?>
<tr>
<td valign=middle nowrap><?php echo escape($grouplist[$n]["name"])?>&nbsp;&nbsp;</td>
<td width=10 valign=middle>
<input
type=checkbox
name="groups[]"
value="<?php echo $grouplist[$n]["ref"]; ?>"
<?php echo (in_array($grouplist[$n]["ref"], $groups)) ? "checked" : ''; ?>
>
</td>
</tr>
<?php
}
?>
</table>
<div class="clearerleft"></div>
</div>

9
include/version.php Executable file
View File

@@ -0,0 +1,9 @@
<?php
$productname = "ResourceSpace"; # Product name. Do not change.
$productversion = "SVN 10.6";
# External items reload key
# Increment the below value to force a reload of the CSS, JavaScripts and other included items.
# This is intended for developer use when altering such files. It will force a reload on all client browsers.
$css_reload_key = 2;

141
include/video_functions.php Normal file
View File

@@ -0,0 +1,141 @@
<?php
/**
* Get video resolution and framerate using FFMpeg
*
* @uses get_video_info()
*
* @param string $file Path to video file
*
* @return array
*/
function get_video_resolution($file)
{
$video_resolution = array(
'width' => 0,
'height' => 0,
'framerate' => 0.0,
);
$video_info = get_video_info($file);
// Different versions of ffprobe store the dimensions in different parts of the json output
if (!empty($video_info['width'])) {
$video_resolution['width'] = intval($video_info['width']);
}
if (!empty($video_info['height'])) {
$video_resolution['height'] = intval($video_info['height']);
}
if (!empty($video_info['r_frame_rate'])) {
$framerate_pieces = explode('/', $video_info['r_frame_rate']);
if (floatval($framerate_pieces[1]) > 0) {
$video_resolution['framerate'] = floatval($framerate_pieces[0] / $framerate_pieces[1]);
}
}
if (isset($video_info['streams']) && is_array($video_info['streams'])) {
foreach ($video_info['streams'] as $stream) {
if (!empty($stream['codec_type']) && 'video' === $stream['codec_type']) {
$video_resolution['width'] = intval($stream['width']);
$video_resolution['height'] = intval($stream['height']);
$framerate_pieces = explode('/', $stream['r_frame_rate']);
if (floatval($framerate_pieces[1]) > 0) {
$video_resolution['framerate'] = floatval($framerate_pieces[0] / $framerate_pieces[1]);
}
break;
}
}
}
return $video_resolution;
}
/**
* Generate HTML to display subtitles in playback of a video resource.
*
* @param int $ref Resource ID
* @param int $access Resource access level - e.g. 0 for open access
*
* @return void
*/
function display_video_subtitles($ref, $access)
{
global $alt_files_visible_when_restricted;
$alt_access = hook("altfilesaccess");
if ($access == 0 || $alt_files_visible_when_restricted) {
$alt_access = true; # open access (not restricted)
}
if ($alt_access) {
$video_altfiles = get_alternative_files($ref);
foreach ($video_altfiles as $video_altfile) {
if (mb_strtolower($video_altfile["file_extension"]) == "vtt") {
$video_altfile["path"] = get_resource_path($ref, false, '', true, $video_altfile["file_extension"], -1, 1, false, '', $video_altfile["ref"]);
?>
<track class="videojs_alt_track" kind="subtitles" src="<?php echo escape($video_altfile["path"]) ?>" label="<?php echo escape($video_altfile["description"]); ?>" ></track>
<?php
}
}
}
}
/**
* Generate JSON array of VideoJS options to be used in the data-setup attribute
*
* @param bool $view_as_gif True if the video is a GIF file
* @param bool $play_on_hover True if playing video on hover
* @param array $video_preview_sources Array of preview sources, including URL, type and label
*
* @return string|false
*/
function generate_videojs_options(bool $view_as_gif, bool $play_on_hover, array $video_preview_sources)
{
global $videojs_resolution_selection, $videojs_resolution_selection_default_res;
$data_setup = ["playbackRates" => [0.5, 1, 1.5, 2]];
if ($view_as_gif) {
$data_setup = array_merge($data_setup, [
"controls" => false,
"autoplay" => true,
"loop" => true,
"muted" => true
]);
}
if ($play_on_hover && !$view_as_gif) {
$data_setup = array_merge($data_setup, [
"loadingSpinner" => false,
"TextTrackDisplay" => true,
"nativeTextTracks" => false,
"children" => [
"bigPlayButton" => false,
"controlBar" => [
"children" => [
"playToggle" => false,
"volumeControl" => false
]
]
]
]);
}
if (isset($videojs_resolution_selection) && count($video_preview_sources) > 0 && !$view_as_gif) {
$data_setup = array_merge($data_setup, [
"plugins" => [
"videoJsResolutionSwitcher" => [
"default" => $videojs_resolution_selection_default_res
]
]
]);
}
return json_encode($data_setup);
}