File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / php / ext / pdo_odbc / odbc_driver.c
Revision 1.1.1.3 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Mon Jul 22 01:31:59 2013 UTC (10 years, 11 months ago) by misho
Branches: php, MAIN
CVS tags: v5_4_17, HEAD
5.4.17

    1: /*
    2:   +----------------------------------------------------------------------+
    3:   | PHP Version 5                                                        |
    4:   +----------------------------------------------------------------------+
    5:   | Copyright (c) 1997-2013 The PHP Group                                |
    6:   +----------------------------------------------------------------------+
    7:   | This source file is subject to version 3.0 of the PHP license,       |
    8:   | that is bundled with this package in the file LICENSE, and is        |
    9:   | available through the world-wide-web at the following url:           |
   10:   | http://www.php.net/license/3_0.txt.                                  |
   11:   | If you did not receive a copy of the PHP license and are unable to   |
   12:   | obtain it through the world-wide-web, please send a note to          |
   13:   | license@php.net so we can mail you a copy immediately.               |
   14:   +----------------------------------------------------------------------+
   15:   | Author: Wez Furlong <wez@php.net>                                    |
   16:   +----------------------------------------------------------------------+
   17: */
   18: 
   19: /* $Id: odbc_driver.c,v 1.1.1.3 2013/07/22 01:31:59 misho Exp $ */
   20: 
   21: #ifdef HAVE_CONFIG_H
   22: #include "config.h"
   23: #endif
   24: 
   25: #include "php.h"
   26: #include "php_ini.h"
   27: #include "ext/standard/info.h"
   28: #include "pdo/php_pdo.h"
   29: #include "pdo/php_pdo_driver.h"
   30: #include "php_pdo_odbc.h"
   31: #include "php_pdo_odbc_int.h"
   32: #include "zend_exceptions.h"
   33: 
   34: static int pdo_odbc_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *info TSRMLS_DC)
   35: {
   36: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
   37: 	pdo_odbc_errinfo *einfo = &H->einfo;
   38: 	pdo_odbc_stmt *S = NULL;
   39: 	char *message = NULL;
   40: 
   41: 	if (stmt) {
   42: 		S = (pdo_odbc_stmt*)stmt->driver_data;
   43: 		einfo = &S->einfo;
   44: 	}
   45: 
   46: 	spprintf(&message, 0, "%s (%s[%ld] at %s:%d)",
   47: 				einfo->last_err_msg,
   48: 				einfo->what, einfo->last_error,
   49: 				einfo->file, einfo->line);
   50: 
   51: 	add_next_index_long(info, einfo->last_error);
   52: 	add_next_index_string(info, message, 0);
   53: 	add_next_index_string(info, einfo->last_state, 1);
   54: 
   55: 	return 1;
   56: }
   57: 
   58: 
   59: void pdo_odbc_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, PDO_ODBC_HSTMT statement, char *what, const char *file, int line TSRMLS_DC) /* {{{ */
   60: {
   61: 	SQLRETURN rc;
   62: 	SQLSMALLINT	errmsgsize = 0;
   63: 	SQLHANDLE eh;
   64: 	SQLSMALLINT htype, recno = 1;
   65: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle*)dbh->driver_data;
   66: 	pdo_odbc_errinfo *einfo = &H->einfo;
   67: 	pdo_odbc_stmt *S = NULL;
   68: 	pdo_error_type *pdo_err = &dbh->error_code;
   69: 
   70: 	if (stmt) {
   71: 		S = (pdo_odbc_stmt*)stmt->driver_data;
   72: 
   73: 		einfo = &S->einfo;
   74: 		pdo_err = &stmt->error_code;
   75: 	}
   76: 
   77: 	if (statement == SQL_NULL_HSTMT && S) {
   78: 		statement = S->stmt;
   79: 	}
   80: 
   81: 	if (statement) {
   82: 		htype = SQL_HANDLE_STMT;
   83: 		eh = statement;
   84: 	} else if (H->dbc) {
   85: 		htype = SQL_HANDLE_DBC;
   86: 		eh = H->dbc;
   87: 	} else {
   88: 		htype = SQL_HANDLE_ENV;
   89: 		eh = H->env;
   90: 	}
   91: 
   92: 	rc = SQLGetDiagRec(htype, eh, recno++, einfo->last_state, &einfo->last_error,
   93: 			einfo->last_err_msg, sizeof(einfo->last_err_msg)-1, &errmsgsize);
   94: 
   95: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
   96: 		errmsgsize = 0;
   97: 	}
   98: 
   99: 	einfo->last_err_msg[errmsgsize] = '\0';
  100: 	einfo->file = file;
  101: 	einfo->line = line;
  102: 	einfo->what = what;
  103: 
  104: 	strcpy(*pdo_err, einfo->last_state);
  105: /* printf("@@ SQLSTATE[%s] %s\n", *pdo_err, einfo->last_err_msg); */
  106: 	if (!dbh->methods) {
  107: 		zend_throw_exception_ex(php_pdo_get_exception(), einfo->last_error TSRMLS_CC, "SQLSTATE[%s] %s: %d %s",
  108: 				*pdo_err, what, einfo->last_error, einfo->last_err_msg);
  109: 	}
  110: 
  111: 	/* just like a cursor, once you start pulling, you need to keep
  112: 	 * going until the end; SQL Server (at least) will mess with the
  113: 	 * actual cursor state if you don't finish retrieving all the
  114: 	 * diagnostic records (which can be generated by PRINT statements
  115: 	 * in the query, for instance). */
  116: 	while (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
  117: 		char discard_state[6];
  118: 		char discard_buf[1024];
  119: 		SQLINTEGER code;
  120: 		rc = SQLGetDiagRec(htype, eh, recno++, discard_state, &code,
  121: 				discard_buf, sizeof(discard_buf)-1, &errmsgsize);
  122: 	}
  123: 
  124: }
  125: /* }}} */
  126: 
  127: static int odbc_handle_closer(pdo_dbh_t *dbh TSRMLS_DC)
  128: {
  129: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle*)dbh->driver_data;
  130: 
  131: 	if (H->dbc != SQL_NULL_HANDLE) {
  132: 		SQLEndTran(SQL_HANDLE_DBC, H->dbc, SQL_ROLLBACK);
  133: 		SQLDisconnect(H->dbc);
  134: 		SQLFreeHandle(SQL_HANDLE_DBC, H->dbc);
  135: 		H->dbc = NULL;
  136: 	}
  137: 	SQLFreeHandle(SQL_HANDLE_ENV, H->env);
  138: 	H->env = NULL;
  139: 	pefree(H, dbh->is_persistent);
  140: 	dbh->driver_data = NULL;
  141: 
  142: 	return 0;
  143: }
  144: 
  145: static int odbc_handle_preparer(pdo_dbh_t *dbh, const char *sql, long sql_len, pdo_stmt_t *stmt, zval *driver_options TSRMLS_DC)
  146: {
  147: 	RETCODE rc;
  148: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  149: 	pdo_odbc_stmt *S = ecalloc(1, sizeof(*S));
  150: 	enum pdo_cursor_type cursor_type = PDO_CURSOR_FWDONLY;
  151: 	int ret;
  152: 	char *nsql = NULL;
  153: 	int nsql_len = 0;
  154: 
  155: 	S->H = H;
  156: 	S->assume_utf8 = H->assume_utf8;
  157: 
  158: 	/* before we prepare, we need to peek at the query; if it uses named parameters,
  159: 	 * we want PDO to rewrite them for us */
  160: 	stmt->supports_placeholders = PDO_PLACEHOLDER_POSITIONAL;
  161: 	ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len TSRMLS_CC);
  162: 	
  163: 	if (ret == 1) {
  164: 		/* query was re-written */
  165: 		sql = nsql;
  166: 	} else if (ret == -1) {
  167: 		/* couldn't grok it */
  168: 		strcpy(dbh->error_code, stmt->error_code);
  169: 		efree(S);
  170: 		return 0;
  171: 	}
  172: 	
  173: 	rc = SQLAllocHandle(SQL_HANDLE_STMT, H->dbc, &S->stmt);
  174: 
  175: 	if (rc == SQL_INVALID_HANDLE || rc == SQL_ERROR) {
  176: 		efree(S);
  177: 		if (nsql) {
  178: 			efree(nsql);
  179: 		}
  180: 		pdo_odbc_drv_error("SQLAllocStmt");
  181: 		return 0;
  182: 	}
  183: 
  184: 	cursor_type = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR, PDO_CURSOR_FWDONLY TSRMLS_CC);
  185: 	if (cursor_type != PDO_CURSOR_FWDONLY) {
  186: 		rc = SQLSetStmtAttr(S->stmt, SQL_ATTR_CURSOR_SCROLLABLE, (void*)SQL_SCROLLABLE, 0);
  187: 		if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  188: 			pdo_odbc_stmt_error("SQLSetStmtAttr: SQL_ATTR_CURSOR_SCROLLABLE");
  189: 			SQLFreeHandle(SQL_HANDLE_STMT, S->stmt);
  190: 			if (nsql) {
  191: 				efree(nsql);
  192: 			}
  193: 			return 0;
  194: 		}
  195: 	}
  196: 	
  197: 	rc = SQLPrepare(S->stmt, (char*)sql, SQL_NTS);
  198: 	if (nsql) {
  199: 		efree(nsql);
  200: 	}
  201: 
  202: 	stmt->driver_data = S;
  203: 	stmt->methods = &odbc_stmt_methods;
  204: 
  205: 	if (rc != SQL_SUCCESS) {
  206: 		pdo_odbc_stmt_error("SQLPrepare");
  207:         if (rc != SQL_SUCCESS_WITH_INFO) {
  208:             /* clone error information into the db handle */
  209:             strcpy(H->einfo.last_err_msg, S->einfo.last_err_msg);
  210:             H->einfo.file = S->einfo.file;
  211:             H->einfo.line = S->einfo.line;
  212:             H->einfo.what = S->einfo.what;
  213:             strcpy(dbh->error_code, stmt->error_code);
  214:         }
  215: 	}
  216: 
  217: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  218: 		return 0;
  219: 	}
  220: 	return 1;
  221: }
  222: 
  223: static long odbc_handle_doer(pdo_dbh_t *dbh, const char *sql, long sql_len TSRMLS_DC)
  224: {
  225: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  226: 	RETCODE rc;
  227: 	SQLLEN row_count = -1;
  228: 	PDO_ODBC_HSTMT	stmt;
  229: 	
  230: 	rc = SQLAllocHandle(SQL_HANDLE_STMT, H->dbc, &stmt);
  231: 	if (rc != SQL_SUCCESS) {
  232: 		pdo_odbc_drv_error("SQLAllocHandle: STMT");
  233: 		return -1;
  234: 	}
  235: 
  236: 	rc = SQLExecDirect(stmt, (char *)sql, sql_len);
  237: 
  238: 	if (rc == SQL_NO_DATA) {
  239: 		/* If SQLExecDirect executes a searched update or delete statement that
  240: 		 * does not affect any rows at the data source, the call to
  241: 		 * SQLExecDirect returns SQL_NO_DATA. */
  242: 		row_count = 0;
  243: 		goto out;
  244: 	}
  245: 
  246: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  247: 		pdo_odbc_doer_error("SQLExecDirect");
  248: 		goto out;
  249: 	}
  250: 
  251: 	rc = SQLRowCount(stmt, &row_count);
  252: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  253: 		pdo_odbc_doer_error("SQLRowCount");
  254: 		goto out;
  255: 	}
  256: 	if (row_count == -1) {
  257: 		row_count = 0;
  258: 	}
  259: out:
  260: 	SQLFreeHandle(SQL_HANDLE_STMT, stmt);
  261: 	return row_count;
  262: }
  263: 
  264: static int odbc_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, int unquotedlen, char **quoted, int *quotedlen, enum pdo_param_type param_type  TSRMLS_DC)
  265: {
  266: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  267: 	/* TODO: figure it out */
  268: 	return 0;
  269: }
  270: 
  271: static int odbc_handle_begin(pdo_dbh_t *dbh TSRMLS_DC)
  272: {
  273: 	if (dbh->auto_commit) {
  274: 		/* we need to disable auto-commit now, to be able to initiate a transaction */
  275: 		RETCODE rc;
  276: 		pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  277: 
  278: 		rc = SQLSetConnectAttr(H->dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, SQL_IS_INTEGER);
  279: 		if (rc != SQL_SUCCESS) {
  280: 			pdo_odbc_drv_error("SQLSetConnectAttr AUTOCOMMIT = OFF");
  281: 			return 0;
  282: 		}
  283: 	}
  284: 	return 1;
  285: }
  286: 
  287: static int odbc_handle_commit(pdo_dbh_t *dbh TSRMLS_DC)
  288: {
  289: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  290: 	RETCODE rc;
  291: 
  292: 	rc = SQLEndTran(SQL_HANDLE_DBC, H->dbc, SQL_COMMIT);
  293: 
  294: 	if (rc != SQL_SUCCESS) {
  295: 		pdo_odbc_drv_error("SQLEndTran: Commit");
  296: 
  297: 		if (rc != SQL_SUCCESS_WITH_INFO) {
  298: 			return 0;
  299: 		}
  300: 	}
  301: 
  302: 	if (dbh->auto_commit) {
  303: 		/* turn auto-commit back on again */
  304: 		rc = SQLSetConnectAttr(H->dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, SQL_IS_INTEGER);
  305: 		if (rc != SQL_SUCCESS) {
  306: 			pdo_odbc_drv_error("SQLSetConnectAttr AUTOCOMMIT = ON");
  307: 			return 0;
  308: 		}
  309: 	}
  310: 	return 1;
  311: }
  312: 
  313: static int odbc_handle_rollback(pdo_dbh_t *dbh TSRMLS_DC)
  314: {
  315: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  316: 	RETCODE rc;
  317: 
  318: 	rc = SQLEndTran(SQL_HANDLE_DBC, H->dbc, SQL_ROLLBACK);
  319: 
  320: 	if (rc != SQL_SUCCESS) {
  321: 		pdo_odbc_drv_error("SQLEndTran: Rollback");
  322: 
  323: 		if (rc != SQL_SUCCESS_WITH_INFO) {
  324: 			return 0;
  325: 		}
  326: 	}
  327: 	if (dbh->auto_commit && H->dbc) {
  328: 		/* turn auto-commit back on again */
  329: 		rc = SQLSetConnectAttr(H->dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, SQL_IS_INTEGER);
  330: 		if (rc != SQL_SUCCESS) {
  331: 			pdo_odbc_drv_error("SQLSetConnectAttr AUTOCOMMIT = ON");
  332: 			return 0;
  333: 		}
  334: 	}
  335: 
  336: 	return 1;
  337: }
  338: 
  339: static int odbc_handle_set_attr(pdo_dbh_t *dbh, long attr, zval *val TSRMLS_DC)
  340: {
  341: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  342: 	switch (attr) {
  343: 		case PDO_ODBC_ATTR_ASSUME_UTF8:
  344: 			H->assume_utf8 = zval_is_true(val);
  345: 			return 1;
  346: 		default:
  347: 			strcpy(H->einfo.last_err_msg, "Unknown Attribute");
  348: 			H->einfo.what = "setAttribute";
  349: 			strcpy(H->einfo.last_state, "IM001");
  350: 			return -1;
  351: 	}
  352: }
  353: 
  354: static int odbc_handle_get_attr(pdo_dbh_t *dbh, long attr, zval *val TSRMLS_DC)
  355: {
  356: 	pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data;
  357: 	switch (attr) {
  358: 		case PDO_ATTR_CLIENT_VERSION:
  359: 			ZVAL_STRING(val, "ODBC-" PDO_ODBC_TYPE, 1);
  360: 			return 1;
  361: 
  362: 		case PDO_ATTR_SERVER_VERSION:
  363: 		case PDO_ATTR_PREFETCH:
  364: 		case PDO_ATTR_TIMEOUT:
  365: 		case PDO_ATTR_SERVER_INFO:
  366: 		case PDO_ATTR_CONNECTION_STATUS:
  367: 			break;
  368: 		case PDO_ODBC_ATTR_ASSUME_UTF8:
  369: 			ZVAL_BOOL(val, H->assume_utf8 ? 1 : 0);
  370: 			return 1;
  371: 
  372: 	}
  373: 	return 0;
  374: }
  375: 
  376: static struct pdo_dbh_methods odbc_methods = {
  377: 	odbc_handle_closer,
  378: 	odbc_handle_preparer,
  379: 	odbc_handle_doer,
  380: 	odbc_handle_quoter,
  381: 	odbc_handle_begin,
  382: 	odbc_handle_commit,
  383: 	odbc_handle_rollback,
  384: 	odbc_handle_set_attr,
  385: 	NULL,	/* last id */
  386: 	pdo_odbc_fetch_error_func,
  387: 	odbc_handle_get_attr,	/* get attr */
  388: 	NULL,	/* check_liveness */
  389: };
  390: 
  391: static int pdo_odbc_handle_factory(pdo_dbh_t *dbh, zval *driver_options TSRMLS_DC) /* {{{ */
  392: {
  393: 	pdo_odbc_db_handle *H;
  394: 	RETCODE rc;
  395: 	int use_direct = 0;
  396: 	SQLUINTEGER cursor_lib;
  397: 
  398: 	H = pecalloc(1, sizeof(*H), dbh->is_persistent);
  399: 
  400: 	dbh->driver_data = H;
  401: 	
  402: 	SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &H->env);
  403: 	rc = SQLSetEnvAttr(H->env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
  404: 
  405: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  406: 		pdo_odbc_drv_error("SQLSetEnvAttr: ODBC3");
  407: 		goto fail;
  408: 	}
  409: 
  410: #ifdef SQL_ATTR_CONNECTION_POOLING
  411: 	if (pdo_odbc_pool_on != SQL_CP_OFF) {
  412: 		rc = SQLSetEnvAttr(H->env, SQL_ATTR_CP_MATCH, (void*)pdo_odbc_pool_mode, 0);
  413: 		if (rc != SQL_SUCCESS) {
  414: 			pdo_odbc_drv_error("SQLSetEnvAttr: SQL_ATTR_CP_MATCH");
  415: 			goto fail;
  416: 		}
  417: 	}
  418: #endif
  419: 	
  420: 	rc = SQLAllocHandle(SQL_HANDLE_DBC, H->env, &H->dbc);
  421: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  422: 		pdo_odbc_drv_error("SQLAllocHandle (DBC)");
  423: 		goto fail;
  424: 	}
  425: 
  426: 	rc = SQLSetConnectAttr(H->dbc, SQL_ATTR_AUTOCOMMIT,
  427: 		(SQLPOINTER)(dbh->auto_commit ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF), SQL_IS_INTEGER);
  428: 	if (rc != SQL_SUCCESS) {
  429: 		pdo_odbc_drv_error("SQLSetConnectAttr AUTOCOMMIT");
  430: 		goto fail;
  431: 	}
  432: 
  433: 	/* set up the cursor library, if needed, or if configured explicitly */
  434: 	cursor_lib = pdo_attr_lval(driver_options, PDO_ODBC_ATTR_USE_CURSOR_LIBRARY, SQL_CUR_USE_IF_NEEDED TSRMLS_CC);
  435: 	rc = SQLSetConnectAttr(H->dbc, SQL_ODBC_CURSORS, (void*)cursor_lib, SQL_IS_INTEGER);
  436: 	if (rc != SQL_SUCCESS && cursor_lib != SQL_CUR_USE_IF_NEEDED) {
  437: 		pdo_odbc_drv_error("SQLSetConnectAttr SQL_ODBC_CURSORS");
  438: 		goto fail;
  439: 	}
  440: 
  441: 	if (strchr(dbh->data_source, ';')) {
  442: 		char dsnbuf[1024];
  443: 		short dsnbuflen;
  444: 
  445: 		use_direct = 1;
  446: 
  447: 		/* Force UID and PWD to be set in the DSN */
  448: 		if (dbh->username && *dbh->username && !strstr(dbh->data_source, "uid")
  449: 				&& !strstr(dbh->data_source, "UID")) {
  450: 			char *dsn;
  451: 			spprintf(&dsn, 0, "%s;UID=%s;PWD=%s", dbh->data_source, dbh->username, dbh->password);
  452: 			pefree((char*)dbh->data_source, dbh->is_persistent);
  453: 			dbh->data_source = dsn;
  454: 		}
  455: 
  456: 		rc = SQLDriverConnect(H->dbc, NULL, (char*)dbh->data_source, strlen(dbh->data_source),
  457: 				dsnbuf, sizeof(dsnbuf)-1, &dsnbuflen, SQL_DRIVER_NOPROMPT);
  458: 	}
  459: 	if (!use_direct) {
  460: 		rc = SQLConnect(H->dbc, (char*)dbh->data_source, SQL_NTS, dbh->username, SQL_NTS, dbh->password, SQL_NTS);
  461: 	}
  462: 
  463: 	if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
  464: 		pdo_odbc_drv_error(use_direct ? "SQLDriverConnect" : "SQLConnect");
  465: 		goto fail;
  466: 	}
  467: 
  468: 	/* TODO: if we want to play nicely, we should check to see if the driver really supports ODBC v3 or not */
  469: 
  470: 	dbh->methods = &odbc_methods;
  471: 	dbh->alloc_own_columns = 1;
  472: 	
  473: 	return 1;
  474: 
  475: fail:
  476: 	dbh->methods = &odbc_methods;
  477: 	return 0;
  478: }
  479: /* }}} */
  480: 
  481: pdo_driver_t pdo_odbc_driver = {
  482: 	PDO_DRIVER_HEADER(odbc),
  483: 	pdo_odbc_handle_factory
  484: };
  485: 
  486: /*
  487:  * Local variables:
  488:  * tab-width: 4
  489:  * c-basic-offset: 4
  490:  * End:
  491:  * vim600: noet sw=4 ts=4 fdm=marker
  492:  * vim<600: noet sw=4 ts=4
  493:  */

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>