Пример #1
0
function processTable($table, $idCol, $cfg)
{
    global $ALLOWED_QUERY_OPERATORS, $ALLOWED_PS_TYPES, $SEARCH_PRESENTATION_DATATABLES_ONLY_PARAMS, $SEARCH_PRESENTATION_DATATABLES_ONLY_COLUMN_PARAMS, $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_PARAMS, $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_COLUMN_PARAMS, $templatesDir, $docroot, $enableLangFiles;
    $generatedFileMessage = <<<EOF
// DO NOT EDIT THIS FILE.
// This file was generated by searchgen.
// If you need to customize this file, please edit the corresponding
// yaml file in the gencfg directory, and then re-generate this file
// by running searchgen, passing in the table name.
EOF;
    $canDoFulltextSearchPHP = 'in_array($db->getDialect(), array(\'mysql\'))';
    $jaxInclude = $cfg['jaxInclude'];
    $jaxJQuery = $cfg['jaxJQuery'];
    $jaxJS = $cfg['jaxJS'];
    // ---------------------------
    // Create the search includes.
    // ---------------------------
    if (!isset($cfg['searches']) || !is_array($cfg['searches'])) {
        $cfg['searches'] = array();
    }
    foreach ($cfg['searches'] as $searchName => $search) {
        $langKeys = array();
        $searchCommand = isset($search['searchCommand']) && trim($search['searchCommand']) != '' ? trim($search['searchCommand']) : 'search' . ucfirst($searchName) . 's';
        $extraSelectColumns = '';
        if (isset($search['extraSelectColumns'])) {
            if (is_array($search['extraSelectColumns'])) {
                $extraSelectColumns = implode(', ', $search['extraSelectColumns']);
            } else {
                $extraSelectColumns = trim((string) $search['extraSelectColumns']);
            }
        }
        $outputPath = isset($search['outputPath']) && is_string($search['outputPath']) ? $search['outputPath'] : '';
        if ($outputPath == '') {
            $outputPath = 'include/search';
        }
        $outputDir = $docroot . '/' . $outputPath;
        @mkdir($outputDir, 0777, true);
        $numDirsDeepUnderHTML = calcDirDepth($outputPath);
        if (isset($search['docRootPath'])) {
            $docRootPath = '"' . addcslashes($search['docRootPath']) . '"';
        } else {
            $docRootPath = 'dirname(__FILE__)';
            for ($i = 0; $i < $numDirsDeepUnderHTML; $i++) {
                $docRootPath = "dirname({$docRootPath})";
            }
        }
        $phpIncludes = getPHPClassesAndIncludes($search, $numDirsDeepUnderHTML);
        $searchTemplate = isset($search['searchTemplate']) && is_string($search['searchTemplate']) ? $search['searchTemplate'] : '';
        if ($searchTemplate == '') {
            $searchTemplate = 'search.include.php';
        }
        $joins = '';
        if (isset($search['joins'])) {
            $joins = trim($search['joins']);
        }
        $arr = getWhereClauseParams($table, $searchName, $search, 'search', $langKeys);
        $haveAnyFulltextQueryOperators = $arr['haveAnyFulltextQueryOperators'];
        $searchWhereClausePHP = $arr['searchWhereClausePHP'];
        $searchWhereAssignments = $arr['searchWhereAssignments'];
        $searchableColumnsPHPArray = $arr['searchableColumnsPHPArray'];
        $searchableColumnsPHPArraySep = $arr['searchableColumnsPHPArraySep'];
        $andWhere = $arr['andWhere'];
        $andWhereAssignments = $arr['andWhereAssignments'];
        $groupBy = '';
        if (isset($search['groupBy'])) {
            $groupBy = trim((string) $search['groupBy']);
        }
        $rowProcessingPHPCode = '';
        if (isset($search['rowProcessingPHPCode'])) {
            $rowProcessingPHPCode = (string) $search['rowProcessingPHPCode'];
        }
        $unsetForbiddenColumns = array();
        if (isset($search['forbiddenColumns'])) {
            foreach ($search['forbiddenColumns'] as $colName) {
                $unsetForbiddenColumns[] = "\t\tunset(\$row->{$colName});";
            }
        }
        $searchFor = array('{{generatedFileMessage}}', '{{docRootPath}}', '{{jaxInclude}}', '{{jaxJQuery}}', '{{jaxJS}}', '{{phpIncludes}}', '{{searchName}}', '{{uSearchName}}', '{{tableName}}', '{{uTableName}}', '{{searchCommand}}', '{{searchableColumnsPHPArray}}', '{{idCol}}', '{{uIdCol}}', '{{extraSelectColumns}}', '{{joins}}', '{{canDoFulltextSearchPHP}}', '{{searchWhereClausePHP}}', '{{andWhere}}', '{{searchWhereAssignments}}', '{{andWhereAssignments}}', '{{groupBy}}', '{{rowProcessingPHPCode}}', '{{unsetForbiddenColumns}}');
        $replaceWith = array($generatedFileMessage, $docRootPath, $jaxInclude, $jaxJQuery, $jaxJS, $phpIncludes, $searchName, ucfirst($searchName), $table->tableName, ucfirst($table->tableName), $searchCommand, $searchableColumnsPHPArray, $idCol, ucfirst($idCol), $extraSelectColumns != '' ? ', ' . $extraSelectColumns : '', $joins != '' ? ' ' . $joins : '', $canDoFulltextSearchPHP, $searchWhereClausePHP, $andWhere != '' ? ' and (' . $andWhere . ')' : '', implode("\n", $searchWhereAssignments), implode("\n", $andWhereAssignments), $groupBy, $rowProcessingPHPCode, implode("\n", $unsetForbiddenColumns));
        $content = str_replace($searchFor, $replaceWith, filterFullTextSearchCode(file_get_contents($templatesDir . '/' . $searchTemplate), $haveAnyFulltextQueryOperators));
        $fn = $outputDir . '/' . $searchName . '_search.include.php';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if ($enableLangFiles) {
            $langfn = $outputDir . '/' . $searchName . '_search.include.php.strings';
            $langResources = loadLangResourceFileContents($langfn);
            $anyResourcesChanged = false;
            foreach ($langKeys as $key => $val) {
                if (getLangResource($langResources, $key) != $val) {
                    setLangResource($langResources, $key, $val);
                    $anyResourcesChanged = true;
                }
            }
            if ($anyResourcesChanged) {
                saveLangResourceFileContents($langfn, $langResources);
            }
        }
    }
    // foreach ($cfg['searches'] as $searchName=>$search)
    // ---------------------------------------
    // Create the autcomplete search includes.
    // ---------------------------------------
    if (!isset($cfg['autocompleteSearches']) || !is_array($cfg['autocompleteSearches'])) {
        $cfg['autocompleteSearches'] = array();
    }
    foreach ($cfg['autocompleteSearches'] as $searchName => $search) {
        $langKeys = array();
        $searchCommand = isset($search['searchCommand']) && trim($search['searchCommand']) != '' ? trim($search['searchCommand']) : 'autocomplete' . ucfirst($searchName) . 's';
        $extraSelectColumns = '';
        if (isset($search['extraSelectColumns'])) {
            if (is_array($search['extraSelectColumns'])) {
                $extraSelectColumns = implode(', ', $search['extraSelectColumns']);
            } else {
                $extraSelectColumns = trim((string) $search['extraSelectColumns']);
            }
        }
        $outputPath = isset($search['outputPath']) && is_string($search['outputPath']) ? $search['outputPath'] : '';
        if ($outputPath == '') {
            $outputPath = 'include/search';
        }
        $outputDir = $docroot . '/' . $outputPath;
        @mkdir($outputDir, 0777, true);
        $numDirsDeepUnderHTML = calcDirDepth($outputPath);
        if (isset($search['docRootPath'])) {
            $docRootPath = '"' . addcslashes($search['docRootPath']) . '"';
        } else {
            $docRootPath = 'dirname(__FILE__)';
            for ($i = 0; $i < $numDirsDeepUnderHTML; $i++) {
                $docRootPath = "dirname({$docRootPath})";
            }
        }
        $phpIncludes = getPHPClassesAndIncludes($search, $numDirsDeepUnderHTML);
        $searchTemplate = isset($search['searchTemplate']) && is_string($search['searchTemplate']) ? $search['searchTemplate'] : '';
        if ($searchTemplate == '') {
            $searchTemplate = 'autocomplete.include.php';
        }
        $idColumn = isset($search['idColumn']) && is_string($search['idColumn']) ? $search['idColumn'] : $idCol;
        $idColumnPSType = isset($search['idColumnPSType']) && is_string($search['idColumnPSType']) ? $search['idColumnPSType'] : 'int';
        if (!in_array($idColumnPSType, $ALLOWED_PS_TYPES)) {
            fprintf(STDERR, "Invalid idColumnPSType \"%s\" in autocompleteSearches.", $idColumnPSType);
            return false;
        }
        switch ($idColumnPSType) {
            case 'boolean':
            case 'int':
                $getIdParam = sprintf("\t\t\$%s = isset(\$params['%s']) ? (int)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                break;
            case 'float':
                $getIdParam = sprintf("\t\t\$%s = isset(\$params['%s']) ? (float)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                break;
            case 'double':
                $getIdParam = sprintf("\t\t\$%s = isset(\$params['%s']) ? (double)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                break;
            case 'string':
            case 'match':
            case 'binary':
            default:
                $getIdParam = sprintf("\t\t\$%s = isset(\$params['%s']) ? (string)\$params['%s'] : '';", $idColumn, $idColumn, $idColumn);
                break;
        }
        $altIdColumn = isset($search['altIdColumn']) && is_string($search['altIdColumn']) ? $search['altIdColumn'] : '';
        if ($altIdColumn != '') {
            $altIdColumnPSType = isset($search['altIdColumnPSType']) && is_string($search['altIdColumnPSType']) ? $search['altIdColumnPSType'] : 'int';
            if (!in_array($altIdColumnPSType, $ALLOWED_PS_TYPES)) {
                fprintf(STDERR, "Invalid altIdColumnPSType \"%s\" in autocompleteSearches.", $altIdColumnPSType);
                return false;
            }
            switch ($altIdColumnPSType) {
                case 'boolean':
                case 'int':
                    $getAltIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (int)trim(\$params['%s']) : null;", $altIdColumn, $altIdColumn, $altIdColumn);
                    break;
                case 'float':
                    $getAltIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (float)trim(\$params['%s']) : null;", $altIdColumn, $altIdColumn, $altIdColumn);
                    break;
                case 'double':
                    $getAltIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (double)trim(\$params['%s']) : null;", $altIdColumn, $altIdColumn, $altIdColumn);
                    break;
                case 'string':
                case 'match':
                case 'binary':
                default:
                    $getAltIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (string)\$params['%s'] : null;", $altIdColumn, $altIdColumn, $altIdColumn);
                    break;
            }
        } else {
            $altIdColumnPSType = '';
            $getAltIdParam = '';
        }
        $joins = '';
        if (isset($search['joins'])) {
            $joins = trim($search['joins']);
        }
        $dummyLangKeys = array();
        $arr = getWhereClauseParams($table, $searchName, $search, 'autocomplete', $dummyLangKeys);
        unset($dummyLangKeys);
        $haveAnyFulltextQueryOperators = $arr['haveAnyFulltextQueryOperators'];
        $searchWhereClausePHP = $arr['searchWhereClausePHP'];
        $searchWhereAssignments = $arr['searchWhereAssignments'];
        $andWhere = $arr['andWhere'];
        $andWhereAssignments = $arr['andWhereAssignments'];
        $searchResultLabelExpression = isset($search['searchResultLabelExpression']) ? trim($search['searchResultLabelExpression']) : '';
        $searchResultValueExpression = isset($search['searchResultValueExpression']) ? trim($search['searchResultValueExpression']) : '$row->' . $idColumn;
        $searchFor = array('{{generatedFileMessage}}', '{{docRootPath}}', '{{jaxInclude}}', '{{jaxJQuery}}', '{{jaxJS}}', '{{phpIncludes}}', '{{searchName}}', '{{uSearchName}}', '{{tableName}}', '{{uTableName}}', '{{searchCommand}}', '{{idCol}}', '{{uIdCol}}', '{{idColumnPSType}}', '{{uIdColumnPSType}}', '{{getIdParam}}', '{{altIdCol}}', '{{uAltIdCol}}', '{{altIdColumnPSType}}', '{{uAltIdColumnPSType}}', '{{getAltIdParam}}', '{{extraSelectColumns}}', '{{joins}}', '{{canDoFulltextSearchPHP}}', '{{searchWhereClausePHP}}', '{{andWhere}}', '{{searchWhereAssignments}}', '{{andWhereAssignments}}', '{{searchResultLabelExpression}}', '{{searchResultValueExpression}}');
        $replaceWith = array($generatedFileMessage, $docRootPath, $jaxInclude, $jaxJQuery, $jaxJS, $phpIncludes, $searchName, ucfirst($searchName), $table->tableName, ucfirst($table->tableName), $searchCommand, $idColumn, ucfirst($idColumn), $idColumnPSType, ucfirst($idColumnPSType), $getIdParam, $altIdColumn, ucfirst($altIdColumn), $altIdColumnPSType, ucfirst($altIdColumnPSType), $getAltIdParam, $extraSelectColumns != '' ? ', ' . $extraSelectColumns : '', $joins != '' ? ' ' . $joins : '', $canDoFulltextSearchPHP, $searchWhereClausePHP, $andWhere != '' ? ' and (' . $andWhere . ')' : '', implode("\n", $searchWhereAssignments), implode("\n", $andWhereAssignments), $searchResultLabelExpression, $searchResultValueExpression);
        $content = str_replace($searchFor, $replaceWith, filterAltIdAutocompleteCode(filterFullTextSearchCode(file_get_contents($templatesDir . '/' . $searchTemplate), $haveAnyFulltextQueryOperators), $altIdColumn != ''));
        $fn = $outputDir . '/' . $searchName . '_autocomplete.include.php';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if ($enableLangFiles) {
            $langfn = $outputDir . '/' . $searchName . '_autocomplete.include.php.strings';
            $langResources = loadLangResourceFileContents($langfn);
            $anyResourcesChanged = false;
            foreach ($langKeys as $key => $val) {
                if (getLangResource($langResources, $key) != $val) {
                    setLangResource($langResources, $key, $val);
                    $anyResourcesChanged = true;
                }
            }
            if ($anyResourcesChanged) {
                saveLangResourceFileContents($langfn, $langResources);
            }
        }
    }
    // foreach ($cfg['autocompleteSearches'] as $searchName=>$search)
    // -----------------------------------------
    // Create the popup search JavaScript files.
    // -----------------------------------------
    if (!isset($cfg['popupSearches']) || !is_array($cfg['popupSearches'])) {
        $cfg['popupSearches'] = array();
    }
    foreach ($cfg['popupSearches'] as $popupSearchName => $popupSearch) {
        if (!is_array($popupSearch)) {
            continue;
        }
        // If we encounter a "likePopupSearch" attribute for this popupSearch, it must
        // reference an existing popupSearch defined in the same YAML file, which may
        // or may not reference another popupSearch.  Circular references are not allowed.
        // When this happens, we merge the referenced popupSearch's attributes with this
        // popupSearch's attributes, allowing this popupSearch's attributes to override
        // the referenced popupSearch's attributes of the same name.
        $referencedPopupSearchNames = array();
        while (isset($popupSearch['likePopupSearch'])) {
            if (!is_string($popupSearch['likePopupSearch']) || !isset($cfg['popupSearches']) || !isset($cfg['popupSearches'][$popupSearch['likePopupSearch']]) || !is_array($cfg['popupSearches'][$popupSearch['likePopupSearch']])) {
                fprintf(STDERR, "Invalid likePopupSearch entry in popupSearch/* section.\n");
                return false;
            }
            $referencedPopupSearchName = $popupSearch['likePopupSearch'];
            unset($popupSearch['likePopupSearch']);
            if (in_array($referencedPopupSearchName, $referencedPopupSearchNames)) {
                fprintf(STDERR, "Circular reference in likePopupSearch: %s.\n", implode('->', $referencedPopupSearchNames));
                return false;
            }
            $referencedPopupSearchNames[] = $referencedPopupSearchName;
            $popupSearch = array_merge($cfg['popupSearches'][$referencedPopupSearchName], $popupSearch);
        }
        unset($referencedPopupSearchNames, $referencedPopupSearchName);
        $langKeys = array();
        $langKeys['popupSearch.' . $popupSearchName . '.tableDescription'] = $cfg['tableDescription'];
        $langKeys['popupSearch.' . $popupSearchName . '.tableDescriptions'] = $cfg['tableDescriptions'];
        $outputPath = isset($popupSearch['outputPath']) && is_string($popupSearch['outputPath']) ? $popupSearch['outputPath'] : '';
        if ($outputPath == '') {
            $outputPath = 'js/search';
        }
        $outputDir = $docroot . '/' . $outputPath;
        @mkdir($outputDir, 0777, true);
        $popupSearchTemplate = isset($popupSearch['popupSearchTemplate']) && is_string($popupSearch['popupSearchTemplate']) ? $popupSearch['popupSearchTemplate'] : '';
        if ($popupSearchTemplate == '') {
            $popupSearchTemplate = 'popupSearch.js';
        }
        if (!isset($popupSearch['searchCommand']) || !is_string($popupSearch['searchCommand'])) {
            continue;
        }
        $searchCommand = $popupSearch['searchCommand'];
        if (isset($popupSearch['searchPresentation']) && is_string($popupSearch['searchPresentation'])) {
            $searchPresentation = $popupSearch['searchPresentation'];
            if ($searchPresentation != 'dataTables' && $searchPresentation != 'AJAXSearchGrid') {
                $searchPresentation = 'dataTables';
            }
        } else {
            $searchPresentation = 'dataTables';
        }
        if ($searchPresentation != 'dataTables') {
            $badattrs = array_intersect(array_keys($popupSearch), $SEARCH_PRESENTATION_DATATABLES_ONLY_PARAMS);
            if (!empty($badattrs)) {
                fprintf(STDERR, "WARNING: The following popup search / CRUD search attributes are ignored when searchPresentation is not set to dataTables:\n    %s\n", implode("\n    ", $badattrs));
            }
        }
        if ($searchPresentation != 'AJAXSearchGrid') {
            $badattrs = array_intersect(array_keys($popupSearch), $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_PARAMS);
            if (!empty($badattrs)) {
                fprintf(STDERR, "WARNING: The following popup search / CRUD search attributes are ignored when searchPresentation is not set to AJAXSearchGrid:\n    %s\n", implode("\n    ", $badattrs));
            }
        }
        $badcolattrs_dataTables = array();
        $badcolattrs_AJAXSearchGrid = array();
        if (isset($popupSearch['columns']) && is_array($popupSearch['columns'])) {
            foreach ($popupSearch['columns'] as $col) {
                if ($searchPresentation != 'dataTables') {
                    $badattrs = array_intersect(array_keys($col), $SEARCH_PRESENTATION_DATATABLES_ONLY_COLUMN_PARAMS);
                    if (!empty($badattrs)) {
                        $badcolattrs_dataTables = array_unique(array_merge($badcolattrs_dataTables, $badattrs));
                    }
                }
                if ($searchPresentation != 'AJAXSearchGrid') {
                    $badattrs = array_intersect(array_keys($col), $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_COLUMN_PARAMS);
                    if (!empty($badattrs)) {
                        $badcolattrs_AJAXSearchGrid = array_unique(array_merge($badcolattrs_AJAXSearchGrid, $badattrs));
                    }
                }
            }
            unset($col);
        }
        if (!empty($badcolattrs_dataTables)) {
            fprintf(STDERR, "WARNING: The following popup search / CRUD search column attributes are ignored when searchPresentation is not set to dataTables:\n    %s\n", implode("\n    ", $badcolattrs_dataTables));
        }
        if (!empty($badcolattrs_AJAXSearchGrid)) {
            fprintf(STDERR, "WARNING: The following popup search / CRUD search column attributes are ignored when searchPresentation is not set to AJAXSearchGrid:\n    %s\n", implode("\n    ", $badcolattrs_AJAXSearchGrid));
        }
        unset($badattrs, $badcolattrs_dataTables, $badcolattrs_AJAXSearchGrid);
        if (!isset($popupSearch['idColumn'])) {
            fprintf(STDERR, "Missing idColumn on popupSearch %s.  Skipping this popupSearch.\n", $popupSearchName);
            continue;
        }
        $idColumn = $popupSearch['idColumn'];
        $beforeSearchCallback = isset($popupSearch['beforeSearchCallback']) ? $popupSearch['beforeSearchCallback'] : '';
        $modifyURLCallback = isset($popupSearch['modifyURLCallback']) ? $popupSearch['modifyURLCallback'] : '';
        $afterSearchCallback = isset($popupSearch['afterSearchCallback']) ? $popupSearch['afterSearchCallback'] : '';
        $popupSearchCallbacks = '';
        if ($beforeSearchCallback != '') {
            $popupSearchCallbacks .= ",\n\t\tbeforeSearchCallback:{$beforeSearchCallback}";
        }
        if ($modifyURLCallback != '') {
            $popupSearchCallbacks .= ",\n\t\tmodifyURLCallback:{$modifyURLCallback}";
        }
        if ($afterSearchCallback != '') {
            $popupSearchCallbacks .= ",\n\t\tafterSearchCallback:{$afterSearchCallback}";
        }
        $rowSelectJavaScriptCallbackFunction = isset($popupSearch['rowSelectJavaScriptCallbackFunction']) ? $popupSearch['rowSelectJavaScriptCallbackFunction'] : '';
        if (!isset($popupSearch['columns']) || !is_array($popupSearch['columns'])) {
            fprintf(STDERR, "Missing or invalid columns list on popupSearch %s.  Skipping this popupSearch.\n", $popupSearchName);
            continue;
        }
        $popupSearchColumns = getDataTableColumns($popupSearch['columns'], 'popupSearch.' . $popupSearchName, $langKeys);
        $popupSearchColumnNames = is_array($popupSearch['columns']) ? array_keys($popupSearch['columns']) : array();
        if (isset($popupSearch['invisibleColumns']) && is_array($popupSearch['invisibleColumns'])) {
            foreach ($popupSearch['invisibleColumns'] as $icn) {
                if (!in_array($icn, $popupSearchColumnNames)) {
                    $popupSearchColumnNames[] = $icn;
                }
            }
        }
        $popupSearchColumnFilters = isset($popupSearch['columnFilters']) && is_array($popupSearch['columnFilters']) ? $popupSearch['columnFilters'] : array();
        $tmp = '{';
        $sep = '';
        foreach ($popupSearchColumnFilters as $funcname => $functext) {
            $tmp .= $sep . json_encode($funcname) . ':' . $functext;
            if ($sep == '') {
                $sep = ',';
            }
        }
        $tmp .= '}';
        $popupSearchColumnFilters = $tmp;
        unset($tmp);
        $popupSearchExtraQueryParams = isset($popupSearch['extraQueryParams']) && is_array($popupSearch['extraQueryParams']) ? (object) $popupSearch['extraQueryParams'] : (object) array();
        $popupSearchDefaultSorts = isset($popupSearch['defaultSorts']) && is_array($popupSearch['defaultSorts']) ? $popupSearch['defaultSorts'] : array();
        $actionsLangKey = 'popupSearch.' . $popupSearchName . '.actions';
        $actionsText = 'Actions';
        $langKeys[$actionsLangKey] = $actionsText;
        $selectLangKey = 'popupSearch.' . $popupSearchName . '.select';
        $selectText = 'Select';
        $langKeys[$selectLangKey] = $selectText;
        list($popupSearchGridHeaderColumnsHTML, $popupSearchGridBodyColumnsHTML) = getAJAXSearchGridColumns($popupSearch['columns'], 'popupSearch.' . $popupSearchName, $langKeys);
        $popupSearchGridHeaderColumnsHTML .= sprintf("<th><<langkey>>%s<</langkey>></th>\n", $actionsLangKey);
        $popupSearchGridBodyColumnsHTML .= sprintf("<td><a href=\"#\" ng-click=\"rowSelectLinkClicked(i, rows[i], \$event)\"><<langkey>>%s<</langkey>></a></td>\n", $selectLangKey);
        $popupSearchGridHeaderColumnsHTMLJSON = json_encode($popupSearchGridHeaderColumnsHTML);
        $popupSearchGridBodyColumnsHTMLJSON = json_encode($popupSearchGridBodyColumnsHTML);
        $srch = array();
        $repl = array();
        foreach ($langKeys as $langKey => $text) {
            $srch[] = '<<langkey>>' . $langKey . '<<\\/langkey>>';
            // special escaping, since we're working with JSON
            $repl[] = '"+_t(' . var_export_normal_precision($langKey, true) . ', ' . var_export_normal_precision($text, true) . ')+"';
        }
        $popupSearchGridHeaderColumnsHTMLJSON = str_replace($srch, $repl, $popupSearchGridHeaderColumnsHTMLJSON);
        $popupSearchGridBodyColumnsHTMLJSON = str_replace($srch, $repl, $popupSearchGridBodyColumnsHTMLJSON);
        $rowSelectCallbackCode = '';
        if ($rowSelectJavaScriptCallbackFunction != '') {
            // Note the '+ and +' around $idColumn.  This is intended to be part of
            // a JavaScript string contcatenation which is building the html variable
            // to be returned for the actions table column.
            $rowSelectCallbackCode .= "if (typeof({$rowSelectJavaScriptCallbackFunction}) == \\'function\\') { {$rowSelectJavaScriptCallbackFunction}('+{$idColumn}+'); }; ";
        }
        $rowSelectCode = "if (activePopupSearch != null) { activePopupSearch.rowSelected(activePopupSearch.dataTable.fnGetData('+oObj.iDataRow+')); }; " . $rowSelectCallbackCode . "if (activePopupSearch != null) { activePopupSearch.hide(); }; return false;";
        if ($popupSearchColumns != '') {
            $popupSearchColumns .= ",\n";
        }
        $popupSearchColumns .= "\t\t{ sName:'actions', sTitle:_t('" . $actionsLangKey . "', '" . $actionsText . "'), aTargets:[ci++], bSortable:false, bUseRendered:false, sType:'html', sClass:'center nowrap', fnRender:function(oObj) {\n" . "\t\t\tvar {$idColumn} = parseInt(oObj.aData[findDataTableColIdx({$popupSearchName}_aoColumnDefs, '{$idColumn}')]) || 0;\n" . "\t\t\tvar html = '<a href=\"#\" onclick=\"{$rowSelectCode}\">'+_t('" . $selectLangKey . "', '" . $selectText . "')+'</a>';\n" . "\t\t\treturn html;\n" . "\t\t}}\n";
        unset($rowSelectCallbackCode, $rowSelectCode);
        $popupSearchTableCallbacks = '';
        if (isset($popupSearch['fnDrawCallback']) && is_string($popupSearch['fnDrawCallback'])) {
            $s = trim($popupSearch['fnDrawCallback']);
            if ($s != '') {
                $popupSearchTableCallbacks .= ', fnDrawCallback: ' . $s;
            }
            unset($s);
        }
        if (isset($popupSearch['fnServerData']) && is_string($popupSearch['fnServerData'])) {
            $s = trim($popupSearch['fnServerData']);
            if ($s != '') {
                $popupSearchTableCallbacks .= ', fnServerData: ' . $s;
            }
            unset($s);
        }
        // Create the popup search JS file.
        $searchFor = array('{{generatedFileMessage}}', '{{popupSearchName}}', '{{popupSearchColumns}}', '{{popupSearchGridHeaderColumnsHTMLJSON}}', '{{popupSearchGridBodyColumnsHTMLJSON}}', '{{popupSearchColumnNamesJSON}}', '{{popupSearchColumnFilters}}', '{{popupSearchExtraQueryParamsJSON}}', '{{popupSearchDefaultSortsJSON}}', '{{popupSearchTableCallbacks}}', '{{searchCommand}}', '{{idColumn}}', '{{idColumnJSON}}', '{{popupSearchCallbacks}}', '{{rowSelectJavaScriptCallbackFunction}}', '{{rowSelectJavaScriptCallbackFunctionJSON}}');
        $replaceWith = array($generatedFileMessage, $popupSearchName, $popupSearchColumns, $popupSearchGridHeaderColumnsHTMLJSON, $popupSearchGridBodyColumnsHTMLJSON, json_encode($popupSearchColumnNames), $popupSearchColumnFilters, json_encode($popupSearchExtraQueryParams), json_encode($popupSearchDefaultSorts), $popupSearchTableCallbacks, $searchCommand, $idColumn, json_encode($idColumn), $popupSearchCallbacks, $rowSelectJavaScriptCallbackFunction, json_encode($rowSelectJavaScriptCallbackFunction));
        $content = str_replace($searchFor, $replaceWith, filterSearchPresentation(file_get_contents($templatesDir . '/' . $popupSearchTemplate), $searchPresentation));
        $fn = $outputDir . '/' . $popupSearchName . '.js';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if ($enableLangFiles) {
            $langfn = $outputDir . '/' . $popupSearchName . '.js.strings';
            $langResources = loadLangResourceFileContents($langfn);
            $anyResourcesChanged = false;
            foreach ($langKeys as $key => $val) {
                if (getLangResource($langResources, $key) != $val) {
                    setLangResource($langResources, $key, $val);
                    $anyResourcesChanged = true;
                }
            }
            if ($anyResourcesChanged) {
                saveLangResourceFileContents($langfn, $langResources);
            }
        }
    }
    // foreach ($cfg['popupSearches'] as $popupSearchName=>$popupSearch)
    // ---------------------------
    // Create the loader includes.
    // ---------------------------
    if (!isset($cfg['loaders']) || !is_array($cfg['loaders'])) {
        $cfg['loaders'] = array();
    }
    foreach ($cfg['loaders'] as $loaderName => $loader) {
        $outputPath = isset($loader['outputPath']) && is_string($loader['outputPath']) ? $loader['outputPath'] : '';
        if ($outputPath == '') {
            $outputPath = 'include/search';
        }
        $outputDir = $docroot . '/' . $outputPath;
        @mkdir($outputDir, 0777, true);
        $numDirsDeepUnderHTML = calcDirDepth($outputPath);
        if (isset($loader['docRootPath'])) {
            $docRootPath = '"' . addcslashes($loader['docRootPath']) . '"';
        } else {
            $docRootPath = 'dirname(__FILE__)';
            for ($i = 0; $i < $numDirsDeepUnderHTML; $i++) {
                $docRootPath = "dirname({$docRootPath})";
            }
        }
        $phpIncludes = getPHPClassesAndIncludes($loader, $numDirsDeepUnderHTML);
        $searchCommand = isset($loader['searchCommand']) && trim($loader['searchCommand']) != '' ? trim($loader['searchCommand']) : 'load' . ucfirst($loaderName);
        $loaderTemplate = isset($loader['loaderTemplate']) && is_string($loader['loaderTemplate']) ? $loader['loaderTemplate'] : '';
        if ($loaderTemplate == '') {
            $loaderTemplate = 'load.include.php';
        }
        $idColumn = isset($loader['idColumn']) && is_string($loader['idColumn']) ? $loader['idColumn'] : $idCol;
        $idColumnPSType = isset($loader['idColumnPSType']) && is_string($loader['idColumnPSType']) ? $loader['idColumnPSType'] : 'int';
        if (!in_array($idColumnPSType, $ALLOWED_PS_TYPES)) {
            $idColumnPSType = 'int';
        }
        switch ($idColumnPSType) {
            case 'boolean':
            case 'int':
                $getIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (int)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                if ($idColumnPSType == 'boolean') {
                    $emptyIdCheck = sprintf('($%s < 0)', $idColumn);
                } else {
                    $emptyIdCheck = sprintf('($%s <= 0)', $idColumn);
                }
                break;
            case 'float':
                $getIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (float)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                $emptyIdCheck = sprintf('($%s <= 0.0)', $idColumn);
                break;
            case 'double':
                $getIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (double)trim(\$params['%s']) : 0;", $idColumn, $idColumn, $idColumn);
                $emptyIdCheck = sprintf('($%s <= 0.0)', $idColumn);
                break;
            case 'string':
            case 'match':
            case 'binary':
            default:
                $getIdParam = sprintf("\t\$%s = isset(\$params['%s']) ? (string)\$params['%s'] : '';", $idColumn, $idColumn, $idColumn);
                $emptyIdCheck = sprintf("(\$%s == '')", $idColumn);
                break;
        }
        $andWhere = '';
        if (isset($loader['andWhere'])) {
            $andWhere = trim((string) $loader['andWhere']);
        }
        $andWhereAssignments = array();
        if (isset($loader['andWhereAssignments']) && is_array($loader['andWhereAssignments'])) {
            foreach ($loader['andWhereAssignments'] as $assignment) {
                $expression = isset($assignment['expression']) && is_string($assignment['expression']) ? $assignment['expression'] : "''";
                switch (isset($assignment['psType']) ? $assignment['psType'] : 'string') {
                    case 'boolean':
                        $andWhereAssignments[] = "\t\$ps->setBoolean({$expression});";
                        break;
                    case 'int':
                        $andWhereAssignments[] = "\t\$ps->setInt({$expression});";
                        break;
                    case 'float':
                        $andWhereAssignments[] = "\t\$ps->setFloat({$expression});";
                        break;
                    case 'double':
                        $andWhereAssignments[] = "\t\$ps->setDouble({$expression});";
                        break;
                    case 'string':
                    default:
                        $andWhereAssignments[] = "\t\$ps->setString({$expression});";
                        break;
                    case 'match':
                        $andWhereAssignments[] = "\t\$ps->setString('%'.{$expression}.'%');";
                        break;
                    case 'binary':
                        $andWhereAssignments[] = "\t\$ps->setBinary({$expression});";
                        break;
                }
            }
        }
        $initRelationDAOs = array();
        $loadRelations = array();
        $psidx = 0;
        $psInitCode = array();
        if (isset($loader['relations'])) {
            foreach ($loader['relations'] as $relationName => $relation) {
                $rtn = $relation['table'];
                $urtn = ucfirst($rtn);
                $useDAO = !isset($relation['useDAO']) || $relation['useDAO'] ? true : false;
                if ($useDAO || !isset($relation['sqlQuery'])) {
                    $initRelationDAOs[] = "\t\${$rtn}DAO = new {$urtn}DAO(\$db);";
                }
                $relationType = isset($relation['relationType']) ? $relation['relationType'] : 'many';
                if ($relationType != 'one' && $relationType != 'many') {
                    $relationType = 'many';
                }
                $offset = isset($relation['offset']) ? (int) $relation['offset'] : 0;
                if ($offset < 0) {
                    $offset = 0;
                }
                if ($relationType == 'one') {
                    $limit = 1;
                } else {
                    $limit = isset($relation['limit']) ? (int) $relation['limit'] : 0;
                    if ($limit < 0) {
                        $limit = 0;
                    }
                }
                $code = '';
                if (isset($relation['sqlQuery'])) {
                    // Retrieve the row(s) using an SQL query.
                    $psidx++;
                    $psInitCode[] = "\t\$ps{$psidx} = new PreparedStatement(<<<EOF\n{$relation['sqlQuery']}\nEOF\n\t, {$offset}, {$limit});\n";
                    $code .= "\t\t\$ps{$psidx}->clearParams();\n";
                    if (isset($relation['sqlQueryAssignments']) && is_array($relation['sqlQueryAssignments'])) {
                        foreach ($relation['sqlQueryAssignments'] as $assignment) {
                            $expression = isset($assignment['expression']) && is_string($assignment['expression']) ? $assignment['expression'] : "''";
                            switch (isset($assignment['psType']) ? $assignment['psType'] : 'string') {
                                case 'boolean':
                                    $code .= "\t\t\$ps{$psidx}->setBoolean({$expression});\n";
                                    break;
                                case 'int':
                                    $code .= "\t\t\$ps{$psidx}->setInt({$expression});\n";
                                    break;
                                case 'float':
                                    $code .= "\t\t\$ps{$psidx}->setFloat({$expression});\n";
                                    break;
                                case 'double':
                                    $code .= "\t\t\$ps{$psidx}->setDouble({$expression});\n";
                                    break;
                                case 'string':
                                default:
                                    $code .= "\t\t\$ps{$psidx}->setString({$expression});\n";
                                    break;
                                case 'match':
                                    $code .= "\t\t\$ps{$psidx}->setString('%'.{$expression}.'%');\n";
                                    break;
                                case 'binary':
                                    $code .= "\t\t\$ps{$psidx}->setBinary({$expression});\n";
                                    break;
                            }
                        }
                    }
                    $code .= "\t\t";
                    if ($relationType == 'one') {
                        $code .= '$relRows = ' . $emptyIdCheck . ' ? array() : ';
                    } else {
                        $code .= '$row->' . $relationName . ' = ' . $emptyIdCheck . ' ? array() : ';
                    }
                    if ($useDAO) {
                        $code .= '$' . $rtn . "DAO->findWithPreparedStatement(\$ps{$psidx});\n";
                    } else {
                        $code .= "\$db->fetchAllObjects(\$db->executeQuery(\$ps{$psidx}), true);\n";
                    }
                    if ($relationType == 'one') {
                        $code .= "\t\t\$row->{$relationName} = empty(\$relRows) ? null : \$relRows[0];";
                        $code .= "\t\tunset(\$relRows);";
                    }
                } else {
                    $queryOperator = isset($relation['queryOperator']) ? $relation['queryOperator'] : '=';
                    if (!in_array($queryOperator, $ALLOWED_QUERY_OPERATORS)) {
                        $queryOperator = '=';
                    }
                    $local = isset($relation['local']) ? $relation['local'] : '';
                    if ($local == '') {
                        continue;
                    }
                    $foreign = isset($relation['foreign']) ? $relation['foreign'] : '';
                    if ($foreign == '') {
                        continue;
                    }
                    $orderBy = isset($relation['orderBy']) ? $relation['orderBy'] : '';
                    if ($orderBy == '') {
                        $orderBy = $foreign;
                    }
                    if (!$useDAO) {
                        $psidx++;
                        $code .= '\\t\\t$ps{$psidx} = ' . $rtn . 'DAO->findBy' . ucfirst($foreign) . 'PS($row->' . $local . ', \'' . $queryOperator . '\', \'' . $orderBy . '\', ' . $offset . ', ' . $limit . ');';
                    }
                    $code .= "\t\t";
                    if ($relationType == 'one') {
                        $code .= '$relRows = ' . $emptyIdCheck . ' ? array() : ';
                    } else {
                        $code .= '$row->' . $relationName . ' = ' . $emptyIdCheck . ' ? array() : ';
                    }
                    if ($useDAO) {
                        $code .= '$' . $rtn . 'DAO->findBy' . ucfirst($foreign) . '($row->' . $local . ', \'' . $queryOperator . '\', \'' . $orderBy . '\', ' . $offset . ', ' . $limit . ');';
                    } else {
                        $code .= "\${$db->fetchAllObjects}(\$db->executeQuery(\$ps{$psidx}), true);\n";
                    }
                    if ($relationType == 'one') {
                        $code .= "\n\t\t\$row->{$relationName} = empty(\$relRows) ? null : \$relRows[0];";
                        $code .= "\n\t\tunset(\$relRows);";
                    }
                }
                if (empty($loadRelations)) {
                    $loadRelations[] = "\tforeach (\$rows as &\$row) {";
                }
                $loadRelations[] = $code;
            }
            if (!empty($loadRelations)) {
                $loadRelations[] = "\t}\n\tunset(\$row);";
            }
            if (!empty($psInitCode)) {
                $loadRelations = array_merge($psInitCode, $loadRelations);
            }
        }
        $rowProcessingPHPCode = '';
        if (isset($loader['rowProcessingPHPCode'])) {
            $rowProcessingPHPCode = (string) $loader['rowProcessingPHPCode'];
            if (trim($rowProcessingPHPCode) != '') {
                $rowProcessingPHPCode = <<<EOF
\tforeach (\$rows as &\$row) {
{$rowProcessingPHPCode}
\t}
\tunset(\$row); // release reference to last element

EOF;
            } else {
                $rowProcessingPHPCode = '';
            }
        }
        $unsetForbiddenColumns = array();
        if (isset($loader['forbiddenColumns'])) {
            foreach ($loader['forbiddenColumns'] as $colName) {
                if (empty($unsetForbiddenColumns)) {
                    $unsetForbiddenColumns[] = "\tforeach (\$rows as &\$row) {";
                }
                $unsetForbiddenColumns[] = "\t\tunset(\$row->{$colName});";
            }
            if (!empty($unsetForbiddenColumns)) {
                $unsetForbiddenColumns[] = "\t}\n\tunset(\$row);";
            }
        }
        // Create the loader include.
        $searchFor = array('{{generatedFileMessage}}', '{{docRootPath}}', '{{jaxInclude}}', '{{jaxJQuery}}', '{{jaxJS}}', '{{phpIncludes}}', '{{loaderName}}', '{{uLoaderName}}', '{{tableName}}', '{{uTableName}}', '{{searchCommand}}', '{{idCol}}', '{{uIdCol}}', '{{idColumnPSType}}', '{{uIdColumnPSType}}', '{{getIdParam}}', '{{emptyIdCheck}}', '{{andWhere}}', '{{andWhereAssignments}}', '{{initRelationDAOs}}', '{{loadRelations}}', '{{rowProcessingPHPCode}}', '{{unsetForbiddenColumns}}');
        $replaceWith = array($generatedFileMessage, $docRootPath, $jaxInclude, $jaxJQuery, $jaxJS, $phpIncludes, $loaderName, ucfirst($loaderName), $table->tableName, ucfirst($table->tableName), $searchCommand, $idColumn, ucfirst($idColumn), $idColumnPSType, ucfirst($idColumnPSType), $getIdParam, $emptyIdCheck, $andWhere != '' ? ' and (' . $andWhere . ')' : '', implode("\n", $andWhereAssignments), implode("\n", $initRelationDAOs), !empty($loadRelations) ? implode("\n", $loadRelations) : '', $rowProcessingPHPCode, implode("\n", $unsetForbiddenColumns));
        $content = str_replace($searchFor, $replaceWith, file_get_contents($templatesDir . '/' . $loaderTemplate));
        $fn = $outputDir . '/' . $loaderName . '_load.include.php';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
    }
    return true;
}
Пример #2
0
function processTable($table, $idCol, $cfg)
{
    global $SEARCH_PRESENTATION_DATATABLES_ONLY_PARAMS, $SEARCH_PRESENTATION_DATATABLES_ONLY_COLUMN_PARAMS, $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_PARAMS, $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_COLUMN_PARAMS, $templatesDir, $cfgDir, $docroot, $oldFileLayout, $enableLangFiles;
    $generatedFileMessage = <<<EOF
// DO NOT EDIT THIS FILE.
// This file was generated by crudgen.
// If you need to customize this file, please edit the corresponding
// yaml file in the gencfg directory, and then re-generate this file
// by running crudgen, passing in the table name.
EOF;
    $mainOkCheck = $oldFileLayout ? '' : "\nif ((!isset(\$mainOk)) || (!\$mainOk)) exit();\n";
    $jaxInclude = $cfg['jaxInclude'];
    $jaxJQuery = $cfg['jaxJQuery'];
    $jaxJS = $cfg['jaxJS'];
    $classAutoloadPaths = isset($cfg['classAutoloadPaths']) ? trim($cfg['classAutoloadPaths']) : '';
    $loggedInId = isset($cfg['loggedInId']) ? trim($cfg['loggedInId']) : '';
    if ($loggedInId == '') {
        $loggedInId = '$loggedInUser->id';
    }
    if ($classAutoloadPaths != '') {
        $classAutoloadPathsInit = sprintf("\$__CLASS_AUTO_LOAD_CLASS_PATHS = %s;\n", $classAutoloadPaths);
    } else {
        $classAutoloadPathsInit = '';
    }
    if (!isset($cfg['cruds']) || !is_array($cfg['cruds'])) {
        $cfg['cruds'] = array();
    }
    foreach ($cfg['cruds'] as $crudName => $crud) {
        $mainHooksInclude = $oldFileLayout ? "@include './{$crudName}_hooks.include.php';\n" : '';
        $viewHooksInclude = $oldFileLayout ? "@include './{$crudName}_view_hooks.include.php';\n" : '';
        if ($oldFileLayout) {
            $controllerIncludes = <<<EOF
\t\$headerScripts[] = str_replace('//', '/', dirname(\$_SERVER['SCRIPT_NAME']).'/{$crudName}_controller.js');
\tif (file_exists(dirname(__FILE__).'/{$crudName}_controller_hooks.js')) {
\t\t\$headerScripts[] = str_replace('//', '/', dirname(\$_SERVER['SCRIPT_NAME']).'/{$crudName}_controller_hooks.js');
\t}

EOF;
        } else {
            $controllerIncludes = <<<EOF
\t\$headerScripts[] = str_replace('//', '/', dirname(\$_SERVER['SCRIPT_NAME']).'/generated/{$crudName}_controller_generated.js');
\tif (file_exists(dirname(dirname(__FILE__)).'/{$crudName}_controller.js')) {
\t\t\$headerScripts[] = str_replace('//', '/', dirname(\$_SERVER['SCRIPT_NAME']).'/{$crudName}_controller.js');
\t}

EOF;
        }
        $langKeys = array();
        if (array_key_exists('tableDescription', $cfg)) {
            $langKeys['crud.' . $crudName . '.tableDescription'] = $cfg['tableDescription'];
        }
        if (array_key_exists('tableDescriptions', $cfg)) {
            $langKeys['crud.' . $crudName . '.tableDescriptions'] = $cfg['tableDescriptions'];
        }
        $outputPath = isset($crud['outputPath']) && is_string($crud['outputPath']) ? $crud['outputPath'] : '';
        $outputDir = $docroot . '/' . $outputPath;
        $generatedOutputDir = $oldFileLayout ? $outputDir : $outputDir . '/generated';
        @mkdir($generatedOutputDir, 0777, true);
        $numDirsDeepUnderHTML = calcDirDepth($outputPath) + ($oldFileLayout ? 0 : 1);
        if (isset($crud['docRootPath'])) {
            $docRootPath = '"' . addcslashes($crud['docRootPath']) . '"';
        } else {
            $docRootPath = 'dirname(__FILE__)';
            for ($i = 0; $i < $numDirsDeepUnderHTML; $i++) {
                $docRootPath = "dirname({$docRootPath})";
            }
        }
        $phpIncludes = getPHPClassesAndIncludes($crud, $numDirsDeepUnderHTML);
        $postInitPHPIncludes = getPostInitPHPIncludes($crud, $numDirsDeepUnderHTML);
        $crudSearchCommand = 'search' . ucfirst($table->tableName) . 's';
        $crudSearchPresentation = 'dataTables';
        $crudSearchTableDisplayColumns = '';
        /// TODO: Build these two up based on the column definitions.
        $crudSearchGridHeaderColumnsHTML = '';
        $crudSearchGridBodyColumnsHTML = '';
        $crudSearchColumnNames = array();
        $crudSearchTableCallbacks = '';
        if (isset($crud['crudSearch']) && is_array($crud['crudSearch'])) {
            $crudSearch = $crud['crudSearch'];
            // If we encounter a "likePopupSearch" attribute for this crudSearch, it must
            // reference an existing popupSearch defined in the same YAML file.
            // When this happens, we merge the popupSearch's attributes with the crudSearch's
            // attributes, allowing the crudSearch's attributes to override the popupSearch's
            // attributes of the same name.
            if (isset($crudSearch['likePopupSearch'])) {
                if (!is_string($crudSearch['likePopupSearch']) || !isset($cfg['popupSearches']) || !isset($cfg['popupSearches'][$crudSearch['likePopupSearch']]) || !is_array($cfg['popupSearches'][$crudSearch['likePopupSearch']])) {
                    fprintf(STDERR, "Invalid likePopupSearch entry in crudSearch section.\n");
                    return false;
                }
                $crudSearch = array_merge($cfg['popupSearches'][$crudSearch['likePopupSearch']], $crudSearch);
            }
            if (isset($crudSearch['searchCommand']) && is_string($crudSearch['searchCommand'])) {
                $crudSearchCommand = $crudSearch['searchCommand'];
            }
            if (isset($crudSearch['searchPresentation']) && is_string($crudSearch['searchPresentation'])) {
                $crudSearchPresentation = $crudSearch['searchPresentation'];
                if ($crudSearchPresentation != 'dataTables' && $crudSearchPresentation != 'AJAXSearchGrid') {
                    $crudSearchPresentation = 'dataTables';
                }
            }
            if ($crudSearchPresentation != 'dataTables') {
                $badattrs = array_intersect(array_keys($crudSearch), $SEARCH_PRESENTATION_DATATABLES_ONLY_PARAMS);
                if (!empty($badattrs)) {
                    fprintf(STDERR, "WARNING: The following popup search / CRUD search attributes are ignored when searchPresentation is not set to dataTables:\n    %s\n", implode("\n    ", $badattrs));
                }
            }
            if ($crudSearchPresentation != 'AJAXSearchGrid') {
                $badattrs = array_intersect(array_keys($crudSearch), $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_PARAMS);
                if (!empty($badattrs)) {
                    fprintf(STDERR, "WARNING: The following popup search / CRUD search attributes are ignored when searchPresentation is not set to AJAXSearchGrid:\n    %s\n", implode("\n    ", $badattrs));
                }
            }
            $badcolattrs_dataTables = array();
            $badcolattrs_AJAXSearchGrid = array();
            if (isset($crudSearch['columns']) && is_array($crudSearch['columns'])) {
                foreach ($crudSearch['columns'] as $col) {
                    if ($crudSearchPresentation != 'dataTables') {
                        $badattrs = array_intersect(array_keys($col), $SEARCH_PRESENTATION_DATATABLES_ONLY_COLUMN_PARAMS);
                        if (!empty($badattrs)) {
                            $badcolattrs_dataTables = array_unique(array_merge($badcolattrs_dataTables, $badattrs));
                        }
                    }
                    if ($crudSearchPresentation != 'AJAXSearchGrid') {
                        $badattrs = array_intersect(array_keys($col), $SEARCH_PRESENTATION_AJAXSEARCHGRID_ONLY_COLUMN_PARAMS);
                        if (!empty($badattrs)) {
                            $badcolattrs_AJAXSearchGrid = array_unique(array_merge($badcolattrs_AJAXSearchGrid, $badattrs));
                        }
                    }
                }
                unset($col);
            }
            if (!empty($badcolattrs_dataTables)) {
                fprintf(STDERR, "WARNING: The following popup search / CRUD search column attributes are ignored when searchPresentation is not set to dataTables:\n    %s\n", implode("\n    ", $badcolattrs_dataTables));
            }
            if (!empty($badcolattrs_AJAXSearchGrid)) {
                fprintf(STDERR, "WARNING: The following popup search / CRUD search column attributes are ignored when searchPresentation is not set to AJAXSearchGrid:\n    %s\n", implode("\n    ", $badcolattrs_AJAXSearchGrid));
            }
            unset($badattrs, $badcolattrs_dataTables, $badcolattrs_AJAXSearchGrid);
            $beforeSearchCallback = isset($crudSearch['beforeSearchCallback']) ? $crudSearch['beforeSearchCallback'] : '';
            $modifyURLCallback = isset($crudSearch['modifyURLCallback']) ? $crudSearch['modifyURLCallback'] : '';
            $afterSearchCallback = isset($crudSearch['afterSearchCallback']) ? $crudSearch['afterSearchCallback'] : '';
            $crudSearchCallbacks = '';
            if ($beforeSearchCallback != '') {
                $crudSearchCallbacks .= ",\n\t\t\tbeforeSearchCallback:{$beforeSearchCallback}";
            }
            if ($modifyURLCallback != '') {
                $crudSearchCallbacks .= ",\n\t\t\tmodifyURLCallback:{$modifyURLCallback}";
            }
            if ($afterSearchCallback != '') {
                $crudSearchCallbacks .= ",\n\t\t\tafterSearchCallback:{$afterSearchCallback}";
            }
            if (!isset($crudSearch['columns']) || !is_array($crudSearch['columns'])) {
                fprintf(STDERR, "Missing or invalid columns list on crudSearch for %s CRUD.  Skipping this CRUD.\n", $crudSearchName);
                continue;
            }
            $crudSearchTableDisplayColumns = getDataTableColumns($crudSearch['columns'], 'crud.' . $crudName, $langKeys);
            if ($crudSearchTableDisplayColumns != '') {
                $crudSearchTableDisplayColumns .= ",\n";
            }
            $crudSearchColumnNames = is_array($crudSearch['columns']) ? array_keys($crudSearch['columns']) : array();
            if (isset($crudSearch['invisibleColumns']) && is_array($crudSearch['invisibleColumns'])) {
                foreach ($crudSearch['invisibleColumns'] as $icn) {
                    if (!in_array($icn, $crudSearchColumnNames)) {
                        $crudSearchColumnNames[] = $icn;
                    }
                }
            }
            $crudSearchColumnFilters = isset($crudSearch['columnFilters']) && is_array($crudSearch['columnFilters']) ? $crudSearch['columnFilters'] : array();
            $tmp = '{';
            $sep = '';
            foreach ($crudSearchColumnFilters as $funcname => $functext) {
                $tmp .= $sep . json_encode($funcname) . ':' . $functext;
                if ($sep == '') {
                    $sep = ',';
                }
            }
            $tmp .= '}';
            $crudSearchColumnFilters = $tmp;
            unset($tmp);
            $crudSearchExtraQueryParams = isset($crudSearch['extraQueryParams']) && is_array($crudSearch['extraQueryParams']) ? (object) $crudSearch['extraQueryParams'] : (object) array();
            list($crudSearchGridHeaderColumnsHTML, $crudSearchGridBodyColumnsHTML) = getAJAXSearchGridColumns($crudSearch['columns'], 'crud.' . $crudName, $langKeys);
            $srch = array();
            $repl = array();
            foreach ($langKeys as $langKey => $text) {
                $srch[] = '<<langkey>>' . $langKey . '<</langkey>>';
                $repl[] = '<?php _e(' . var_export_normal_precision($langKey, true) . ', ' . var_export_normal_precision($text, true) . '); ?' . '>';
            }
            $crudSearchGridHeaderColumnsHTML = str_replace($srch, $repl, $crudSearchGridHeaderColumnsHTML);
            $crudSearchGridBodyColumnsHTML = str_replace($srch, $repl, $crudSearchGridBodyColumnsHTML);
            $crudSearchDefaultSorts = isset($crudSearch['defaultSorts']) && is_array($crudSearch['defaultSorts']) ? $crudSearch['defaultSorts'] : array();
            if (isset($crudSearch['fnDrawCallback']) && is_string($crudSearch['fnDrawCallback'])) {
                $s = trim($crudSearch['fnDrawCallback']);
                if ($s != '') {
                    $crudSearchTableCallbacks .= ', fnDrawCallback: ' . $s;
                }
                unset($s);
            }
            if (isset($crudSearch['fnServerData']) && is_string($crudSearch['fnServerData'])) {
                $s = trim($crudSearch['fnServerData']);
                if ($s != '') {
                    $crudSearchTableCallbacks .= ', fnServerData: ' . $s;
                }
                unset($s);
            }
        }
        $ajaxAutocompleteInitJS = '';
        $ajaxComboboxInitJS = '';
        $openContainers = array();
        $hiddenFormFields = '';
        $formFields = '';
        if (!isset($crud['formFields']) || !is_array($crud['formFields'])) {
            $crud['formFields'] = array();
        }
        foreach ($crud['formFields'] as $fieldName => $field) {
            $thisFormField = '';
            $inputType = isset($field['inputType']) ? $field['inputType'] : 'text';
            if ($inputType == 'select' && isset($field['multiple']) && $field['multiple'] && substr($fieldName, -2) != '[]') {
                fprintf(STDERR, "WARNING: For inputType: select, multiple: Yes, input name must end with '[]' in input \"%s\" in formFields section.\n", $fieldName);
            }
            $id = isset($field['id']) ? $field['id'] : $fieldName;
            if (substr($id, -2) == '[]') {
                $id = trim(substr($id, 0, strlen($id) - 2));
            }
            $titleLangKey = 'crud.' . $crudName . '.form.input.' . $id . '.label';
            $title = isset($field['title']) ? $field['title'] : ucwords(str_replace('_', ' ', $id));
            $langKeys[$titleLangKey] = $title;
            $titleExpr = "_t('" . $titleLangKey . "', " . var_export_normal_precision($title, true) . ")";
            if (($placeholder = isset($field['placeholder']) ? $field['placeholder'] : '') != '') {
                $placeholderLangKey = 'crud.' . $crudName . '.form.input.' . $id . '.placeholder';
                $langKeys[$placeholderLangKey] = $placeholder;
                $placeholderExpr = "<?php _e('" . $placeholderLangKey . "', " . var_export_normal_precision($placeholder, true) . "); ?>";
            } else {
                $placeholderLangKey = '';
                $placeholderExpr = '';
            }
            $onclickTag = isset($field['onclick']) && $field['onclick'] != '' ? ' onclick="' . $field['onclick'] . '"' : '';
            $onchangeTag = isset($field['onchange']) && $field['onchange'] != '' ? ' onchange="' . $field['onchange'] . '"' : '';
            $containerCSSClass = isset($field['containerCSSClass']) && $field['containerCSSClass'] != '' ? trim($field['containerCSSClass']) : '';
            $cssClass = isset($field['cssClass']) && $field['cssClass'] != '' ? trim($field['cssClass']) : '';
            $helpTopic = isset($field['helpTopic']) && $field['helpTopic'] != '' ? trim($field['helpTopic']) : '';
            $onPopupSearch = isset($field['onPopupSearch']) ? trim($field['onPopupSearch']) : '';
            $descriptionField = isset($field['descriptionField']) ? $field['descriptionField'] : '';
            $descriptionFieldSize = isset($field['descriptionFieldSize']) ? (int) $field['descriptionFieldSize'] : 0;
            if ($descriptionFieldSize <= 0) {
                $descriptionFieldSize = 40;
            }
            $descriptionFieldMaxLength = isset($field['descriptionFieldMaxLength']) ? (int) $field['descriptionFieldMaxLength'] : 0;
            if ($descriptionFieldMaxLength < 0) {
                $descriptionFieldMaxLength = 0;
            }
            if ($onPopupSearch != '') {
                if (substr($onPopupSearch, strlen($onPopupSearch) - 1) != ';') {
                    $onPopupSearch .= ';';
                }
                $onPopupSearch .= 'return false;';
                // Default popup search icon (controlled by CSS).
                $searchDescHTML = '<a class="btn btn-default popupSearchLink" href="#" onclick="' . $onPopupSearch . '"><i class="glyphicon glyphicon-search"></i></a>';
            } else {
                $searchDescHTML = '';
            }
            if ($descriptionField != '') {
                $descriptionClassTag = getBootstrapTextInputSizeClass($descriptionFieldSize);
                $descriptionClassTag = trim('form-control ' . $descriptionClassTag);
                if ($descriptionClassTag != '') {
                    $descriptionClassTag = ' class="' . $descriptionClassTag . '"';
                }
                $searchDescHTML .= '&nbsp;-&nbsp;<input type="text" name="' . $descriptionField . '" id="' . $descriptionField . '"' . $descriptionClassTag . ($descriptionFieldSize > 0 ? ' size="' . $descriptionFieldSize . '"' : '') . ($descriptionFieldMaxLength > 0 ? ' maxlength="' . $descriptionFieldMaxLength . '"' : '') . ' readonly="readonly" disabled="disabled"/>';
            }
            // For inputs which use label-before-input layout, open the row and emit the
            // first cell containing the label.  For checkboxes, open the row and emit a
            // blank cell.  In both cases, open the second cell so we're ready to emit
            // the input element.
            switch ($inputType) {
                case 'text':
                case 'password':
                case 'textarea':
                case 'select':
                case 'file':
                case 'radio':
                    $containerCSSClass = trim('form-group ' . $containerCSSClass);
                    $thisFormField .= '<div class="' . $containerCSSClass . '" id="' . $id . 'TR">' . '<label for="' . $id . '" class="col-sm-2 control-label"><?php echo ' . $titleExpr . '; ?>:</label>' . '<div class="col-sm-10">' . '<div id="fieldErrorMsg_' . $id . '" class="fieldErrorMsg"></div>';
                    break;
                case 'checkbox':
                    $containerCSSClass = trim('form-group ' . $containerCSSClass);
                    $thisFormField .= '<div class="' . $containerCSSClass . '" id="' . $id . 'TR">' . '<label class="col-sm-2 control-label"></label>' . '<div class="col-sm-10">' . '<div id="fieldErrorMsg_' . $id . '" class="fieldErrorMsg"></div>' . '<label for="' . $id . '">';
                    break;
            }
            switch ($inputType) {
                case 'hidden':
                    $hiddenFormFields .= '<input type="hidden" name="' . $fieldName . '" id="' . $id . '"/>' . "\n";
                    break;
                case 'text':
                case 'password':
                    $size = isset($field['size']) ? (int) $field['size'] : 0;
                    $maxlength = isset($field['maxlength']) ? (int) $field['maxlength'] : 0;
                    $cssClass = trim('form-control ' . $cssClass);
                    $cssClass = mergeSizeClass($cssClass, getBootstrapTextInputSizeClass($size));
                    $readonlyTag = isset($field['readonly']) && $field['readonly'] ? ' readonly="readonly"' : '';
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    $thisFormField .= '<input type="' . $inputType . '" name="' . $fieldName . '" id="' . $id . '"' . ($size > 0 ? ' size="' . $size . '"' : '') . ($maxlength > 0 ? ' maxlength="' . $maxlength . '"' : '') . ($placeholderExpr != '' ? " placeholder=\"{$placeholderExpr}\"" : '') . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $readonlyTag . $disabledTag . $onclickTag . $onchangeTag . '/>' . $searchDescHTML;
                    break;
                case 'textarea':
                    $rows = isset($field['rows']) ? (int) $field['rows'] : 20;
                    $cols = isset($field['cols']) ? (int) $field['cols'] : 20;
                    $cssClass = trim('form-control ' . $cssClass);
                    $cssClass = mergeSizeClass($cssClass, getBootstrapTextInputSizeClass($cols));
                    $readonlyTag = isset($field['readonly']) && $field['readonly'] ? ' readonly="readonly"' : '';
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    $thisFormField .= '<textarea name="' . $fieldName . '" id="' . $id . '" rows="' . $rows . '" cols="' . $cols . '"' . ($placeholderExpr != '' ? " placeholder=\"{$placeholderExpr}\"" : '') . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $readonlyTag . $disabledTag . $onclickTag . $onchangeTag . '></textarea>' . $searchDescHTML;
                    break;
                case 'select':
                    $size = isset($field['size']) ? (int) $field['size'] : 0;
                    $cssClass = trim('form-control ' . $cssClass);
                    $multipleTag = isset($field['multiple']) && $field['multiple'] ? ' multiple="multiple"' : '';
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    $thisFormField .= '<select' . $multipleTag . ' name="' . $fieldName . '" id="' . $id . '"' . ($size > 0 ? ' size="' . $size . '"' : '') . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $disabledTag . $onclickTag . $onchangeTag . ">\n";
                    if (isset($field['optionsFromAssociativeArray']) && is_string($field['optionsFromAssociativeArray']) && $field['optionsFromAssociativeArray'] != '') {
                        $thisFormField .= '<?php foreach (' . $field['optionsFromAssociativeArray'] . ' as $__val=>$__desc) { ?' . ">\n" . ' <option value="<?php echo htmlspecialchars($__val); ?' . '>">' . '<?php echo htmlspecialchars($__desc); ?' . '>' . "</option>\n" . '<?php } ?' . ">\n";
                    } else {
                        $options = isset($field['options']) && is_array($field['options']) ? $field['options'] : array();
                        foreach ($options as $option => $optionParams) {
                            $optionTitleLangKey = 'crud.' . $crudName . '.form.input.' . $id . '.option.' . optionToName($option) . '.title';
                            $title = isset($optionParams['title']) ? $optionParams['title'] : $option;
                            $langKeys[$optionTitleLangKey] = $title;
                            $optionTitleExpr = "_t('" . $optionTitleLangKey . "', " . var_export_normal_precision($title, true) . ")";
                            $thisFormField .= '     <option value="' . htmlspecialchars($option) . '">' . '<?php echo ' . $optionTitleExpr . '; ?>' . "</option>\n";
                        }
                    }
                    $thisFormField .= '</select>' . $searchDescHTML;
                    break;
                case 'file':
                    $cssClass = trim('form-control ' . $cssClass);
                    $readonlyTag = isset($field['readonly']) && $field['readonly'] ? ' readonly="readonly"' : '';
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    $acceptTag = isset($field['accept']) ? trim($field['accept']) : '';
                    if ($acceptTag != '') {
                        $acceptTag = " accept=\"{$acceptTag}\"";
                    }
                    $thisFormField .= '<input type="' . $inputType . '" name="' . $fieldName . '" id="' . $id . '"' . $acceptTag . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $readonlyTag . $disabledTag . $onclickTag . $onchangeTag . '/>' . $searchDescHTML;
                    break;
                case 'checkbox':
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    $value = isset($field['value']) ? $field['value'] : '1';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    $thisFormField .= '<input type="' . $inputType . '" name="' . $fieldName . '" id="' . $id . '"' . ' value="' . $value . '"' . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $disabledTag . $onclickTag . $onchangeTag . '/>' . ' <?php echo ' . $titleExpr . '; ?></label>' . $searchDescHTML;
                    break;
                case 'radio':
                    $disabledTag = isset($field['disabled']) && $field['disabled'] ? ' disabled="disabled"' : '';
                    // Disabling the id field causes updates to become inserts.  Don't allow that.
                    if ($fieldName == $idCol) {
                        $disabledTag = '';
                    }
                    if (isset($field['optionsFromAssociativeArray']) && is_string($field['optionsFromAssociativeArray']) && $field['optionsFromAssociativeArray'] != '') {
                        $thisFormField .= '<?php $__i = 0;' . "\n" . 'foreach (' . $field['optionsFromAssociativeArray'] . ' as $__val=>$__desc) { ?' . ">\n\t" . '$__i++;' . "\n" . '<div><input type="radio" name="' . $fieldName . '" id="' . $id . '__<?php echo $__i; ?>" value="<?php echo htmlspecialchars($__val); ?' . '>"' . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $disabledTag . '/>' . '<label for="' . $id . '__<?php echo $__i; ?>"><?php echo htmlspecialchars($__desc); ?' . '>' . "</label></div>\n" . '<?php } ?' . ">\n";
                    } else {
                        $options = isset($field['options']) && is_array($field['options']) ? $field['options'] : array();
                        $__i = 0;
                        foreach ($options as $option => $optionParams) {
                            $__i++;
                            $optionTitleLangKey = 'crud.' . $crudName . '.form.input.' . $id . '.option.' . optionToName($option) . '.title';
                            $title = isset($optionParams['title']) ? $optionParams['title'] : $option;
                            $langKeys[$optionTitleLangKey] = $title;
                            $optionTitleExpr = "_t(" . $optionTitleLangKey . "', " . var_export_normal_precision($title, true) . ")";
                            $thisFormField .= '<div><input type="radio" name="' . $fieldName . '" id="' . $id . '__' . $__i . '" value="' . htmlspecialchars($option) . '"' . ($cssClass != '' ? ' class="' . $cssClass . '"' : '') . ($helpTopic != '' ? ' data-help-topic="' . $helpTopic . '"' : '') . $disabledTag . '/>' . '<label for="' . $id . '__' . $__i . '">' . '<?php echo ' . $optionTitleExpr . '; ?>' . "</label></div>\n";
                        }
                    }
                    $thisFormField .= $searchDescHTML . "\n";
                    break;
                case 'htmlfragment':
                    $thisFormField .= isset($field['html']) ? $field['html'] . "\n" : '';
                    break;
                case 'tabs':
                    // *** WARNING: Tab-related elements manipulate $formFields directly instead of populating $thisFormField.
                    $tabsPosition = isset($field['tabsPosition']) ? strtolower(trim($field['tabsPosition'])) : 'top';
                    switch ($tabsPosition) {
                        case 'top':
                            $tabbableClasses = 'tabbable';
                            break;
                        case 'left':
                            $tabbableClasses = 'tabbable tabs-left';
                            break;
                        case 'right':
                            $tabbableClasses = 'tabbable tabs-right';
                            break;
                        case 'bottom':
                            $tabbableClasses = 'tabbable tabs-below';
                            break;
                        default:
                            fprintf(STDERR, "inputType:tabs tabsPosition attribute must be one of top, left, right, bottom, or omitted (defaults to top).\n");
                            return false;
                    }
                    $ulOpenHTML = " <ul class=\"nav nav-tabs\">\n";
                    $ulCloseHTML = " </ul>";
                    $formFields .= "<div class=\"{$tabbableClasses}\">\n";
                    if ($tabsPosition != 'bottom') {
                        $formFields .= $ulOpenHTML;
                    }
                    $openContainers[] = (object) array('id' => $id, 'type' => 'tabs', 'tabsPosition' => $tabsPosition, 'tabInsertIdx' => strlen($formFields), 'openGroupId' => null, 'numGroups' => 0, 'closingHTMLPart1' => $tabsPosition == 'bottom' ? $ulOpenHTML : '', 'closingHTMLPart2' => '', 'closingHTMLPart3' => ($tabsPosition == 'bottom' ? $ulCloseHTML : '') . "\n</div> <!-- {$id} -->\n</div> <!-- class=\"tabbable\" -->\n");
                    if ($tabsPosition != 'bottom') {
                        $formFields .= $ulCloseHTML;
                    }
                    $formFields .= "\n <div id=\"{$id}\" class=\"tab-content\">\n";
                    break;
                case 'tabsClose':
                    // *** WARNING: Tab-related elements manipulate $formFields directly instead of populating $thisFormField.
                    if (empty($openContainers)) {
                        fprintf(STDERR, "inputType:tabsClose with no corresponding inputType:tabs in formFields section.\n");
                        return false;
                    }
                    $tc = array_pop($openContainers);
                    if ($tc->type != 'tabs') {
                        fprintf(STDERR, "inputType:tabsClose trying to close non-tabs container of type %s.\n", $tc->type);
                        return false;
                    }
                    if ($tc->openGroupId !== null) {
                        // Close the last tab in this tabs container.
                        $formFields .= "  </div> <!-- {$tc->openGroupId} -->\n\n";
                    }
                    $formFields .= $tc->closingHTMLPart1 . $tc->closingHTMLPart2 . $tc->closingHTMLPart3;
                    break;
                case 'tab':
                    // *** WARNING: Tab-related elements manipulate $formFields directly instead of populating $thisFormField.
                    if (empty($openContainers)) {
                        fprintf(STDERR, "tab found outside of tabs ... tabsClose in formFields section.\n");
                        return false;
                    }
                    $tc = $openContainers[count($openContainers) - 1];
                    if ($tc->type != 'tabs') {
                        fprintf(STDERR, "inputType:tab inside non-tabs container of type %s.\n", $tc->type);
                        return false;
                    }
                    $tc->numGroups++;
                    if ($tc->openGroupId !== null) {
                        // Close the previous tab in this tabs container.
                        $formFields .= "  </div> <!-- {$tc->openGroupId} -->\n\n";
                    }
                    $tc->openGroupId = $id;
                    $liClasses = $tc->numGroups == 1 ? 'active' : '';
                    $liClassTag = $liClasses != '' ? " class=\"{$liClasses}\"" : '';
                    $tabLinkHTML = "  <li{$liClassTag}><a href=\"#{$id}\" data-toggle=\"tab\"><?php echo {$titleExpr}; ?></a></li>\n";
                    // For active (first) tab, in addition to the .active selector, the .fade.in
                    // selector must also be present in order to indicate that the fade is complete;
                    // otherwise, the content for the active tab will not be visible until the
                    // user actually clicks on a tab.
                    $divClasses = 'tab-pane fade' . ($tc->numGroups == 1 ? ' in active' : '');
                    $divClassTag = $divClasses != '' ? " class=\"{$divClasses}\"" : '';
                    if ($tc->tabsPosition == 'bottom') {
                        $tc->closingHTMLPart2 .= $tabLinkHTML;
                    } else {
                        $formFields = substr($formFields, 0, $tc->tabInsertIdx) . $tabLinkHTML . substr($formFields, $tc->tabInsertIdx);
                    }
                    $formFields .= "\n  <div{$divClassTag} id=\"{$id}\">\n";
                    $tc->tabInsertIdx += strlen($tabLinkHTML);
                    break;
                case 'accordion':
                    $openContainers[] = (object) array('id' => $id, 'type' => 'accordion', 'tabsPosition' => '', 'tabInsertIdx' => -1, 'openGroupId' => null, 'numGroups' => 0, 'closingHTMLPart1' => '', 'closingHTMLPart2' => '', 'closingHTMLPart3' => "\n</div> <!-- {$id} -->\n");
                    $thisFormField .= "<div class=\"accordion\" id=\"{$id}\">\n";
                    break;
                case 'accordionClose':
                    if (empty($openContainers)) {
                        fprintf(STDERR, "inputType:accordionClose with no corresponding inputType:accordion in formFields section.\n");
                        return false;
                    }
                    $tc = array_pop($openContainers);
                    if ($tc->type != 'accordion') {
                        fprintf(STDERR, "inputType:accordionClose trying to close non-accordion container of type %s.\n", $tc->type);
                        return false;
                    }
                    if ($tc->openGroupId !== null) {
                        // Close the last group in this accordion container.
                        $thisFormField .= <<<EOF
   </div> <!-- class=\\"accordion-inner\\" -->
  </div> <!-- {$tc->openGroupId}-body -->
 </div> <!-- class=\\"accordion-group\\" -->


EOF;
                    }
                    $thisFormField .= $tc->closingHTMLPart1 . $tc->closingHTMLPart2 . $tc->closingHTMLPart3;
                    break;
                case 'accordionGroup':
                    if (empty($openContainers)) {
                        fprintf(STDERR, "accordionGroup found outside of accordion ... accordionClose in formFields section.\n");
                        return false;
                    }
                    $tc = $openContainers[count($openContainers) - 1];
                    if ($tc->type != 'accordion') {
                        fprintf(STDERR, "inputType:accordionGroup inside non-accordion container of type %s.\n", $tc->type);
                        return false;
                    }
                    $tc->numGroups++;
                    if ($tc->openGroupId !== null) {
                        // Close the previous group in this accordion container.
                        $thisFormField .= <<<EOF
   </div> <!-- class=\\"accordion-inner\\" -->
  </div> <!-- {$tc->openGroupId}-body -->
 </div> <!-- class=\\"accordion-group\\" -->


EOF;
                    }
                    $tc->openGroupId = $id;
                    $accordionBodyClasses = 'accordion-body collapse';
                    if ($tc->numGroups == 1) {
                        $accordionBodyClasses .= ' in';
                    }
                    $accordionBodyClassesTag = $accordionBodyClasses != '' ? " class=\"{$accordionBodyClasses}\"" : '';
                    $thisFormField .= <<<EOF
 <div class="accordion-group" id="{$id}">
  <div class="accordion-heading">
   <a class="accordion-toggle" data-toggle="collapse" data-parent="#{$tc->id}" href="#{$id}-body"><?php echo {$titleExpr}; ?></a>
  </div>
 <div id="{$id}-body"{$accordionBodyClassesTag}>
  <div class="accordion-inner">

EOF;
                    break;
                    /// TODO: Add support for other input types.  Button may be the only remaining desirable type which is not supported.
                /// TODO: Add support for other input types.  Button may be the only remaining desirable type which is not supported.
                default:
                    fprintf(STDERR, "Invalid inputType \"%s\" on input \"%s\" in formFields section.  Skipping this input.\n", $inputType, $fieldName);
                    continue;
            }
            // switch ($inputType)
            // Close the layout cell and row.
            switch ($inputType) {
                case 'text':
                case 'password':
                case 'textarea':
                case 'select':
                case 'file':
                case 'radio':
                case 'checkbox':
                    $thisFormField .= "</div></div>\n";
                    break;
            }
            if ($inputType == 'text' || $inputType == 'textarea') {
                $ajaxAutocompleteCommand = isset($field['ajaxAutocompleteCommand']) ? $field['ajaxAutocompleteCommand'] : '';
                $ajaxAutocompleteMinLength = isset($field['ajaxAutocompleteMinLength']) ? (int) trim($field['ajaxAutocompleteMinLength']) : 0;
                if ($ajaxAutocompleteMinLength <= 0) {
                    $ajaxAutocompleteMinLength = 2;
                }
                if ($ajaxAutocompleteCommand != '') {
                    $ajaxAutocompleteInitJS .= sprintf(<<<EOF
\t\$('#%s').ajaxAutocomplete({
\t\tautocompleteCommand:'%s',
\t\tminimumInputLength:%d
\t});

EOF
, $id, $ajaxAutocompleteCommand, $ajaxAutocompleteMinLength);
                }
                // if ($ajaxAutocompleteCommand != '')
                if (isset($field['ajaxCombobox']) && is_array($field['ajaxCombobox'])) {
                    $arr = $field['ajaxCombobox'];
                    unset($arr['rowFetcher']);
                    $ajaxComboboxInitJS .= "\t\$('#" . $id . "').ajaxCombobox(" . json_encode($arr) . ");\n";
                }
            }
            // if (($inputType == 'text') || ($inputType == 'textarea'))
            if ($thisFormField != '') {
                $formFields .= $thisFormField;
            }
        }
        // foreach ($crud['formFields'] as $fieldName=>$field)
        if (!empty($openContainers)) {
            fprintf(STDERR, "One or more open containers (tabs or accordion) without corresponding close (tabsClose, accordionClose) found in formFields section.\n");
            return false;
        }
        $includesByClass = array();
        $filterCode = '';
        if (!isset($crud['filters']) || !is_array($crud['filters'])) {
            $crud['filters'] = array();
        }
        foreach ($crud['filters'] as $fieldName => $filterGroup) {
            if (!is_array($filterGroup)) {
                continue;
            }
            foreach ($filterGroup as $filter) {
                if (!isset($filter['class']) || !is_string($filter['class'])) {
                    continue;
                }
                $filterClass = $filter['class'];
                $filterInclude = isset($filter['include']) && is_string($filter['include']) ? $filter['include'] : '';
                if (!isset($filter['params']) || !is_array($filter['params'])) {
                    $filter['params'] = array();
                }
                $filterParams = $filter['params'];
                if (!isset($filterParams['valueName'])) {
                    $filterParams['valueName'] = $fieldName;
                }
                if ($filterInclude != '') {
                    if (!isset($includesByClass[$filterClass])) {
                        $includesByClass[$filterClass] = array($filterInclude);
                    } else {
                        if (!in_array($filterInclude, $includesByClass[$filterClass])) {
                            $includesByClass[$filterClass][] = $filterInclude;
                        }
                    }
                }
                $paramsEnc = var_export_normal_precision($filterParams, true);
                $filterCode .= "\t\t\$__filter = new {$filterClass}({$paramsEnc});\n\t\t\$__filter->filter(\$db, \$row);\n\n";
            }
        }
        $filterCode .= "\t\tunset(\$__filter);\n";
        foreach ($includesByClass as $cls => $incs) {
            foreach ($incs as $inc) {
                $phpIncludes .= "if (!class_exists('{$cls}', false)) include {$docRootPath}.'/{$inc}';\n";
            }
        }
        $includesByClass = array();
        $validationCode = '';
        if (!isset($crud['validators']) || !is_array($crud['validators'])) {
            $crud['validators'] = array();
        }
        foreach ($crud['validators'] as $fieldName => $validatorGroup) {
            if (!is_array($validatorGroup)) {
                continue;
            }
            $fieldNameEnc = var_export_normal_precision($fieldName, true);
            $validatorNameCounts = array();
            foreach ($validatorGroup as $validator) {
                if (!isset($validator['class']) || !is_string($validator['class'])) {
                    continue;
                }
                $validatorClass = $validator['class'];
                $validatorInclude = isset($validator['include']) && is_string($validator['include']) ? $validator['include'] : '';
                if (isset($validatorNameCounts[$validatorClass])) {
                    $validatorNameCounts[$validatorClass]++;
                } else {
                    $validatorNameCounts[$validatorClass] = 1;
                }
                if (!isset($validator['params']) || !is_array($validator['params'])) {
                    $validator['params'] = array();
                }
                $validatorParams = $validator['params'];
                if (!is_array($validatorParams)) {
                    $validatorParams = array();
                }
                if (!isset($validatorParams['valueName'])) {
                    $validatorParams['valueName'] = $fieldName;
                }
                if ($validatorInclude != '') {
                    if (!isset($includesByClass[$validatorClass])) {
                        $includesByClass[$validatorClass] = array($validatorInclude);
                    } else {
                        if (!in_array($validatorInclude, $includesByClass[$validatorClass])) {
                            $includesByClass[$validatorClass][] = $validatorInclude;
                        }
                    }
                }
                $phpCondition = isset($validator['phpCondition']) && is_string($validator['phpCondition']) ? trim($validator['phpCondition']) : '';
                $phpc1 = $phpCondition != '' ? '( ' : '';
                $phpc2 = $phpCondition != '' ? " && ({$phpCondition}) )" : '';
                if (isset($validatorParams['errorMsg'])) {
                    $errorMsg = $validatorParams['errorMsg'];
                    unset($validatorParams['errorMsg']);
                    $paramsEnc = var_export_normal_precision($validatorParams, true);
                    if (substr($paramsEnc, -1) == ')') {
                        $validatorErrorMsgLangKey = "crud.{$crudName}.validator.{$fieldName}.{$validatorClass}" . ($validatorNameCounts[$validatorClass] > 1 ? '.' . $validatorNameCounts[$validatorClass] : '') . '.errorMsg';
                        $langKeys[$validatorErrorMsgLangKey] = $errorMsg;
                        $paramsEnc = rtrim(substr($paramsEnc, 0, strlen($paramsEnc) - 1), ", \t\n\r\v") . ",\n  'errorMsg'=>_t('{$validatorErrorMsgLangKey}', " . var_export_normal_precision($errorMsg, true) . "),\n )\n";
                    }
                } else {
                    $paramsEnc = var_export_normal_precision($validatorParams, true);
                }
                $validationCode .= "\t\tif {$phpc1}(!isset(\$result->fieldErrors[{$fieldNameEnc}])){$phpc2} {\n" . "\t\t\t\$__validator = new {$validatorClass}({$paramsEnc});\n" . "\t\t\t\$__validatorError = \$__validator->validate(\$db, \$row);\n" . "\t\t\tif (\$__validatorError != '') {\n" . "\t\t\t\t\$result->fieldErrors[{$fieldNameEnc}] = \$__validatorError;\n" . "\t\t\t}\n" . "\t\t}\n" . "\n";
            }
        }
        $validationCode .= "\t\tunset(\$__validator);\n" . "\t\tunset(\$__validatorError);\n";
        foreach ($includesByClass as $cls => $incs) {
            foreach ($incs as $inc) {
                $phpIncludes .= "if (!class_exists('{$cls}', false)) include {$docRootPath}.'/{$inc}';\n";
            }
        }
        $crudLoadCommand = 'load' . ucfirst($table->tableName);
        $crudLoadSynchronousOrAsynchronous = 'asynchronous';
        if (isset($crud['crudLoad'])) {
            $load = $crud['crudLoad'];
            if (isset($load['loadCommand']) && is_string($load['loadCommand'])) {
                $crudLoadCommand = $load['loadCommand'];
            }
            if (isset($load['synchronous']) && $load['synchronous']) {
                $crudLoadSynchronousOrAsynchronous = 'synchronous';
            }
        }
        $addFocusJS = isset($crud['addFocusField']) && is_string($crud['addFocusField']) ? $crud['addFocusField'] : '';
        if ($addFocusJS != '') {
            $addFocusJS = "\t\t\t\$('#{$table->tableName}Form #{$addFocusJS}').focus();";
        }
        $editFocusJS = isset($crud['editFocusField']) && is_string($crud['editFocusField']) ? $crud['editFocusField'] : '';
        if ($editFocusJS != '') {
            $editFocusJS = "\t\t\t\$('#{$table->tableName}Form #{$editFocusJS}').focus();";
        }
        $onlyUpdateColumns = isset($crud['onlyUpdateColumns']) ? $crud['onlyUpdateColumns'] : array();
        if (!is_array($onlyUpdateColumns)) {
            $onlyUpdateColumns = array();
        }
        if (empty($onlyUpdateColumns)) {
            $onlyUpdateColumns = 'array()';
        } else {
            $onlyUpdateColumns = "array('" . implode("', '", $onlyUpdateColumns) . "')";
        }
        $neverUpdateColumns = isset($crud['neverUpdateColumns']) ? $crud['neverUpdateColumns'] : array();
        if (!is_array($neverUpdateColumns)) {
            $neverUpdateColumns = array();
        }
        if (empty($neverUpdateColumns)) {
            $neverUpdateColumns = 'array()';
        } else {
            $neverUpdateColumns = "array('" . implode("', '", $neverUpdateColumns) . "')";
        }
        $javaScriptFiles = '';
        if (isset($crud['javaScriptFiles']) && is_array($crud['javaScriptFiles'])) {
            foreach ($crud['javaScriptFiles'] as $file) {
                $javaScriptFiles .= "\$headerScripts[] = '{$file}';\n";
            }
        }
        $cssFiles = '';
        if (isset($crud['cssFiles']) && is_array($crud['cssFiles'])) {
            foreach ($crud['cssFiles'] as $file) {
                $cssFiles .= "\$headerStylesheets[] = '{$file}';\n";
            }
        }
        $allowAddSimilar = isset($crud['allowAddSimilar']) && $crud['allowAddSimilar'] ? true : false;
        $viewInclude = $oldFileLayout ? "dirname(__FILE__).'/{$crudName}_view.include.php'" : "dirname(dirname(__FILE__)).'/{$crudName}_view.include.php'";
        $searchFor = array('{{generatedFileMessage}}', '{{mainOkCheck}}', '{{classAutoloadPathsInit}}', '{{mainHooksInclude}}', '{{viewHooksInclude}}', '{{controllerIncludes}}', '{{docRootPath}}', '{{jaxInclude}}', '{{jaxJQuery}}', '{{jaxJS}}', '{{phpIncludes}}', '{{postInitPHPIncludes}}', '{{tableDescription}}', '{{tableDescriptions}}', '{{tableName}}', '{{uTableName}}', '{{crudName}}', '{{ucrudName}}', '{{idCol}}', '{{uIdCol}}', '{{loggedInId}}', '{{crudSearchCommand}}', '{{crudSearchCallbacks}}', '{{crudSearchTableDisplayColumns}}', '{{crudSearchGridHeaderColumnsHTML}}', '{{crudSearchGridBodyColumnsHTML}}', '{{crudSearchColumnNamesJSON}}', '{{crudSearchColumnFilters}}', '{{crudSearchExtraQueryParamsJSON}}', '{{crudSearchDefaultSortsJSON}}', '{{crudSearchTableCallbacks}}', '{{ajaxAutocompleteInitJS}}', '{{ajaxComboboxInitJS}}', '{{crudLoadCommand}}', '{{hiddenFormFields}}', '{{formFields}}', '{{filterCode}}', '{{validationCode}}', '{{addFocusJS}}', '{{editFocusJS}}', '{{onlyUpdateColumns}}', '{{neverUpdateColumns}}', '{{javaScriptFiles}}', '{{cssFiles}}', '{{allowAddSimilar}}', '{{viewInclude}}');
        $replaceWith = array($generatedFileMessage, $mainOkCheck, $classAutoloadPathsInit, $mainHooksInclude, $viewHooksInclude, $controllerIncludes, $docRootPath, $jaxInclude, $jaxJQuery, $jaxJS, $phpIncludes, $postInitPHPIncludes, $cfg['tableDescription'], $cfg['tableDescriptions'], $table->tableName, ucfirst($table->tableName), $crudName, ucfirst($crudName), $idCol, ucfirst($idCol), $loggedInId, $crudSearchCommand, $crudSearchCallbacks, $crudSearchTableDisplayColumns, $crudSearchGridHeaderColumnsHTML, $crudSearchGridBodyColumnsHTML, json_encode($crudSearchColumnNames), $crudSearchColumnFilters, json_encode($crudSearchExtraQueryParams), json_encode($crudSearchDefaultSorts), $crudSearchTableCallbacks, $ajaxAutocompleteInitJS, $ajaxComboboxInitJS, $crudLoadCommand, $hiddenFormFields, $formFields, $filterCode, $validationCode, $addFocusJS, $editFocusJS, $onlyUpdateColumns, $neverUpdateColumns, $javaScriptFiles, $cssFiles, $allowAddSimilar ? 'true' : 'false', $viewInclude);
        $content = str_replace($searchFor, $replaceWith, file_get_contents($templatesDir . '/main.php'));
        $fn = $generatedOutputDir . '/' . $crudName . ($oldFileLayout ? '' : '_generated.include') . '.php';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if ($enableLangFiles) {
            $langfn = $outputDir . '/' . $crudName . '.php.strings';
            $langResources = loadLangResourceFileContents($langfn);
            $anyResourcesChanged = false;
            foreach ($langKeys as $key => $val) {
                if (getLangResource($langResources, $key) != $val) {
                    setLangResource($langResources, $key, $val);
                    $anyResourcesChanged = true;
                }
            }
            if ($anyResourcesChanged) {
                saveLangResourceFileContents($langfn, $langResources);
            }
        }
        if (!$oldFileLayout) {
            $fn = $outputDir . '/' . $crudName . '.php';
            if (!file_exists($fn)) {
                file_put_contents($fn, sprintf(<<<EOF
<?php
\$mainOk = true;
include dirname(__FILE__).'/generated/%s_generated.include.php';

// Add any custom functionality or hook functions for the main page below.


EOF
, $crudName));
            }
        }
        $content = str_replace($searchFor, $replaceWith, filterCRUDLoadSynchronousOrAsynchronous(filterSearchPresentation(file_get_contents($templatesDir . '/view.include.php'), $crudSearchPresentation), $crudLoadSynchronousOrAsynchronous));
        $fn = $generatedOutputDir . '/' . $crudName . '_view' . ($oldFileLayout ? '' : '_generated') . '.include.php';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if (!$oldFileLayout) {
            $fn = $outputDir . '/' . $crudName . '_view.include.php';
            if (!file_exists($fn)) {
                file_put_contents($fn, sprintf(<<<EOF
<?php
include dirname(__FILE__).'/generated/%s_view_generated.include.php';

// Add any custom functionality or hook functions for the view below.


EOF
, $crudName));
            }
        }
        $content = str_replace($searchFor, $replaceWith, filterCRUDLoadSynchronousOrAsynchronous(filterSearchPresentation(file_get_contents($templatesDir . '/controller.js'), $crudSearchPresentation), $crudLoadSynchronousOrAsynchronous));
        $fn = $generatedOutputDir . '/' . $crudName . '_controller' . ($oldFileLayout ? '' : '_generated') . '.js';
        if (!file_exists($fn) || file_get_contents($fn) != $content) {
            file_put_contents($fn, $content);
        }
        if (!$oldFileLayout) {
            $fn = $outputDir . '/' . $crudName . '_controller.js';
            if (!file_exists($fn)) {
                file_put_contents($fn, sprintf(<<<EOF
// Add any custom functionality or hook functions for the controller below.


EOF
, $crudName));
            }
        }
    }
    return true;
}