php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login

Patch bug60891-v1.patch for FPM related Bug #60891

Patch version 2012-05-28 23:08 UTC

Return to Bug #60891 | Download this patch
Patch Revisions:

Developer: fat@php.net

diff --git a/sapi/fpm/fpm/fpm.c b/sapi/fpm/fpm/fpm.c
index dab415d..34e8f78 100644
--- a/sapi/fpm/fpm/fpm.c
+++ b/sapi/fpm/fpm/fpm.c
@@ -19,6 +19,7 @@
 #include "fpm_conf.h"
 #include "fpm_worker_pool.h"
 #include "fpm_scoreboard.h"
+#include "fpm_status.h"
 #include "fpm_stdio.h"
 #include "fpm_log.h"
 #include "zlog.h"
@@ -40,6 +41,7 @@ struct fpm_globals_s fpm_globals = {
 	.heartbeat = 0,
 	.run_as_root = 0,
 	.send_config_signal = 0,
+	.scoreboards = NULL,
 };
 
 int fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root) /* {{{ */
@@ -57,6 +59,7 @@ int fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int t
 	    0 > fpm_stdio_init_main()         ||
 	    0 > fpm_conf_init_main(test_conf) ||
 	    0 > fpm_unix_init_main()          ||
+	    0 > fpm_status_init_main()        ||
 	    0 > fpm_scoreboard_init_main()    ||
 	    0 > fpm_pctl_init_main()          ||
 	    0 > fpm_env_init_main()           ||
diff --git a/sapi/fpm/fpm/fpm.h b/sapi/fpm/fpm/fpm.h
index 7a2903d..e735254 100644
--- a/sapi/fpm/fpm/fpm.h
+++ b/sapi/fpm/fpm/fpm.h
@@ -56,6 +56,7 @@ struct fpm_globals_s {
 	int heartbeat;
 	int run_as_root;
 	int send_config_signal;
+	struct fpm_scoreboard_list_s *scoreboards;
 };
 
 extern struct fpm_globals_s fpm_globals;
diff --git a/sapi/fpm/fpm/fpm_children.c b/sapi/fpm/fpm/fpm_children.c
index 84a9474..67ba6b8 100644
--- a/sapi/fpm/fpm/fpm_children.c
+++ b/sapi/fpm/fpm/fpm_children.c
@@ -338,13 +338,14 @@ static void fpm_resources_discard(struct fpm_child_s *child) /* {{{ */
 static void fpm_child_resources_use(struct fpm_child_s *child) /* {{{ */
 {
 	struct fpm_worker_pool_s *wp;
+/*
 	for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
 		if (wp == child->wp) {
 			continue;
 		}
 		fpm_scoreboard_free(wp->scoreboard);
 	}
-
+*/
 	fpm_scoreboard_child_use(child->wp->scoreboard, child->scoreboard_i, getpid());
 	fpm_stdio_child_use_pipes(child);
 	fpm_child_free(child);
diff --git a/sapi/fpm/fpm/fpm_conf.c b/sapi/fpm/fpm/fpm_conf.c
index 1f3258f..def6aca 100644
--- a/sapi/fpm/fpm/fpm_conf.c
+++ b/sapi/fpm/fpm/fpm_conf.c
@@ -123,6 +123,7 @@ static struct ini_value_parser_s ini_fpm_pool_options[] = {
 	{ "pm.process_idle_timeout",   &fpm_conf_set_time,        WPO(pm_process_idle_timeout) },
 	{ "pm.max_requests",           &fpm_conf_set_integer,     WPO(pm_max_requests) },
 	{ "pm.status_path",            &fpm_conf_set_string,      WPO(pm_status_path) },
+	{ "pm.status_allow",           &fpm_conf_set_string,      WPO(pm_status_allow) },
 	{ "ping.path",                 &fpm_conf_set_string,      WPO(ping_path) },
 	{ "ping.response",             &fpm_conf_set_string,      WPO(ping_response) },
 	{ "access.log",                &fpm_conf_set_string,      WPO(access_log) },
@@ -614,6 +615,7 @@ int fpm_worker_pool_config_free(struct fpm_worker_pool_config_s *wpc) /* {{{ */
 	free(wpc->listen_mode);
 	free(wpc->listen_allowed_clients);
 	free(wpc->pm_status_path);
+	free(wpc->pm_status_allow);
 	free(wpc->ping_path);
 	free(wpc->ping_response);
 	free(wpc->access_log);
@@ -840,6 +842,10 @@ static int fpm_conf_process_all_pools() /* {{{ */
 					return -1;
 				}
 			}
