View Issue Details

IDProjectCategoryView StatusLast Update
0031678mantisbtapi restpublic2023-09-01 07:22
Reportermgmantis Assigned Todregad  
PriorityhighSeveritymajorReproducibilityalways
Status closedResolutionno change required 
Product Version2.25.5 
Summary0031678: cannot fetch data with rest api after upgrade
Description

recently we upgraded mantis from 2.11.1 to 2.25.5, and PHP from 7 to 8.1.11, but it is noticed that users can no longer fetch data with the rest api

Steps To Reproduce

simply go to <mantis home>/api/rest, we got the following:

Deprecated: Return type of Pimple\Container::offsetExists($id) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/pimple/pimple/src/Pimple/Container.php on line 133

Deprecated: Return type of Pimple\Container::offsetGet($id) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/pimple/pimple/src/Pimple/Container.php on line 98

Deprecated: Return type of Pimple\Container::offsetSet($id, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/pimple/pimple/src/Pimple/Container.php on line 79

Deprecated: Return type of Pimple\Container::offsetUnset($id) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/pimple/pimple/src/Pimple/Container.php on line 143

Deprecated: Return type of Slim\Collection::offsetExists($key) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 112

Deprecated: Return type of Slim\Collection::offsetGet($key) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 124

Deprecated: Return type of Slim\Collection::offsetSet($key, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 135

Deprecated: Return type of Slim\Collection::offsetUnset($key) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 145

Deprecated: Return type of Slim\Collection::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 155

Deprecated: Return type of Slim\Collection::getIterator() should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 165

Deprecated: Return type of Slim\Collection::offsetExists($key) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 112

Deprecated: Return type of Slim\Collection::offsetGet($key) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 124

Deprecated: Return type of Slim\Collection::offsetSet($key, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 135

Deprecated: Return type of Slim\Collection::offsetUnset($key) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 145

Deprecated: Return type of Slim\Collection::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 155

Deprecated: Return type of Slim\Collection::getIterator() should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Collection.php on line 165

Fatal error: Uncaught RuntimeException: Unexpected data in output buffer. Maybe you have characters before an opening <?php tag? in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/App.php:621 Stack trace: #0 /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/App.php(317): Slim\App->finalize(Object(Slim\Http\Response)) 0000001 /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/api/rest/index.php(107): Slim\App->run() 0000002 {main} thrown in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/App.php on line 621

TagsNo tags attached.

Relationships

related to 0031695 closeddregad Fix Automatic conversion of false to array is deprecated notice on PHP 8.1 
related to 0031699 confirmed Upgrade Slim Framework to 4.x 
related to 0032866 closeddregad Allow REST API to run on PHP 8.1 without squelching E_DEPRECATED notices 

Activities

cbspencer

cbspencer

2022-11-29 16:24

reporter   ~0067174

We have checked file encoding and also php end tags, and everything seems ok.

dregad

dregad

2022-11-30 05:13

developer   ~0067175

Pimple is a dependency of the Slim Framework that we use to power the REST API. According to its README file,

Pimple is now closed for changes. No new features will be added and no cosmetic changes will be accepted either. The only accepted changes are compatibility with newer PHP versions and security issue fixes.

Recent versions (4.x) of Slim no longer require Pimple, but unfortunately we're currently stuck with 3.x due to our PHP requirements.

We could try to submit a pull request to Pimple repository since they mention accepting PHP compatibility changes.

That being said, while I do get the deprecation notices in my PHP error log, I am not able to reproduce the Uncaught RuntimeException: Unexpected data in output buffer fatal error you report. The REST API seems to work just fine (tested on PHP 8.1.2). What are the contents of the output buffer ? Can you provide additional information and detailed steps to reproduce ?

dregad

dregad

2022-11-30 06:26

developer   ~0067176

As it turns out, Pimple has already fixed this issue in release 3.5.0.

The problem is that in Mantis 2.25 we're stuck with Pimple 3.2.3, as we still support PHP 5.

I tried update composer.json to match Pimple's requirements by updating this section (see 0023542 for the reason why it's there):

    "config": {
        "platform": {
            "php": "7.2.5"
        }
    }

Then run composer update -w pimple/pimple, but I still get deprecated warnings, this time on Slim\Collection and unfortunately it seems that Slim will not fix these.

cbspencer

cbspencer

2022-11-30 08:22

reporter   ~0067178

We have a workaround. After reading https://stackoverflow.com/questions/71133749/reference-return-type-of-should-either-be-compatible-with-or-the-re , I updated the offending lines in vendor/pimple/pimple/src/Pimple/Container.php and vendor/slim/slim/Slim/Collection.php to have the line,

#[\ReturnTypeWillChange]

For example:

#[\ReturnTypeWillChange]
public function offsetUnset($id)

We were able to use the API and get JSON data in response but it also introduced a couple of new Deprecated warnings (in HTML):



Deprecated: preg_replace_callback(): Passing null to parameter 0000003 ($subject) of type array|string is deprecated in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/vendor/slim/slim/Slim/Http/Uri.php on line 717


Deprecated: Automatic conversion of false to array is deprecated in /proj/radiosw_webdocs/lmr-radiosw-mantis/root/mantisbt-2.25.5/core/ldap_api.php on line 291

A solution for the "Passing null to parameter" is described in https://stackoverflow.com/questions/71707325/migration-to-php-8-1-how-to-fix-deprecated-passing-null-to-parameter-error-r, and I have added a solution to the Slim/Http/Uri.php file, in filterQuery(). Change:

$query

to:

$query ?? ''

Article https://stackoverflow.com/questions/71035322/php-deprecated-automatic-conversion-of-false-to-array-is-deprecated-adodb-mssql explains how to fix the "Automatic conversion of false to array " error. I have implemented a simple change. For that problem, I changed ldap_cache_user_data() in core/ldap_api.php as follows:

$t_data = false

to:

$t_data = array();

These changes seem to have resolved all of our issues with the REST API. I suspect if you want to support from PHP 5 to PHP 8.1, you should make similar changes to your code.

dregad

dregad

2022-11-30 09:56

developer   ~0067179

Last edited: 2022-11-30 11:54

Thanks for the feedback.

As you probably know, deprecated warnings are pretty harmless, and instead of investing time and effort to maintain a modified version of the code, you can simply decide to squelch them by setting error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT in your php.ini (which is the recommended default value for Production servers).

Automatic conversion of false to array is deprecated in .../mantisbt-2.25.5/core/ldap_api.php on line 291

This one I can fix :-)
See 0031695

if you want to support from PHP 5 to PHP 8.1, you should make similar changes to your code.

The plan is and has been for nearly 2 years now, to drop support for PHP 5 and move on to more modern and supported PHP versions, but MantisBT is a hobby project for me nowadays, and I unfortunately lack the time to invest into it so we're still stuck with ageing dependencies.

I would still be interested to know what was triggering the Uncaught RuntimeException error in your initial report. Knowing the contents of the output buffer would help diagnosing that.

cbspencer

cbspencer

2022-11-30 10:41

reporter   ~0067181

Thanks for the input, and I understand about Mantis being a hobby project. I will contact my webserver team about updating the php.ini file to suppress Deprecated warnings.

The main reason we upgraded from 2.11.1 to 2.25.5 was because our webserver team wanted to upgrade from PHP 7 to PHP 8, since PHP 7 was EOL'ed this month. But 2.11.1 gave us too many issues on PHP 8, so we decided to upgrade for the first time since 2018.

dregad

dregad

2022-11-30 11:55

developer   ~0067182

And you still have not answered my question ;-)

I would still be interested to know what was triggering the Uncaught RuntimeException error in your initial report. Knowing the contents of the output buffer would help diagnosing that.

cbspencer

cbspencer

2022-11-30 13:18

reporter   ~0067184

Sorry about that. I guess I missed the question. :(

The fatal error disappeared after I changed vendor/pimple/pimple/src/Pimple/Container.php and vendor/slim/slim/Slim/Collection.php, adding the "#[\ReturnTypeWillChange]" mentioned above.

I've attached the four files that I changed to resolve all the problems encountered, so you can check for yourself, but diffs against the nightly backup prior to my changes show only the changes that I've recalled making (mentioned above).

I did also make a change (added a line) to config/config_inc.php, but it didn't resolve anything so I reverted the change. To be sure, I restored it from backup and the problems are still all gone.

So, AFAICT, my changes to Container.php and Collection.php resovled the fatal error. I'm sorry that I cannot be more helpful than that.

ldap_api.php (17,577 bytes)   
<?php
# MantisBT - A PHP based bugtracking system

# MantisBT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# MantisBT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.

/**
 * LDAP API
 *
 * @package CoreAPI
 * @subpackage LDAPAPI
 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 *
 * @uses config_api.php
 * @uses constant_inc.php
 * @uses logging_api.php
 * @uses user_api.php
 * @uses utility_api.php
 */

require_api( 'config_api.php' );
require_api( 'constant_inc.php' );
require_api( 'logging_api.php' );
require_api( 'user_api.php' );
require_api( 'utility_api.php' );

/**
 * @var array $g_cache_ldap_data LDAP attributes cache, indexed by username
 */
$g_cache_ldap_data = array();

/**
 * Logs the most recent LDAP error
 * @param resource $p_ds LDAP resource identifier returned by ldap_connect.
 * @return void
 */
function ldap_log_error( $p_ds ) {
	log_event( LOG_LDAP, 'ERROR #' . ldap_errno( $p_ds ) . ': ' . ldap_error( $p_ds ) );
}

/**
 * Connect and bind to the LDAP directory
 * @param string $p_binddn   DN to use for LDAP bind.
 * @param string $p_password Password to use for LDAP bind.
 * @return resource|false
 */
function ldap_connect_bind( $p_binddn = '', $p_password = '' ) {
	if( !extension_loaded( 'ldap' ) ) {
		log_event( LOG_LDAP, 'Error: LDAP extension missing in php' );
		trigger_error( ERROR_LDAP_EXTENSION_NOT_LOADED, ERROR );
	}

	$t_ldap_server = config_get_global( 'ldap_server' );

	log_event( LOG_LDAP, 'Checking syntax of LDAP server URI \'' . $t_ldap_server . '\'.' );
	$t_ds = @ldap_connect( $t_ldap_server );
	if( $t_ds === false ) {
		log_event( LOG_LDAP, 'LDAP server URI syntax check failed, make sure its in URI form' );
		trigger_error( ERROR_LDAP_SERVER_CONNECT_FAILED, ERROR );
		# Return required as function may be called with error suppressed
		return false;
	}

	log_event( LOG_LDAP, 'LDAP server URI syntax check succeeded' );

	$t_network_timeout = config_get_global( 'ldap_network_timeout' );
	if( $t_network_timeout > 0 ) {
		log_event( LOG_LDAP, "Setting LDAP network timeout to " . $t_network_timeout );
		$t_result = @ldap_set_option( $t_ds, LDAP_OPT_NETWORK_TIMEOUT, $t_network_timeout );
		if( !$t_result ) {
			ldap_log_error( $t_ds );
		}
	}

	$t_protocol_version = config_get_global( 'ldap_protocol_version' );
	if( $t_protocol_version > 0 ) {
		log_event( LOG_LDAP, 'Setting LDAP protocol version to ' . $t_protocol_version );
		$t_result = @ldap_set_option( $t_ds, LDAP_OPT_PROTOCOL_VERSION, $t_protocol_version );
		if( !$t_result ) {
			ldap_log_error( $t_ds );
		}
	}

	# Set referrals flag.
	$t_follow_referrals = ON == config_get_global( 'ldap_follow_referrals' );
	$t_result = @ldap_set_option( $t_ds, LDAP_OPT_REFERRALS, $t_follow_referrals );
	if( !$t_result ) {
		ldap_log_error( $t_ds );
	}

	# Set minimum TLS protocol version flag (ex: LDAP_OPT_X_TLS_PROTOCOL_TLS1_2).
	if( version_compare( PHP_VERSION, '7.1.0', '>=' ) ) {
		$t_tls_protocol_min = config_get_global( 'ldap_tls_protocol_min' );
		if( $t_tls_protocol_min > 0 ) {
			log_event( LOG_LDAP, 'Attempting to set minimum TLS protocol' );
			$t_result = @ldap_set_option( $t_ds, LDAP_OPT_X_TLS_PROTOCOL_MIN, $t_tls_protocol_min );
			if( !$t_result ) {
				ldap_log_error( $t_ds );
				log_event( LOG_LDAP, "Error: Failed to set minimum TLS version on LDAP server" );
				trigger_error( ERROR_LDAP_UNABLE_TO_SET_MIN_TLS, ERROR );

				# Return required as function may be called with error suppressed
				return false;
			}
		}
	}

	$t_use_starttls = config_get_global( 'ldap_use_starttls' );
	if ( $t_use_starttls ) {
		log_event( LOG_LDAP, 'Attempting StartTLS' );
		$t_result = @ldap_start_tls( $t_ds );
		if( !$t_result ) {
			ldap_log_error( $t_ds );
			log_event( LOG_LDAP, "Error: Cannot initiate StartTLS on LDAP server" );
			trigger_error( ERROR_LDAP_UNABLE_TO_STARTTLS, ERROR );

			# Return required as function may be called with error suppressed
			return false;
		}
	}
	
	# If no Bind DN and Password is set, attempt to login as the configured
	# Bind DN.
	if( is_blank( $p_binddn ) && is_blank( $p_password ) ) {
		$p_binddn = config_get_global( 'ldap_bind_dn', '' );
		$p_password = config_get_global( 'ldap_bind_passwd', '' );
	}

	if( !is_blank( $p_binddn ) && !is_blank( $p_password ) ) {
		log_event( LOG_LDAP, "Attempting bind to ldap server as '$p_binddn'" );
		$t_br = @ldap_bind( $t_ds, $p_binddn, $p_password );
	} else {
		# Either the Bind DN or the Password are empty, so attempt an anonymous bind.
		log_event( LOG_LDAP, 'Attempting anonymous bind to ldap server' );
		$t_br = @ldap_bind( $t_ds );
	}

	if( !$t_br ) {
		ldap_log_error( $t_ds );
		log_event( LOG_LDAP, 'Bind to ldap server failed' );
		trigger_error( ERROR_LDAP_SERVER_CONNECT_FAILED, ERROR );
	} else {
		log_event( LOG_LDAP, 'Bind to ldap server successful' );
	}

	return $t_ds;
}

/**
 * returns an email address from LDAP, given a userid
 * @param integer $p_user_id A valid user identifier.
 * @return string
 */
function ldap_email( $p_user_id ) {
	return ldap_email_from_username( user_get_username( $p_user_id ) );
}

/**
 * Return an email address from LDAP, given a username
 * @param string $p_username The username of a user to lookup.
 * @return string
 */
function ldap_email_from_username( $p_username ) {
	if( ldap_simulation_is_enabled() ) {
		$t_email = ldap_simulation_email_from_username( $p_username );
	} else {
		$t_email = (string)ldap_get_field_from_username( $p_username, 'mail' );
	}
	return $t_email;
}

/**
 * Gets a user's real name (common name) given the id.
 *
 * @param integer $p_user_id The user id.
 * @return string real name.
 */
function ldap_realname( $p_user_id ) {
	return ldap_realname_from_username( user_get_username( $p_user_id ) );
}

/**
 * Gets a user real name given their user name.
 * @param string $p_username The user's name.
 * @return string The user's real name.
 */
function ldap_realname_from_username( $p_username ) {
	if( ldap_simulation_is_enabled() ) {
		$t_realname = ldap_simulatiom_realname_from_username( $p_username );
	} else {
		$t_ldap_realname_field = config_get_global( 'ldap_realname_field' );
		$t_realname = (string)ldap_get_field_from_username( $p_username, $t_ldap_realname_field );
	}
	return $t_realname;
}

/**
 * Escapes the LDAP string to disallow injection.
 *
 * @param string $p_string The string to escape.
 * @return string The escaped string.
 */
function ldap_escape_string( $p_string ) {
	$t_find = array( '\\', '*', '(', ')', '/', "\x00" );
	$t_replace = array( '\5c', '\2a', '\28', '\29', '\2f', '\00' );

	$t_string = str_replace( $t_find, $t_replace, $p_string );

	return $t_string;
}

/**
 * Retrieves user data from LDAP and stores it in cache.
 *
 * Uses a single LDAP query to retrieve the following fields:
 * - email (mail)
 * - realname {@see $g_ldap_realname_field}
 *
 * @param string $p_username The username.
 *
 * @return array|false User data, false if not found or errors occurred
 */
function ldap_cache_user_data( $p_username ) {
	global $g_cache_ldap_data;

	# Return cached data if available
	if( isset( $g_cache_ldap_data[$p_username] ) ) {
		return $g_cache_ldap_data[$p_username];
	}

	log_event( LOG_LDAP, "Retrieving data for '$p_username' from LDAP server" );

	# Bind and connect.
	# We suppress errors, because failing to connect is not blocking in this
	# context, it just means we won't be able to retrieve user data from LDAP.
	$t_ds = @ldap_connect_bind();
	if( $t_ds === false ) {
		log_event( LOG_LDAP, "ERROR: could not bind to LDAP server" );
		return false;
	}

	# Search
	$t_ldap_organization = config_get_global( 'ldap_organization' );
	$t_ldap_root_dn      = config_get_global( 'ldap_root_dn' );
	$t_ldap_uid_field    = config_get_global( 'ldap_uid_field' );

	$t_search_filter = '(&' . $t_ldap_organization
		. '(' . $t_ldap_uid_field . '=' . ldap_escape_string( $p_username ) . '))';
	$t_search_attrs = array(
		'mail',
		config_get_global( 'ldap_realname_field' )
	);

	log_event( LOG_LDAP, 'Searching for ' . $t_search_filter );
	$t_sr = @ldap_search( $t_ds, $t_ldap_root_dn, $t_search_filter, $t_search_attrs );
	if( $t_sr === false ) {
		ldap_log_error( $t_ds );
		ldap_unbind( $t_ds );
		log_event( LOG_LDAP, "Search '$t_search_filter' failed" );
		return false;
	}

	# Get results
	$t_entry = ldap_first_entry( $t_ds, $t_sr );
	if( $t_entry === false ) {
		log_event( LOG_LDAP, 'No matches found.' );
		$g_cache_ldap_data[$p_username] = false;
		return false;
	}

	# $t_data = false;
        $t_data = array();
	foreach( $t_search_attrs as $t_attr ) {
		# Suppress error to avoid Warning in case an invalid attribute was specified
		$t_value = @ldap_get_values( $t_ds, $t_entry, $t_attr );
		if( $t_value === false ) {
			log_event( LOG_LDAP, "WARNING: field '$t_attr' does not exist" );
			continue;
		}
		$t_data[$t_attr] = $t_value[0];
	}

	# Store data in the cache
	$g_cache_ldap_data[$p_username] = $t_data;

	# Unbind
	log_event( LOG_LDAP, 'Unbinding from LDAP server' );
	ldap_unbind( $t_ds );

	return $t_data;
}

/**
 * Gets the value of a specific LDAP field given the user name.
 *
 * Values are retrieved from the LDAP cache.
 * {@see ldap_cache_user_data()} for the list of valid field names.
 *
 * @param string $p_username The user name.
 * @param string $p_field    The LDAP field name.
 *
 * @return string The field value or null if not found.
 */
function ldap_get_field_from_username( $p_username, $p_field ) {
	log_event( LOG_LDAP, "Retrieving field '$p_field' for '$p_username'" );
	$t_ldap_data = ldap_cache_user_data( $p_username );

	# Make sure LDAP data is available and the requested field exists
	if( !$t_ldap_data || !isset( $t_ldap_data[$p_field] ) ) {
		return null;
	}

	return $t_ldap_data[$p_field];
}

/**
 * Attempt to authenticate the user against the LDAP directory
 * return true on successful authentication, false otherwise
 * @param integer $p_user_id  A valid user identifier.
 * @param string  $p_password A password to test against the user user.
 * @return boolean
 */
function ldap_authenticate( $p_user_id, $p_password ) {
	# if password is empty and ldap allows anonymous login, then
	# the user will be able to login, hence, we need to check
	# for this special case.
	if( is_blank( $p_password ) ) {
		return false;
	}

	$t_username = user_get_username( $p_user_id );

	return ldap_authenticate_by_username( $t_username, $p_password );
}

/**
 * Authenticates a user via LDAP given the username and password.
 *
 * @param string $p_username The user name.
 * @param string $p_password The password.
 * @return true: authenticated, false: failed to authenticate.
 */
function ldap_authenticate_by_username( $p_username, $p_password ) {
	if( ldap_simulation_is_enabled() ) {
		log_event( LOG_LDAP, 'Authenticating via LDAP simulation' );
		$t_authenticated = ldap_simulation_authenticate_by_username( $p_username, $p_password );
	} else {
		$c_username = ldap_escape_string( $p_username );

		$t_ldap_organization = config_get_global( 'ldap_organization' );
		$t_ldap_root_dn = config_get_global( 'ldap_root_dn' );

		$t_ldap_uid_field = config_get_global( 'ldap_uid_field', 'uid' );
		$t_search_filter = '(&' . $t_ldap_organization . '(' . $t_ldap_uid_field . '=' . $c_username . '))';
		$t_search_attrs = array(
			$t_ldap_uid_field,
			'dn',
		);

		# Bind and connect.
		# No need to check for failures, as ldap_connect_bind() throws errors.
		log_event( LOG_LDAP, 'Binding to LDAP server' );
		$t_ds = ldap_connect_bind();

		# Search for the user id
		log_event( LOG_LDAP, 'Searching for ' . $t_search_filter );
		$t_sr = ldap_search( $t_ds, $t_ldap_root_dn, $t_search_filter, $t_search_attrs );
		if( $t_sr === false ) {
			ldap_log_error( $t_ds );
			ldap_unbind( $t_ds );
			log_event( LOG_LDAP, "Search '$t_search_filter' failed" );
			trigger_error( ERROR_LDAP_AUTH_FAILED, ERROR );
		}

		$t_info = @ldap_get_entries( $t_ds, $t_sr );
		if( $t_info === false ) {
			ldap_log_error( $t_ds );
			ldap_unbind( $t_ds );
			trigger_error( ERROR_LDAP_AUTH_FAILED, ERROR );
		}

		$t_authenticated = false;

		if( $t_info['count'] > 0 ) {
			# Try to authenticate to each until we get a match
			for( $i = 0; $i < $t_info['count']; $i++ ) {
				$t_dn = $t_info[$i]['dn'];
				log_event( LOG_LDAP, 'Checking ' . $t_info[$i]['dn'] );

				# Attempt to bind with the DN and password
				if( @ldap_bind( $t_ds, $t_dn, $p_password ) ) {
					$t_authenticated = true;
					break;
				}
			}
		} else {
			log_event( LOG_LDAP, 'No matching entries found' );
		}

		log_event( LOG_LDAP, 'Unbinding from LDAP server' );
		ldap_unbind( $t_ds );
	}

	# If user authenticated successfully then update the local DB with information
	# from LDAP.  This will allow us to use the local data after login without
	# having to go back to LDAP.  This will also allow fallback to DB if LDAP is down.
	if( $t_authenticated ) {
		$t_user_id = user_get_id_by_name( $p_username );

		if( false !== $t_user_id ) {

			$t_fields_to_update = array('password' => md5( $p_password ));

			if( ON == config_get_global( 'use_ldap_realname' ) ) {
				$t_fields_to_update['realname'] = ldap_realname_from_username( $p_username );
			}

			if( ON == config_get_global( 'use_ldap_email' ) ) {
				$t_fields_to_update['email'] = ldap_email_from_username( $p_username );
			}

			user_set_fields( $t_user_id, $t_fields_to_update );
		}
		log_event( LOG_LDAP, 'User \'' . $p_username . '\' authenticated' );
	} else {
		log_event( LOG_LDAP, 'Authentication failed' );
	}

	return $t_authenticated;
}

/**
 * Checks if the LDAP simulation mode is enabled.
 *
 * @return boolean true if enabled, false otherwise.
 */
function ldap_simulation_is_enabled() {
	$t_filename = config_get_global( 'ldap_simulation_file_path' );
	return !is_blank( $t_filename );
}

/**
 * Gets a user from LDAP simulation mode given the username.
 *
 * @param string $p_username The user name.
 * @return array|null An associate array with user information or null if not found.
 */
function ldap_simulation_get_user( $p_username ) {
	$t_filename = config_get_global( 'ldap_simulation_file_path' );
	$t_lines = file( $t_filename );
	if( $t_lines === false ) {
		log_event( LOG_LDAP, 'ldap_simulation_get_user: could not read simulation data from ' . $t_filename );
		trigger_error( ERROR_LDAP_SERVER_CONNECT_FAILED, ERROR );
	}

	foreach ( $t_lines as $t_line ) {
		$t_line = trim( $t_line, " \t\r\n" );
		$t_row = explode( ',', $t_line );

		if( $t_row[0] != $p_username ) {
			continue;
		}

		$t_user = array();

		$t_user['username'] = $t_row[0];
		$t_user['realname'] = $t_row[1];
		$t_user['email'] = $t_row[2];
		$t_user['password'] = $t_row[3];

		return $t_user;
	}

	log_event( LOG_LDAP, 'ldap_simulation_get_user: user \'' . $p_username . '\' not found.' );
	return null;
}

/**
 * Given a username, gets the email address or empty address if user is not found.
 *
 * @param string $p_username The user name.
 * @return string The email address or blank if user is not found.
 */
function ldap_simulation_email_from_username( $p_username ) {
	$t_user = ldap_simulation_get_user( $p_username );
	if( $t_user === null ) {
		log_event( LOG_LDAP, 'ldap_simulation_email_from_username: user \'' . $p_username . '\' not found.' );
		return '';
	}

	log_event( LOG_LDAP, 'ldap_simulation_email_from_username: user \'' . $p_username . '\' has email \'' . $t_user['email'] .'\'.' );
	return $t_user['email'];
}

/**
 * Given a username, this methods gets the realname or empty string if not found.
 *
 * @param string $p_username The username.
 * @return string The real name or an empty string if not found.
 */
function ldap_simulatiom_realname_from_username( $p_username ) {
	$t_user = ldap_simulation_get_user( $p_username );
	if( $t_user === null ) {
		log_event( LOG_LDAP, 'ldap_simulatiom_realname_from_username: user \'' . $p_username . '\' not found.' );
		return '';
	}

	log_event( LOG_LDAP, 'ldap_simulatiom_realname_from_username: user \'' . $p_username . '\' has real name \'' . $t_user['realname'] . '\'.' );
	return $t_user['realname'];
}

/**
 * Authenticates the specified user id / password based on the simulation data.
 *
 * @param string $p_username The username.
 * @param string $p_password The password.
 * @return boolean true for authenticated, false otherwise.
 */
function ldap_simulation_authenticate_by_username( $p_username, $p_password ) {
	$c_username = ldap_escape_string( $p_username );

	$t_user = ldap_simulation_get_user( $c_username );
	if( $t_user === null ) {
		log_event( LOG_LDAP, 'ldap_simulation_authenticate: user \'' . $p_username . '\' not found.' );
		return false;
	}

	if( $t_user['password'] != $p_password ) {
		log_event( LOG_LDAP, 'ldap_simulation_authenticate: expected password \'' . $t_user['password'] . '\' and got \'' . $p_password . '\'.' );
		return false;
	}

	log_event( LOG_LDAP, 'ldap_simulation_authenticate: authentication successful for user \'' . $p_username . '\'.' );
	return true;
}
ldap_api.php (17,577 bytes)   
Uri.php (26,143 bytes)   
<?php
/**
 * Slim Framework (https://slimframework.com)
 *
 * @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License)
 */

namespace Slim\Http;

use InvalidArgumentException;
use Psr\Http\Message\UriInterface;

/**
 * Value object representing a URI.
 *
 * This interface is meant to represent URIs according to RFC 3986 and to
 * provide methods for most common operations. Additional functionality for
 * working with URIs can be provided on top of the interface or externally.
 * Its primary use is for HTTP requests, but may also be used in other
 * contexts.
 *
 * Instances of this interface are considered immutable; all methods that
 * might change state MUST be implemented such that they retain the internal
 * state of the current instance and return an instance that contains the
 * changed state.
 *
 * Typically the Host header will be also be present in the request message.
 * For server-side requests, the scheme will typically be discoverable in the
 * server parameters.
 *
 * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
 */
class Uri implements UriInterface
{
    /**
     * Uri scheme (without "://" suffix)
     *
     * @var string
     */
    protected $scheme = '';

    /**
     * Uri user
     *
     * @var string
     */
    protected $user = '';

    /**
     * Uri password
     *
     * @var string
     */
    protected $password = '';

    /**
     * Uri host
     *
     * @var string
     */
    protected $host = '';

    /**
     * Uri port number
     *
     * @var null|int
     */
    protected $port;

    /**
     * Uri base path
     *
     * @var string
     */
    protected $basePath = '';

    /**
     * Uri path
     *
     * @var string
     */
    protected $path = '';

    /**
     * Uri query string (without "?" prefix)
     *
     * @var string
     */
    protected $query = '';

    /**
     * Uri fragment string (without "#" prefix)
     *
     * @var string
     */
    protected $fragment = '';

    /**
     * @param string $scheme   Uri scheme.
     * @param string $host     Uri host.
     * @param int    $port     Uri port number.
     * @param string $path     Uri path.
     * @param string $query    Uri query string.
     * @param string $fragment Uri fragment.
     * @param string $user     Uri user.
     * @param string $password Uri password.
     */
    public function __construct(
        $scheme,
        $host,
        $port = null,
        $path = '/',
        $query = '',
        $fragment = '',
        $user = '',
        $password = ''
    ) {
        $this->scheme = $this->filterScheme($scheme);
        $this->host = $host;
        $this->port = $this->filterPort($port);
        $this->path = ($path === null || !strlen($path)) ? '/' : $this->filterPath($path);
        $this->query = $this->filterQuery($query);
        $this->fragment = $this->filterQuery($fragment);
        $this->user = $user;
        $this->password = $password;
    }

    /**
     * Create new Uri from string.
     *
     * @param  string $uri Complete Uri string (i.e., https://user:pass@host:443/path?query).
     *
     * @return self
     */
    public static function createFromString($uri)
    {
        if (!is_string($uri) && !method_exists($uri, '__toString')) {
            throw new InvalidArgumentException('Uri must be a string');
        }

        $parts = parse_url($uri);
        $scheme = isset($parts['scheme']) ? $parts['scheme'] : '';
        $user = isset($parts['user']) ? $parts['user'] : '';
        $pass = isset($parts['pass']) ? $parts['pass'] : '';
        $host = isset($parts['host']) ? $parts['host'] : '';
        $port = isset($parts['port']) ? $parts['port'] : null;
        $path = isset($parts['path']) ? $parts['path'] : '';
        $query = isset($parts['query']) ? $parts['query'] : '';
        $fragment = isset($parts['fragment']) ? $parts['fragment'] : '';

        return new static($scheme, $host, $port, $path, $query, $fragment, $user, $pass);
    }

    /**
     * Create new Uri from environment.
     *
     * @param Environment $env
     *
     * @return self
     */
    public static function createFromEnvironment(Environment $env)
    {
        // Scheme
        $isSecure = $env->get('HTTPS');
        $scheme = (empty($isSecure) || $isSecure === 'off') ? 'http' : 'https';

        // Authority: Username and password
        $username = $env->get('PHP_AUTH_USER', '');
        $password = $env->get('PHP_AUTH_PW', '');

        // Authority: Host and Port
        if ($env->has('HTTP_HOST')) {
            $host = $env->get('HTTP_HOST');
            // set a port default
            $port = null;
        } else {
            $host = $env->get('SERVER_NAME');
            // set a port default
            $port = (int)$env->get('SERVER_PORT', 80);
        }

        if (preg_match('/^(\[[a-fA-F0-9:.]+\])(:\d+)?\z/', $host, $matches)) {
            $host = $matches[1];

            if (isset($matches[2])) {
                $port = (int) substr($matches[2], 1);
            }
        } else {
            $pos = strpos($host, ':');
            if ($pos !== false) {
                $port = (int) substr($host, $pos + 1);
                $host = strstr($host, ':', true);
            }
        }

        // Path
        $requestScriptName = (string) parse_url($env->get('SCRIPT_NAME'), PHP_URL_PATH);
        $requestScriptDir = dirname($requestScriptName);

        // parse_url() requires a full URL. As we don't extract the domain name or scheme,
        // we use a stand-in.
        $requestUri = (string) parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_PATH);

        $basePath = '';
        $virtualPath = $requestUri;
        if (stripos($requestUri, $requestScriptName) === 0) {
            $basePath = $requestScriptName;
        } elseif ($requestScriptDir !== '/' && stripos($requestUri, $requestScriptDir) === 0) {
            $basePath = $requestScriptDir;
        }

        if ($basePath) {
            $virtualPath = ltrim(substr($requestUri, strlen($basePath)), '/');
        }

        // Query string
        $queryString = $env->get('QUERY_STRING', '');
        if ($queryString === '') {
            $queryString = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_QUERY);
        }

        // Fragment
        $fragment = '';

        // Build Uri
        $uri = new static($scheme, $host, $port, $virtualPath, $queryString, $fragment, $username, $password);
        if ($basePath) {
            $uri = $uri->withBasePath($basePath);
        }

        return $uri;
    }

    /**
     * Retrieve the scheme component of the URI.
     *
     * If no scheme is present, this method MUST return an empty string.
     *
     * The value returned MUST be normalized to lowercase, per RFC 3986
     * Section 3.1.
     *
     * The trailing ":" character is not part of the scheme and MUST NOT be
     * added.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
     *
     * @return string The URI scheme.
     */
    public function getScheme()
    {
        return $this->scheme;
    }

    /**
     * Return an instance with the specified scheme.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified scheme.
     *
     * Implementations MUST support the schemes "http" and "https" case
     * insensitively, and MAY accommodate other schemes if required.
     *
     * An empty scheme is equivalent to removing the scheme.
     *
     * @param string $scheme The scheme to use with the new instance.
     *
     * @return self A new instance with the specified scheme.
     *
     * @throws InvalidArgumentException for invalid or unsupported schemes.
     */
    public function withScheme($scheme)
    {
        $scheme = $this->filterScheme($scheme);
        $clone = clone $this;
        $clone->scheme = $scheme;

        return $clone;
    }

    /**
     * Filter Uri scheme.
     *
     * @param  string $scheme Raw Uri scheme.
     * @return string
     *
     * @throws InvalidArgumentException If the Uri scheme is not a string.
     * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http".
     */
    protected function filterScheme($scheme)
    {
        static $valid = [
            '' => true,
            'https' => true,
            'http' => true,
        ];

        if (!is_string($scheme) && !method_exists($scheme, '__toString')) {
            throw new InvalidArgumentException('Uri scheme must be a string');
        }

        $scheme = str_replace('://', '', strtolower((string)$scheme));
        if (!isset($valid[$scheme])) {
            throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"');
        }

        return $scheme;
    }

    /**
     * Retrieve the authority component of the URI.
     *
     * If no authority information is present, this method MUST return an empty
     * string.
     *
     * The authority syntax of the URI is:
     *
     * <pre>
     * [user-info@]host[:port]
     * </pre>
     *
     * If the port component is not set or is the standard port for the current
     * scheme, it SHOULD NOT be included.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
     *
     * @return string The URI authority, in "[user-info@]host[:port]" format.
     */
    public function getAuthority()
    {
        $userInfo = $this->getUserInfo();
        $host = $this->getHost();
        $port = $this->getPort();

        return ($userInfo !== '' ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : '');
    }

    /**
     * Retrieve the user information component of the URI.
     *
     * If no user information is present, this method MUST return an empty
     * string.
     *
     * If a user is present in the URI, this will return that value;
     * additionally, if the password is also present, it will be appended to the
     * user value, with a colon (":") separating the values.
     *
     * The trailing "@" character is not part of the user information and MUST
     * NOT be added.
     *
     * @return string The URI user information, in "username[:password]" format.
     */
    public function getUserInfo()
    {
        return $this->user . ($this->password !== '' ? ':' . $this->password : '');
    }

    /**
     * Return an instance with the specified user information.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified user information.
     *
     * Password is optional, but the user information MUST include the
     * user; an empty string for the user is equivalent to removing user
     * information.
     *
     * @param string $user The user name to use for authority.
     * @param null|string $password The password associated with $user.
     *
     * @return self A new instance with the specified user information.
     */
    public function withUserInfo($user, $password = null)
    {
        $clone = clone $this;
        $clone->user = $this->filterUserInfo($user);
        if ('' !== $clone->user) {
            $clone->password = !in_array($password, [null, ''], true) ? $this->filterUserInfo($password) : '';
        } else {
            $clone->password = '';
        }

        return $clone;
    }

    /**
     * Filters the user info string.
     *
     * @param string $query The raw uri query string.
     *
     * @return string The percent-encoded query string.
     */
    protected function filterUserInfo($query)
    {
        return preg_replace_callback(
            '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=]+|%(?![A-Fa-f0-9]{2}))/u',
            function ($match) {
                return rawurlencode($match[0]);
            },
            $query
        );
    }

    /**
     * Retrieve the host component of the URI.
     *
     * If no host is present, this method MUST return an empty string.
     *
     * The value returned MUST be normalized to lowercase, per RFC 3986
     * Section 3.2.2.
     *
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
     *
     * @return string The URI host.
     */
    public function getHost()
    {
        return $this->host;
    }

    /**
     * Return an instance with the specified host.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified host.
     *
     * An empty host value is equivalent to removing the host.
     *
     * @param string $host The hostname to use with the new instance.
     *
     * @return self A new instance with the specified host.
     */
    public function withHost($host)
    {
        $clone = clone $this;
        $clone->host = $host;

        return $clone;
    }

    /**
     * Retrieve the port component of the URI.
     *
     * If a port is present, and it is non-standard for the current scheme,
     * this method MUST return it as an integer. If the port is the standard port
     * used with the current scheme, this method SHOULD return null.
     *
     * If no port is present, and no scheme is present, this method MUST return
     * a null value.
     *
     * If no port is present, but a scheme is present, this method MAY return
     * the standard port for that scheme, but SHOULD return null.
     *
     * @return null|int The URI port.
     */
    public function getPort()
    {
        return $this->port && !$this->hasStandardPort() ? $this->port : null;
    }

    /**
     * Return an instance with the specified port.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified port.
     *
     * Implementations MUST raise an exception for ports outside the
     * established TCP and UDP port ranges.
     *
     * A null value provided for the port is equivalent to removing the port
     * information.
     *
     * @param null|int $port The port to use with the new instance; a null value
     *     removes the port information.
     *
     * @return self A new instance with the specified port.
     */
    public function withPort($port)
    {
        $port = $this->filterPort($port);
        $clone = clone $this;
        $clone->port = $port;

        return $clone;
    }

    /**
     * Does this Uri use a standard port?
     *
     * @return bool
     */
    protected function hasStandardPort()
    {
        return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443);
    }

    /**
     * Filter Uri port.
     *
     * @param  null|int $port The Uri port number.
     * @return null|int
     *
     * @throws InvalidArgumentException If the port is invalid.
     */
    protected function filterPort($port)
    {
        if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) {
            return $port;
        }

        throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)');
    }

    /**
     * Retrieve the path component of the URI.
     *
     * The path can either be empty or absolute (starting with a slash) or
     * rootless (not starting with a slash). Implementations MUST support all
     * three syntaxes.
     *
     * Normally, the empty path "" and absolute path "/" are considered equal as
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
     * do this normalization because in contexts with a trimmed base path, e.g.
     * the front controller, this difference becomes significant. It's the task
     * of the user to handle both "" and "/".
     *
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
     * any characters. To determine what characters to encode, please refer to
     * RFC 3986, Sections 2 and 3.3.
     *
     * As an example, if the value should include a slash ("/") not intended as
     * delimiter between path segments, that value MUST be passed in encoded
     * form (e.g., "%2F") to the instance.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
     *
     * @return string The URI path.
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Return an instance with the specified path.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified path.
     *
     * The path can either be empty or absolute (starting with a slash) or
     * rootless (not starting with a slash). Implementations MUST support all three syntaxes.
     *
     * If the path is intended to be domain-relative rather than path relative then
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
     * are assumed to be relative to some base path known to the application or
     * consumer.
     *
     * Users can provide both encoded and decoded path characters.
     * Implementations ensure the correct encoding as outlined in getPath().
     *
     * @param string $path The path to use with the new instance.
     *
     * @return static A new instance with the specified path.
     *
     * @throws InvalidArgumentException For invalid paths.
     */
    public function withPath($path)
    {
        if (!is_string($path)) {
            throw new InvalidArgumentException('Uri path must be a string');
        }

        $clone = clone $this;
        $clone->path = $this->filterPath($path);

        // if the path is absolute, then clear basePath
        if (substr($path, 0, 1) == '/') {
            $clone->basePath = '';
        }

        return $clone;
    }

    /**
     * Retrieve the base path segment of the URI.
     *
     * Note: This method is not part of the PSR-7 standard.
     *
     * This method MUST return a string; if no path is present it MUST return
     * an empty string.
     *
     * @return string The base path segment of the URI.
     */
    public function getBasePath()
    {
        return $this->basePath;
    }

    /**
     * Set base path.
     *
     * Note: This method is not part of the PSR-7 standard.
     *
     * @param  string $basePath
     *
     * @return static
     */
    public function withBasePath($basePath)
    {
        if (!is_string($basePath)) {
            throw new InvalidArgumentException('Uri path must be a string');
        }
        if (!empty($basePath)) {
            $basePath = '/' . trim($basePath, '/'); // <-- Trim on both sides
        }
        $clone = clone $this;

        if ($basePath !== '/') {
            $clone->basePath = $this->filterPath($basePath);
        }

        return $clone;
    }

    /**
     * Filter Uri path.
     *
     * Returns a RFC 3986 percent-encoded uri path.
     *
     * This method percent-encodes all reserved
     * characters in the provided path string. This method
     * will NOT double-encode characters that are already
     * percent-encoded.
     *
     * @param  string $path The raw uri path.
     *
     * @return string
     *
     * @link   http://www.faqs.org/rfcs/rfc3986.html
     */
    protected function filterPath($path)
    {
        return preg_replace_callback(
            '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
            function ($match) {
                return rawurlencode($match[0]);
            },
            $path
        );
    }

    /**
     * Retrieve the query string of the URI.
     *
     * If no query string is present, this method MUST return an empty string.
     *
     * The leading "?" character is not part of the query and MUST NOT be
     * added.
     *
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
     * any characters. To determine what characters to encode, please refer to
     * RFC 3986, Sections 2 and 3.4.
     *
     * As an example, if a value in a key/value pair of the query string should
     * include an ampersand ("&") not intended as a delimiter between values,
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
     *
     * @return string
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * Return an instance with the specified query string.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified query string.
     *
     * Users can provide both encoded and decoded query characters.
     * Implementations ensure the correct encoding as outlined in getQuery().
     *
     * An empty query string value is equivalent to removing the query string.
     *
     * @param string $query The query string to use with the new instance.
     *
     * @return self A new instance with the specified query string.
     *
     * @throws InvalidArgumentException For invalid query strings.
     */
    public function withQuery($query)
    {
        if (!is_string($query) && !method_exists($query, '__toString')) {
            throw new InvalidArgumentException('Uri query must be a string');
        }
        $query = ltrim((string)$query, '?');
        $clone = clone $this;
        $clone->query = $this->filterQuery($query);

        return $clone;
    }

    /**
     * Filters the query string or fragment of a URI.
     *
     * @param string $query The raw uri query string.
     *
     * @return string The percent-encoded query string.
     */
    protected function filterQuery($query)
    {
        return preg_replace_callback(
            '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
            function ($match) {
                return rawurlencode($match[0]);
            },
            $query ?? ''
        );
    }

    /**
     * Retrieve the fragment component of the URI.
     *
     * If no fragment is present, this method MUST return an empty string.
     *
     * The leading "#" character is not part of the fragment and MUST NOT be
     * added.
     *
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
     * any characters. To determine what characters to encode, please refer to
     * RFC 3986, Sections 2 and 3.5.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
     *
     * @return string The URI fragment.
     */
    public function getFragment()
    {
        return $this->fragment;
    }

    /**
     * Return an instance with the specified URI fragment.
     *
     * This method MUST retain the state of the current instance, and return
     * an instance that contains the specified URI fragment.
     *
     * Users can provide both encoded and decoded fragment characters.
     * Implementations ensure the correct encoding as outlined in getFragment().
     *
     * An empty fragment value is equivalent to removing the fragment.
     *
     * @param string $fragment The fragment to use with the new instance.
     *
     * @return static A new instance with the specified fragment.
     */
    public function withFragment($fragment)
    {
        if (!is_string($fragment) && !method_exists($fragment, '__toString')) {
            throw new InvalidArgumentException('Uri fragment must be a string');
        }
        $fragment = ltrim((string)$fragment, '#');
        $clone = clone $this;
        $clone->fragment = $this->filterQuery($fragment);

        return $clone;
    }

    /**
     * Return the string representation as a URI reference.
     *
     * Depending on which components of the URI are present, the resulting
     * string is either a full URI or relative reference according to RFC 3986,
     * Section 4.1. The method concatenates the various components of the URI,
     * using the appropriate delimiters:
     *
     * - If a scheme is present, it MUST be suffixed by ":".
     * - If an authority is present, it MUST be prefixed by "//".
     * - The path can be concatenated without delimiters. But there are two
     *   cases where the path has to be adjusted to make the URI reference
     *   valid as PHP does not allow to throw an exception in __toString():
     *     - If the path is rootless and an authority is present, the path MUST
     *       be prefixed by "/".
     *     - If the path is starting with more than one "/" and no authority is
     *       present, the starting slashes MUST be reduced to one.
     * - If a query is present, it MUST be prefixed by "?".
     * - If a fragment is present, it MUST be prefixed by "#".
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
     *
     * @return string
     */
    public function __toString()
    {
        $scheme = $this->getScheme();
        $authority = $this->getAuthority();
        $basePath = $this->getBasePath();
        $path = $this->getPath();
        $query = $this->getQuery();
        $fragment = $this->getFragment();

        $path = $basePath . '/' . ltrim($path, '/');

        return ($scheme !== '' ? $scheme . ':' : '')
            . ($authority !== '' ? '//' . $authority : '')
            . $path
            . ($query !== '' ? '?' . $query : '')
            . ($fragment !== '' ? '#' . $fragment : '');
    }

    /**
     * Return the fully qualified base URL.
     *
     * Note that this method never includes a trailing /
     *
     * This method is not part of PSR-7.
     *
     * @return string
     */
    public function getBaseUrl()
    {
        $scheme = $this->getScheme();
        $authority = $this->getAuthority();
        $basePath = $this->getBasePath();

        if ($authority !== '' && substr($basePath, 0, 1) !== '/') {
            $basePath = $basePath . '/' . $basePath;
        }

        return ($scheme !== '' ? $scheme . ':' : '')
            . ($authority ? '//' . $authority : '')
            . rtrim($basePath, '/');
    }
}
Uri.php (26,143 bytes)   
Collection.php (3,239 bytes)   
<?php
/**
 * Slim Framework (https://slimframework.com)
 *
 * @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License)
 */