+
+			/* status allow */
+			if (wp->config->pm_status_allow && *wp->config->pm_status_allow) {
+			}
 		}
 
 		/* ping */
@@ -1550,6 +1556,7 @@ static void fpm_conf_dump() /* {{{ */
 		zlog(ZLOG_NOTICE, "\tpm.process_idle_timeout = %d",    wp->config->pm_process_idle_timeout);
 		zlog(ZLOG_NOTICE, "\tpm.max_requests = %d",            wp->config->pm_max_requests);
 		zlog(ZLOG_NOTICE, "\tpm.status_path = %s",             STR2STR(wp->config->pm_status_path));
+		zlog(ZLOG_NOTICE, "\tpm.status_allow = %s",            STR2STR(wp->config->pm_status_allow));
 		zlog(ZLOG_NOTICE, "\tping.path = %s",                  STR2STR(wp->config->ping_path));
 		zlog(ZLOG_NOTICE, "\tping.response = %s",              STR2STR(wp->config->ping_response));
 		zlog(ZLOG_NOTICE, "\taccess.log = %s",                 STR2STR(wp->config->access_log));
diff --git a/sapi/fpm/fpm/fpm_conf.h b/sapi/fpm/fpm/fpm_conf.h
index f780f03..9423c5e 100644
--- a/sapi/fpm/fpm/fpm_conf.h
+++ b/sapi/fpm/fpm/fpm_conf.h
@@ -67,6 +67,7 @@ struct fpm_worker_pool_config_s {
 	int pm_process_idle_timeout;
 	int pm_max_requests;
 	char *pm_status_path;
+	char *pm_status_allow;
 	char *ping_path;
 	char *ping_response;
 	char *access_log;
diff --git a/sapi/fpm/fpm/fpm_log.c b/sapi/fpm/fpm/fpm_log.c
index 69bd31b..93d0762 100644
--- a/sapi/fpm/fpm/fpm_log.c
+++ b/sapi/fpm/fpm/fpm_log.c
@@ -122,7 +122,7 @@ int fpm_log_write(char *log_format TSRMLS_DC) /* {{{ */
 	now_epoch = time(NULL);
 
 	if (!test) {
-		scoreboard = fpm_scoreboard_get();
+		scoreboard = fpm_scoreboard_get(NULL);
 		if (!scoreboard) {
 			zlog(ZLOG_WARNING, "unable to get scoreboard while preparing the access log");
 			return -1;
diff --git a/sapi/fpm/fpm/fpm_php.c b/sapi/fpm/fpm/fpm_php.c
index 840eec7..a886aa4 100644
--- a/sapi/fpm/fpm/fpm_php.c
+++ b/sapi/fpm/fpm/fpm_php.c
@@ -257,3 +257,40 @@ int fpm_php_limit_extensions(char *path) /* {{{ */
 	return 1; /* extension not found: not allowed  */
 }
 /* }}} */
+
+char* fpm_php_get_string_from_table(char *table, char *key TSRMLS_DC) /* {{{ */
+{
+	zval **data, **tmp;
+	char *string_key;
+	uint string_len;
+	ulong num_key;
+	if (!table || !key) {
+		return NULL;
+	}
+
+	/* inspired from ext/standard/info.c */
+
+	zend_is_auto_global(table, strlen(table) TSRMLS_CC);
+
+	/* find the table and ensure it's an array */
+	if (zend_hash_find(&EG(symbol_table), table, strlen(table) + 1, (void **) &data) == SUCCESS && Z_TYPE_PP(data) == IS_ARRAY) {
+
+		/* reset the internal pointer */
+		zend_hash_internal_pointer_reset(Z_ARRVAL_PP(data));
+
+		/* parse the array to look for our key */
+		while (zend_hash_get_current_data(Z_ARRVAL_PP(data), (void **) &tmp) == SUCCESS) {
+			/* ensure the key is a string */
+			if (zend_hash_get_current_key_ex(Z_ARRVAL_PP(data), &string_key, &string_len, &num_key, 0, NULL) == HASH_KEY_IS_STRING) {
+				/* compare to our key */
+				if (!strncmp(string_key, key, string_len)) {
+					return Z_STRVAL_PP(tmp);
+				}
+			}
+			zend_hash_move_forward(Z_ARRVAL_PP(data));
+		}
+	}
+
+	return NULL;
+}
+/* }}} */
diff --git a/sapi/fpm/fpm/fpm_php.h b/sapi/fpm/fpm/fpm_php.h
index a2c7ed3..d605473 100644
--- a/sapi/fpm/fpm/fpm_php.h
+++ b/sapi/fpm/fpm/fpm_php.h
@@ -44,6 +44,7 @@ void fpm_php_soft_quit();
 int fpm_php_init_main();
 int fpm_php_apply_defines_ex(struct key_value_s *kv, int mode);
 int fpm_php_limit_extensions(char *path);
+char* fpm_php_get_string_from_table(char *table, char *key TSRMLS_DC);
 
 #endif
 
diff --git a/sapi/fpm/fpm/fpm_scoreboard.c b/sapi/fpm/fpm/fpm_scoreboard.c
index 4222f60..d44dc9b 100644
--- a/sapi/fpm/fpm/fpm_scoreboard.c
+++ b/sapi/fpm/fpm/fpm_scoreboard.c
@@ -7,6 +7,7 @@
 #include <stdio.h>
 #include <time.h>
 
+#include "fpm.h"
 #include "fpm_config.h"
 #include "fpm_scoreboard.h"
 #include "fpm_shm.h"
@@ -26,6 +27,7 @@ int fpm_scoreboard_init_main() /* {{{ */
 {
 	struct fpm_worker_pool_s *wp;
 	int i;
+	struct fpm_scoreboard_list_s *sb1, *sb2;
 
 #ifdef HAVE_TIMES
 #if (defined(HAVE_SYSCONF) && defined(_SC_CLK_TCK))
@@ -68,6 +70,29 @@ int fpm_scoreboard_init_main() /* {{{ */
 		wp->scoreboard->pm = wp->config->pm;
 		wp->scoreboard->start_epoch = time(NULL);
 		strlcpy(wp->scoreboard->pool, wp->config->name, sizeof(wp->scoreboard->pool));
+
+		/* add the scoreboard to the global SHM list */
+		sb1 = malloc(sizeof(struct fpm_scoreboard_list_s));
+		if (!sb1) {
+			zlog(ZLOG_SYSERROR, "[pool %s] Unable to malloc scoreboard list", wp->config->name);
+			return -1;
+		}
+		/* save the SHM addr */
+		sb1->scoreboard = wp->scoreboard;
+		/* mark the end of the list */
+		sb1->next = NULL;
+
+		/* find the last element, and add this scoreboard at the end */
+		if (!fpm_globals.scoreboards) {
+			fpm_globals.scoreboards = sb1;
+		} else {
+			sb2 = fpm_globals.scoreboards;
+			while (sb2->next) {
+				sb2 = sb2->next;
+			}
+			sb2->next = sb1;
+		}
+		zlog(ZLOG_DEBUG, "Adding pool %s to scoreboards", sb1->scoreboard->pool);
 	}
 	return 0;	
 }
@@ -144,9 +169,25 @@ void fpm_scoreboard_update(int idle, int active, int lq, int lq_len, int request
 }
 /* }}} */
 
-struct fpm_scoreboard_s *fpm_scoreboard_get() /* {{{*/
+struct fpm_scoreboard_s *fpm_scoreboard_get(char *pool TSRMLS_DC) /* {{{*/
 {
-	return fpm_scoreboard;
+	struct fpm_scoreboard_list_s *sb;
+	if (!pool) {
+		return fpm_scoreboard;
+	}
+
+	for (sb=fpm_globals.scoreboards; sb; sb=sb->next) {
+		if (!sb || !sb->scoreboard || !sb->scoreboard->pool) {
+			continue;
+		}
+
+		if (strcmp(pool, sb->scoreboard->pool) == 0) {
+			return sb->scoreboard;
+		}
+	}
+
+	zlog(ZLOG_WARNING, "Unable to find scoreboard for pool %s", pool);
+	return NULL;
 }
 /* }}} */
 
diff --git a/sapi/fpm/fpm/fpm_scoreboard.h b/sapi/fpm/fpm/fpm_scoreboard.h
index 136ea48..9b9cab7 100644
--- a/sapi/fpm/fpm/fpm_scoreboard.h
+++ b/sapi/fpm/fpm/fpm_scoreboard.h
@@ -67,11 +67,16 @@ struct fpm_scoreboard_s {
 	struct fpm_scoreboard_proc_s *procs[];
 };
 
+struct fpm_scoreboard_list_s {
+	struct fpm_scoreboard_s *scoreboard;
+	struct fpm_scoreboard_list_s *next;
+};
+
 int fpm_scoreboard_init_main();
 int fpm_scoreboard_init_child(struct fpm_worker_pool_s *wp);
 
 void fpm_scoreboard_update(int idle, int active, int lq, int lq_len, int requests, int max_children_reached, int action, struct fpm_scoreboard_s *scoreboard);
-struct fpm_scoreboard_s *fpm_scoreboard_get();
+struct fpm_scoreboard_s *fpm_scoreboard_get(char *pool);
 struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get(struct fpm_scoreboard_s *scoreboard, int child_index);
 
 struct fpm_scoreboard_s *fpm_scoreboard_acquire(struct fpm_scoreboard_s *scoreboard, int nohang);
diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c
index e64a645..c8d8e88 100644
--- a/sapi/fpm/fpm/fpm_status.c
+++ b/sapi/fpm/fpm/fpm_status.c
@@ -14,22 +14,137 @@
 #include "zlog.h"
 #include "fpm_atomic.h"
 #include "fpm_conf.h"
+#include "fpm_php.h"
 #include <ext/standard/html.h>
 
 static char *fpm_status_uri = NULL;
 static char *fpm_status_ping_uri = NULL;
 static char *fpm_status_ping_response = NULL;
+static int fpm_status_allow_nb = 0;
+static char **fpm_status_allow_pools;
+static char *fpm_status_pool_name = NULL;
 
+int fpm_status_init_main() /* {{{*/
+{
+	struct fpm_worker_pool_s *wp, *wp2;
+
+	/* loop on each pool configuration */
+	for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
+
+		/* if the status path is not set, skip */
+		if (!wp->config->pm_status_path || !*wp->config->pm_status_path) {
+			continue;
+		}
+
+		if (wp->config->pm_status_allow && *wp->config->pm_status_allow) {
+			char *orig, *orig2, *s;
+			int i;
+			char *saveptr = NULL;
+
+			wp->pm_status_allow_nb = 0;
+
+			/* dup the original string as strtok alterates it */
+			if (!(orig = strdup(wp->config->pm_status_allow))) {
+				return -1;
+			}
+			orig2 = orig;
+
+			while ((s = strtok_r(orig2, ",", &saveptr))) {
+				/* reset strtok ptr to avoid loop */
+				orig2 = NULL;
+
+				/* if '*', then all are allowed */
+				if (strncmp(s, "*", 1) == 0) {
+					wp->pm_status_allow_nb = -1; /* -1 means all */
+					break;
+				}
+
+				if (strlen(s) <= 0) {
+					zlog(ZLOG_ERROR, "[pool %s] pm.status_allow can't have empty elements", wp->config->name);
+					free(orig);
+					return -1;
+				}
+				/* s is not empty */
+
+				/* search if the pool exists */
+				i = 0;
+				for (wp2 = fpm_worker_all_pools; wp2; wp2 = wp2->next) {
+					if (wp2->config->name && strcmp(wp2->config->name, s) == 0) {
+						i = 1;
+						break;
+					}
+				}
+
+				if (!i) {
+					zlog(ZLOG_ERROR, "[pool %s] pm.status_allow specifies an unknown pool: %s", wp->config->name, s);
+					return -1;
+				}
+
+				/* we have one more winner */
+				wp->pm_status_allow_nb++;
+			}
+
+			/* alloc allowed pools strings */
+			if (wp->pm_status_allow_nb > 0) {
+				if (!(wp->pm_status_allow_pools = malloc(sizeof(char *) * wp->pm_status_allow_nb))) {
+					return -1;
+				}
+
+				/* dup the original string as strtok alterates it */
+				if (!(orig = strdup(wp->config->pm_status_allow))) {
+					return -1;
+				}
+				orig2 = orig;
+
+				i = 0;
+				while ((s = strtok_r(orig2, ",", &saveptr))) {
+					orig2 = NULL;
+					if (strlen(s) > 0) {
+						if (!(wp->pm_status_allow_pools[i] = strdup(s))) {
+							free(orig);
+							return -1;
+						}
+						i++;
+					}
+				}
+
+				free(orig);
+			}
+		}
+	}
+	return 0;
+}
+/* }}} */
 
 int fpm_status_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
 {
-	if (!wp || !wp->config) {
+	if (!wp || !wp->config || !wp->config->name) {
 		zlog(ZLOG_ERROR, "unable to init fpm_status because conf structure is NULL");
 		return -1;
 	}
 
+	/* save our own pool name */
+	if (!(fpm_status_pool_name = strdup(wp->config->name))) {
+		return -1;
+	}
+
 	if (wp->config->pm_status_path) {
 		fpm_status_uri = strdup(wp->config->pm_status_path);
+
+		fpm_status_allow_nb = wp->pm_status_allow_nb;
+		/* duplicate allowed pools as the origin will be freed right after child init */
+		if (fpm_status_allow_nb > 0) {
+			int i;
+			if (!(fpm_status_allow_pools = malloc(sizeof(char *) * fpm_status_allow_nb))) {
+				return -1;
+			}
+
+			for (i=0; i<fpm_status_allow_nb; i++) {
+				if (!(fpm_status_allow_pools[i] = strdup(wp->pm_status_allow_pools[i]))) {
+					return -1;
+				}
+			}
+		}
 	}
 
 	if (wp->config->ping_path) {
@@ -78,9 +193,36 @@ int fpm_status_handle_request(TSRMLS_D) /* {{{ */
 
 	/* STATUS */
 	if (fpm_status_uri && !strcmp(fpm_status_uri, SG(request_info).request_uri)) {
+		char *pool;
+
 		fpm_request_executing();
 
-		scoreboard_p = fpm_scoreboard_get();
+		pool = fpm_php_get_string_from_table("_GET", "pool" TSRMLS_CC);
+
+		/* we're asked for a different pool than us, check if allowed */
+		if (pool && strlen(pool) > 0 && strcmp(fpm_status_pool_name, pool) != 0 && fpm_status_allow_nb >= 0) {
+			int ok = 0;
+			int i;
+			for (i=0; i<fpm_status_allow_nb; i++) {
+				if (!strcmp(fpm_status_allow_pools[i], pool)) {
+					ok = 1;
+					break;
+				}
+			}
+
+			/* nothing has been found, permission is not allowed to show this pool status */
+			if (!ok) {
+				zlog(ZLOG_ERROR, "status: trying to access pool %s status, but it's not been allowed in pm.status_allow", pool);
+				SG(sapi_headers).http_response_code = 403;
+				sapi_add_header_ex(ZEND_STRL("Content-Type: text/plain"), 1, 1 TSRMLS_CC);
+				sapi_add_header_ex(ZEND_STRL("Expires: Thu, 01 Jan 1970 00:00:00 GMT"), 1, 1 TSRMLS_CC);
+				sapi_add_header_ex(ZEND_STRL("Cache-Control: no-cache, no-store, must-revalidate, max-age=0"), 1, 1 TSRMLS_CC);
+				PUTS("Access denied");
+				return 1;
+			}
+		}
+
+		scoreboard_p = fpm_scoreboard_get(pool TSRMLS_CC);
 		if (!scoreboard_p) {
 			zlog(ZLOG_ERROR, "status: unable to find or access status shared memory");
 			SG(sapi_headers).http_response_code = 500;
@@ -125,13 +267,13 @@ int fpm_status_handle_request(TSRMLS_D) /* {{{ */
 		}
 
 		/* full status ? */
-		full = SG(request_info).request_uri && strstr(SG(request_info).query_string, "full");
+		full = (fpm_php_get_string_from_table("_GET", "full" TSRMLS_CC) != NULL);
 		short_syntax = short_post = NULL;
 		full_separator = full_pre = full_syntax = full_post = NULL;
 		encode = 0;
 
 		/* HTML */
-		if (SG(request_info).query_string && strstr(SG(request_info).query_string, "html")) {
+		if (fpm_php_get_string_from_table("_GET", "html")) {
 			sapi_add_header_ex(ZEND_STRL("Content-Type: text/html"), 1, 1 TSRMLS_CC);
 			time_format = "%d/%b/%Y:%H:%M:%S %z";
 			encode = 1;
@@ -205,7 +347,7 @@ int fpm_status_handle_request(TSRMLS_D) /* {{{ */
 			}
 
 		/* XML */
-		} else if (SG(request_info).request_uri && strstr(SG(request_info).query_string, "xml")) {
+		} else if (fpm_php_get_string_from_table("_GET", "xml")) {
 			sapi_add_header_ex(ZEND_STRL("Content-Type: text/xml"), 1, 1 TSRMLS_CC);
 			time_format = "%s";
 			encode = 1;
@@ -256,7 +398,7 @@ int fpm_status_handle_request(TSRMLS_D) /* {{{ */
 				}
 
 			/* JSON */
-		} else if (SG(request_info).request_uri && strstr(SG(request_info).query_string, "json")) {
+		} else if (fpm_php_get_string_from_table("_GET", "json")) {
 			sapi_add_header_ex(ZEND_STRL("Content-Type: application/json"), 1, 1 TSRMLS_CC);
 			time_format = "%s";
 
diff --git a/sapi/fpm/fpm/fpm_status.h b/sapi/fpm/fpm/fpm_status.h
index 8f3daf9..56ee819 100644
--- a/sapi/fpm/fpm/fpm_status.h
+++ b/sapi/fpm/fpm/fpm_status.h
@@ -21,6 +21,7 @@ struct fpm_status_s {
 	struct timeval last_update;
 };
 
+int fpm_status_init_main();
 int fpm_status_init_child(struct fpm_worker_pool_s *wp);
 void fpm_status_update_activity(struct fpm_shm_s *shm, int idle, int active, int total, unsigned cur_lq, int max_lq, int clear_last_update);
 void fpm_status_update_accepted_conn(struct fpm_shm_s *shm, unsigned long int accepted_conn);
diff --git a/sapi/fpm/fpm/fpm_worker_pool.h b/sapi/fpm/fpm/fpm_worker_pool.h
index 6688e6d..8279948 100644
--- a/sapi/fpm/fpm/fpm_worker_pool.h
+++ b/sapi/fpm/fpm/fpm_worker_pool.h
@@ -42,6 +42,10 @@ struct fpm_worker_pool_s {
 	/* for ondemand PM */
 	struct fpm_event_s *ondemand_event;
 	int socket_event_set;
+
+	/* status */
+	int pm_status_allow_nb;
+	char **pm_status_allow_pools;
 };
 
 struct fpm_worker_pool_s *fpm_worker_pool_alloc();
diff --git a/sapi/fpm/php-fpm.conf.in b/sapi/fpm/php-fpm.conf.in
index 44e4dba..8892113 100644
--- a/sapi/fpm/php-fpm.conf.in
+++ b/sapi/fpm/php-fpm.conf.in
@@ -340,6 +340,13 @@ pm.max_spare_servers = 3
 ;       may conflict with a real PHP file.
 ; Default Value: not set 
 ;pm.status_path = /status
+
+; Allow this pool to access other status pools
+; Specify the pool when calling the status page: /status?pool=xxxxx
+; Note: - Different pool names can be specified using the comma ',' character
+;       - Allow to access all status pools by setting pm.status_allow to '*'
+; Default Value: not set (means only the current pool)
+;pm.status_allow = pool1,pool2,pool3
  
 ; The ping URI to call the monitoring page of FPM. If this value is not set, no
 ; URI will be recognized as a ping page. This could be used to test from outside
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Apr 25 09:01:29 2024 UTC