namespace Slim;

use ArrayIterator;
use Slim\Interfaces\CollectionInterface;

/**
 * Collection
 *
 * This class provides a common interface used by many other
 * classes in a Slim application that manage "collections"
 * of data that must be inspected and/or manipulated
 */
class Collection implements CollectionInterface
{
    /**
     * The source data
     *
     * @var array
     */
    protected $data = [];

    /**
     * @param array $items Pre-populate collection with this key-value array
     */
    public function __construct(array $items = [])
    {
        $this->replace($items);
    }

    /**
     * {@inheritdoc}
     */
    public function set($key, $value)
    {
        $this->data[$key] = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function get($key, $default = null)
    {
        return $this->has($key) ? $this->data[$key] : $default;
    }

    /**
     * {@inheritdoc}
     */
    public function replace(array $items)
    {
        foreach ($items as $key => $value) {
            $this->set($key, $value);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function all()
    {
        return $this->data;
    }

    /**
     * Get collection keys
     *
     * @return array The collection's source data keys
     */
    public function keys()
    {
        return array_keys($this->data);
    }

    /**
     * {@inheritdoc}
     */
    public function has($key)
    {
        return array_key_exists($key, $this->data);
    }

    /**
     * {@inheritdoc}
     */
    public function remove($key)
    {
        unset($this->data[$key]);
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $this->data = [];
    }

    /**
     * Does this collection have a given key?
     *
     * @param  string $key The data key
     *
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($key)
    {
        return $this->has($key);
    }

    /**
     * Get collection item for key
     *
     * @param string $key The data key
     *
     * @return mixed The key's value, or the default value
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($key)
    {
        return $this->get($key);
    }

    /**
     * Set collection item
     *
     * @param string $key   The data key
     * @param mixed  $value The data value
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($key, $value)
    {
        $this->set($key, $value);
    }

    /**
     * Remove item from collection
     *
     * @param string $key The data key
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($key)
    {
        $this->remove($key);
    }

    /**
     * Get number of items in collection
     *
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->data);
    }

    /**
     * Get collection iterator
     *
     * @return ArrayIterator
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        return new ArrayIterator($this->data);
    }
}
Collection.php (3,239 bytes)   
Container.php (9,459 bytes)   
<?php

/*
 * This file is part of Pimple.
 *
 * Copyright (c) 2009 Fabien Potencier
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

namespace Pimple;

use Pimple\Exception\ExpectedInvokableException;
use Pimple\Exception\FrozenServiceException;
use Pimple\Exception\InvalidServiceIdentifierException;
use Pimple\Exception\UnknownIdentifierException;

/**
 * Container main class.
 *
 * @author Fabien Potencier
 */
class Container implements \ArrayAccess
{
    private $values = array();
    private $factories;
    private $protected;
    private $frozen = array();
    private $raw = array();
    private $keys = array();

    /**
     * Instantiates the container.
     *
     * Objects and parameters can be passed as argument to the constructor.
     *
     * @param array $values The parameters or objects
     */
    public function __construct(array $values = array())
    {
        $this->factories = new \SplObjectStorage();
        $this->protected = new \SplObjectStorage();

        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }

    /**
     * Sets a parameter or an object.
     *
     * Objects must be defined as Closures.
     *
     * Allowing any PHP callable leads to difficult to debug problems
     * as function names (strings) are callable (creating a function with
     * the same name as an existing parameter would break your container).
     *
     * @param string $id    The unique identifier for the parameter or object
     * @param mixed  $value The value of the parameter or a closure to define an object
     *
     * @throws FrozenServiceException Prevent override of a frozen service
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($id, $value)
    {
        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }

        $this->values[$id] = $value;
        $this->keys[$id] = true;
    }

    /**
     * Gets a parameter or an object.
     *
     * @param string $id The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or an object
     *
     * @throws UnknownIdentifierException If the identifier is not defined
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($id)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);
        }

        if (
            isset($this->raw[$id])
            || !\is_object($this->values[$id])
            || isset($this->protected[$this->values[$id]])
            || !\method_exists($this->values[$id], '__invoke')
        ) {
            return $this->values[$id];
        }

        if (isset($this->factories[$this->values[$id]])) {
            return $this->values[$id]($this);
        }

        $raw = $this->values[$id];
        $val = $this->values[$id] = $raw($this);
        $this->raw[$id] = $raw;

        $this->frozen[$id] = true;

        return $val;
    }

    /**
     * Checks if a parameter or an object is set.
     *
     * @param string $id The unique identifier for the parameter or object
     *
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($id)
    {
        return isset($this->keys[$id]);
    }

    /**
     * Unsets a parameter or an object.
     *
     * @param string $id The unique identifier for the parameter or object
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($id)
    {
        if (isset($this->keys[$id])) {
            if (\is_object($this->values[$id])) {
                unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]);
            }

            unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]);
        }
    }

    /**
     * Marks a callable as being a factory service.
     *
     * @param callable $callable A service definition to be used as a factory
     *
     * @return callable The passed callable
     *
     * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object
     */
    public function factory($callable)
    {
        if (!\method_exists($callable, '__invoke')) {
            throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.');
        }

        $this->factories->attach($callable);

        return $callable;
    }

    /**
     * Protects a callable from being interpreted as a service.
     *
     * This is useful when you want to store a callable as a parameter.
     *
     * @param callable $callable A callable to protect from being evaluated
     *
     * @return callable The passed callable
     *
     * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object
     */
    public function protect($callable)
    {
        if (!\method_exists($callable, '__invoke')) {
            throw new ExpectedInvokableException('Callable is not a Closure or invokable object.');
        }

        $this->protected->attach($callable);

        return $callable;
    }

    /**
     * Gets a parameter or the closure defining an object.
     *
     * @param string $id The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or the closure defining an object
     *
     * @throws UnknownIdentifierException If the identifier is not defined
     */
    public function raw($id)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);
        }

        if (isset($this->raw[$id])) {
            return $this->raw[$id];
        }

        return $this->values[$id];
    }

    /**
     * Extends an object definition.
     *
     * Useful when you want to extend an existing object definition,
     * without necessarily loading that object.
     *
     * @param string   $id       The unique identifier for the object
     * @param callable $callable A service definition to extend the original
     *
     * @return callable The wrapped callable
     *
     * @throws UnknownIdentifierException        If the identifier is not defined
     * @throws FrozenServiceException            If the service is frozen
     * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter
     * @throws ExpectedInvokableException        If the extension callable is not a closure or an invokable object
     */
    public function extend($id, $callable)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);
        }

        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }

        if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) {
            throw new InvalidServiceIdentifierException($id);
        }

        if (isset($this->protected[$this->values[$id]])) {
            @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), \E_USER_DEPRECATED);
        }

        if (!\is_object($callable) || !\method_exists($callable, '__invoke')) {
            throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.');
        }

        $factory = $this->values[$id];

        $extended = function ($c) use ($callable, $factory) {
            return $callable($factory($c), $c);
        };

        if (isset($this->factories[$factory])) {
            $this->factories->detach($factory);
            $this->factories->attach($extended);
        }

        return $this[$id] = $extended;
    }

    /**
     * Returns all defined value names.
     *
     * @return array An array of value names
     */
    public function keys()
    {
        return \array_keys($this->values);
    }

    /**
     * Registers a service provider.
     *
     * @param ServiceProviderInterface $provider A ServiceProviderInterface instance
     * @param array                    $values   An array of values that customizes the provider
     *
     * @return static
     */
    public function register(ServiceProviderInterface $provider, array $values = array())
    {
        $provider->register($this);

        foreach ($values as $key => $value) {
            $this[$key] = $value;
        }

        return $this;
    }
}
Container.php (9,459 bytes)   
dregad

dregad

2022-12-01 07:44

developer   ~0067185

OK, based on that, I assume that your server's error reporting settings caused REST API calls to generate some output due to the deprecation warnings, triggering the fatal error. That should allow me to reproduce the error.

dregad

dregad

2022-12-01 08:43

developer   ~0067187

That was it. I now consistently get the deprecated warnings and fatal error also, using the following php.ini settings when accessing http://path.to/mantis/api/rest/ on MantisBT 2.25.5

error_reporting = E_ALL
display_errors = On

Considering that display_errors = On is definitely NOT something you would want to do on a production server, we're actually not facing a severe error but a minor issue which can be solved very simply by setting

error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT 
display_errors = Off

With this simple workaround available, I don't think it's worth patching and maintaining a PHP 8.1-compatible version of Slim and Pimple, therefore I will

  • resolve this issue as no change required and
  • open a separate issue to track the required upgrade to Slim Framework 4.x (see 0031699). This is most likely not going to be a small undertaking, and can only happen once we've increased our minimum PHP requirements (the latest Slim version 4.11.0 requires 7.4).
mgmantis

mgmantis

2022-12-01 13:09

reporter   ~0067189

thanks for your help!