#include #include #include using namespace cgicc; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace boost; using namespace std; BOOLEAN_ELEMENT (font, "font"); // form element BOOLEAN_ELEMENT (embed, "embed"); // embedded object // The number of words that the student is focused on at a // given time. These excluded the words that are mastered by // the student. size_t const focusedWordsCount = 10; // The number of most recent words to try to avoid to ask // again. size_t const recentWordsToAvoidCount = 5; // When picking among the mastered words, we will try this // many times if he word selected appears in the most recent // words. If still not successful, we will give up and use // one of them anyway. size_t const recentWordsAvoidanceAttemptCount = 10; // The chance of selecting the word among the mastered words // is one-in-oddsOfSelectingNonMastered. (e.g. making the // following 5 would mean a 20% chance for the mastered // words. size_t const oddsOfSelectingNonMastered = 10; string const varName = "name"; string const varPassword = "password"; string const varSolution = "solution"; string const varDefinition = "definition"; string const varTryCount = "try-count"; string const varWord = "word"; string const varTotalCount = "total-count"; string const varCurrentCount = "current-count"; string const varEasyWordsTotal = "easy-words-total"; string const valueZero = "0"; string const nbsp = " "; string const programDir = "/home/ali/public_html/no_excuse_words/"; string const usersDir = programDir + "users/"; string const pronunciationDir = programDir + "pronunciations/"; string const urlPrefix = "http://acehreli.org/~ali/no_excuse_words/pronunciations/"; string const programTitle = "No-Excuse Words - "; // Using '-' characters did not work (focus() was not // working) string const randomizationPrefix = "_rand_"; string const triedPrefix = "tried-"; string const triedWordPrefix = triedPrefix + "wrong-"; string const triedCorrectPrefix = triedPrefix + "correct"; string const triedSolution = triedPrefix + varSolution; string const triedDefinition = triedPrefix + varDefinition; string const colorWordWrong = "#cc0000"; string const colorWordCorrect = "#006600"; string const colorEasyWords = "#0000cc"; size_t const totalTries = 3; // This must match the separator in the directory names char const namePasswordSeparator = '_'; class Error : public exception { string reason_; public: Error(string const & reason) : reason_(reason) {} virtual ~Error() throw() {} virtual const char * what() const throw() { return reason_.c_str(); } }; string makeActionUrl(Cgicc const & cgi, string const & action) { static string const programPrefix = "http://acehreli.org"; ostringstream result; result << programPrefix << cgi.getEnvironment().getScriptName() << '/' << action; return result.str(); } string timeStamp() { time_t now; time(&now); tm * timePieces = localtime(&now); char buffer[1000]; if (!strftime(buffer, sizeof(buffer), "[%Y-%m-%d %H:%M]", timePieces)) { return "[unknown time]"; } else { return buffer; } } void logMessage(string const & message) { ofstream logFile("/tmp/no_excuse_words_log", ios::out | ios::app); logFile << '\n' << timeStamp() << ' ' << message; } string htmlEscaper(char ch) { if (ch == '\"') return """; // if (ch == '<') return "<"; // if (ch == '>') return ">"; // if (ch == '&') return "&"; // if (ch == '\'') return "'"; return string(1, ch); } string replaceHtmlCharacters(string const & text) { ostringstream result; transform(text.begin(), text.end(), ostream_iterator(result, ""), htmlEscaper); return result.str(); } struct WordInfo { string word; string definition; WordInfo(string const & w = "", string const & d = "") : word(w), definition(replaceHtmlCharacters(d)) {} bool operator< (WordInfo const & other) const { return word < other.word; } }; void toLowerCaseWord(string & word) { for (string::iterator i = word.begin(); i != word.end(); ++i) { *i = tolower(*i); } } string getLowerCaseWord(istream & input) { string word; input >> word; toLowerCaseWord(word); return word; } string garbledWordName(string const & word) { string result = word; for (size_t i = 0; i != word.length(); ++i) { if (isalpha(result[i]) && (result[i] != 'z') && (result[i] != 'Z')) { ++result[i]; } else { result[i] = '_'; } } return result; } string makePronunciationFileName(string const & word) { ostringstream stream; stream << pronunciationDir << garbledWordName(word) << ".wav"; return stream.str(); } void makePronunciation(string const & text, string const & fileName) { ostringstream commandLine; commandLine << "echo \"" << text << "\" | /var/local/build/festival/bin/text2wave -o " << fileName; system(commandLine.str().c_str()); } void ensurePronunciation(string const & word, string const & text) { string const fileName = makePronunciationFileName(word); ifstream file(fileName.c_str()); if (!file) { makePronunciation(text, fileName); } } string embedWordField(string const & definition, string const & what) { string fieldText = definition; string::size_type const beg = definition.find('_'); if (beg != string::npos) { string::size_type const end = definition.find_first_not_of("_", beg); fieldText.replace(beg, end - beg, what); } else { fieldText += ": "; fieldText += what; } return fieldText; } istream & operator>> (istream & input, WordInfo & wordInfo) { bool done = false; while (input && !done) { string const word = getLowerCaseWord(input); string restOfLine; getline(input, restOfLine); if (input && !restOfLine.empty() && (word[0] != '#')) { string::size_type const beg = restOfLine.find_first_not_of(" "); if (beg != string::npos) { string const definition = restOfLine.substr(beg); ensurePronunciation(word, embedWordField(definition, word)); wordInfo = WordInfo(word, definition); done = true; } } } return input; } ostream & operator<< (ostream & output, WordInfo const & wordInfo) { return output << wordInfo.word << "\t\t" << wordInfo.definition; } typedef vector WordInfos; WordInfos readWords(string const & wordFileName, string const & /*name*/, size_t & /*oldCount*/) { WordInfos wordInfos; ifstream input(wordFileName.c_str()); if (!input) { cerr << "Cannot open " << wordFileName << '\n'; } else { istream_iterator beg(input); istream_iterator end; copy(beg, end, back_inserter(wordInfos)); } return wordInfos; } int randN(int n) { const unsigned aralik_genisligi = ((unsigned)(RAND_MAX)+1) / n; int r; do r = rand() / aralik_genisligi; while (r >= n); return r; } typedef std::set Words; WordInfo const & matchingWordInfo(WordInfos const & wordInfos, string const & word) { for (WordInfos::const_iterator infoIter = wordInfos.begin(); infoIter != wordInfos.end(); ++infoIter) { if (infoIter->word == word) { return *infoIter; } } ostringstream errorMessage; errorMessage << "Cannot find " << word << " in word infos"; throw Error(errorMessage.str()); } WordInfo const & selectFromMasteredWords(WordInfos const & wordInfos, Words const & masteredWords, Words const & recentWords) { assert(!masteredWords.empty()); Words::const_iterator wordIter = masteredWords.begin(); for (size_t i = 0; i != recentWordsAvoidanceAttemptCount; ++i) { wordIter = masteredWords.begin(); advance(wordIter, randN(masteredWords.size())); if (recentWords.find(*wordIter) == recentWords.end()) { break; } } return matchingWordInfo(wordInfos, *wordIter); } WordInfo const & selectFromDifficultWords(WordInfos const & wordInfos, Words const & masteredWords, Words const & recentWords) { vector candidates; for (size_t i = 0; ((i != wordInfos.size()) && (candidates.size() < focusedWordsCount)); ++i) { // Exclude the mastered words and the most recently used words if ((masteredWords.find(wordInfos[i].word) == masteredWords.end()) && (recentWords.find(wordInfos[i].word) == recentWords.end())) { candidates.push_back(&wordInfos[i]); } } if (!candidates.empty()) { // There are suitable words; pick one return *candidates[randN(candidates.size())]; } else { // The attempt for a decent pick has failed; just pick a word return wordInfos[randN(wordInfos.size())]; } } WordInfo const & selectWord(WordInfos const & wordInfos, Words const & masteredWords, Words const & recentWords) { return (masteredWords.empty() || (randN(oddsOfSelectingNonMastered) != 0) ? selectFromDifficultWords(wordInfos, masteredWords, recentWords) : selectFromMasteredWords(wordInfos, masteredWords, recentWords)); } struct PageInfo { string title; string content; PageInfo(string const & t, string const & c) : title(t), content(c) {} }; void sendPage(PageInfo const & pageInfo) { ostringstream pageText; // Send HTTP header pageText << head(title(programTitle + pageInfo.title)) << '\n' << pageInfo.content; ostringstream output; output << HTTPHTMLHeader() << '\n' << html(pageText.str()); // logPage(output.str()); cout << output.str(); } string randomizeFieldName(string const & prefix) { ostringstream name; name << prefix << randomizationPrefix << rand(); return name.str(); } string removeRandomization(string const & word) { string::size_type const randBegin = word.find(randomizationPrefix); return ((randBegin == string::npos) ? word : word.substr(0, randBegin)); } string makeInputField(string const & fieldName, string const & fieldValue) { ostringstream field; field << input() .set("type", "text") .set("name", fieldName) .set("size", (fieldValue.empty() ? "10" : lexical_cast(fieldValue.length() + 3))); return field.str(); } typedef std::pair WordPlace; input makeHiddenField(string const & name, string const & value) { input field; field .set("type", "hidden") .set("name", name) .set("value", value); return field; } // string getValue(Cgicc const & cgi, string const & name) // { // const_form_iterator const it = cgi.getElement(name); // if (it == cgi.getElements().end()) // { // ostringstream message; // message << "The variable '" << name << "' is not found"; // throw Error(message.str()); // } // FormEntry const & entry = *it; // return entry.getValue(); // } typedef std::map StateVariables; size_t getUIntValue(StateVariables & state, string const & name) { return state[name].empty() ? 0 : lexical_cast(state[name]); } size_t incrementUIntValue(StateVariables & state, string const & name) { size_t value = getUIntValue(state, name); ++value; state[name] = lexical_cast(value); return value; } size_t decrementUIntValue(StateVariables & state, string const & name) { size_t value = getUIntValue(state, name); --value; state[name] = lexical_cast(value); return value; } class VariableToForm { ostream & os_; public: explicit VariableToForm(ostream & os) : os_(os) {} void operator() (StateVariables::value_type const & state) { os_ << makeHiddenField(state.first, state.second) << '\n'; } }; string coloredText(string const & text, string const & color) { ostringstream result; result << font().set("color", color) << text << font(); return result.str(); } class ValueToStream { ostream & os_; public: explicit ValueToStream(ostream & os) : os_(os) {} void operator() (StateVariables::value_type const & state) { os_ << state.second << ' '; } }; class ColoredValueToStream { ostream & os_; string color_; public: explicit ColoredValueToStream(ostream & os, string const & color) : os_(os), color_(color) {} void operator() (StateVariables::value_type const & state) { os_ << coloredText(state.second, color_) << ' '; } }; void addStateToForm(ostream & os, StateVariables const & state) { for_each(state.begin(), state.end(), VariableToForm(os)); } string randomColor() { int const brightness = 128; int const boost = 128; int red = randN(brightness); int green = randN(brightness); int blue = randN(brightness); // Favor red and blue "just because" :) if (red > green) red += boost; if (blue > green) blue += boost; ostringstream color; color << setfill('0') << hex << "#" << setw(2) << red << setw(2) << green << setw(2) << blue; return color.str(); } string lastKeyInMap(string const & key) { return key + static_cast(CHAR_MAX); } string alreadyTriedWords(StateVariables & state) { ostringstream content; for_each(state.lower_bound(triedWordPrefix), state.upper_bound(lastKeyInMap(triedWordPrefix)), ColoredValueToStream(content, colorWordWrong)); for_each(state.lower_bound(triedCorrectPrefix), state.upper_bound(lastKeyInMap(triedCorrectPrefix)), ColoredValueToStream(content, colorWordCorrect)); return content.str(); } void eraseKeyPrefix(StateVariables & state, string const & keyPrefix) { state.erase(state.lower_bound(keyPrefix), state.upper_bound(lastKeyInMap(keyPrefix))); } class WordSkill { // 0: novice, 1: intermediate, 2: master size_t level_; public: WordSkill() : level_(0) {} void update(size_t tryCount) { assert((tryCount >= 1) && (tryCount <= 3)); size_t const transitions[3][3] = { //1 2 3 <- number of tries to spell correctly //--------- { 1, 0, 0 }, // | novice { 2, 0, 0 }, // | intermediate { 2, 1, 0 }, // | master }; level_ = transitions[level_][tryCount - 1]; } bool isMaster() const { return level_ == 2; } }; typedef std::map WordSkills; size_t numberOfWords(string const & line) { size_t count = 0; istringstream input(line); while (input) { string word; input >> word; if (!word.empty()) { ++count; } } return count; } string userDirName(string const & name, string const & password) { ostringstream dir; dir << usersDir << name << namePasswordSeparator << password << '/'; return dir.str(); } string userHistoryName(string const & name, string const & password) { ostringstream dir; dir << userDirName(name, password) << "history"; return dir.str(); } bool isMaster(WordSkills::value_type const & skillEntry) { return skillEntry.second.isMaster(); } Words selectMasteredWords(WordSkills const & skills) { Words words; for (WordSkills::const_iterator it = skills.begin(); it != skills.end(); ++it) { if (it->second.isMaster()) { words.insert(it->first); } } return words; } typedef queue WordQueue; Words queueToWords(WordQueue & queue) { Words words; while (!queue.empty()) { words.insert(queue.front()); queue.pop(); } return words; } pair readHistory(string const & name, string const & password) { WordSkills skills; WordQueue recentWords; ifstream file(userHistoryName(name, password).c_str()); while (file) { string word; file >> word; if (!word.empty()) { string restOfLine; getline(file, restOfLine); size_t const totalTries = numberOfWords(restOfLine); skills[word].update(totalTries); recentWords.push(word); if (recentWords.size() > recentWordsToAvoidCount) { recentWords.pop(); } } } return make_pair(selectMasteredWords(skills), queueToWords(recentWords)); } void pickWord(StateVariables & state) { string const wordFileName = programDir + "words"; size_t oldCount = 0; WordInfos const wordInfos = readWords(wordFileName, state[varName], oldCount); pair const historyInfo = readHistory(state[varName], state[varPassword]); Words const & masteredWords = historyInfo.first; Words const & recentWords = historyInfo.second; state[varEasyWordsTotal] = lexical_cast(masteredWords.size()); WordInfo const & wordInfo = selectWord(wordInfos, masteredWords, recentWords); state[varSolution] = wordInfo.word; state[varDefinition] = wordInfo.definition; state[varTryCount] = valueZero; } void normalizeUserName(StateVariables & state) { string name = state[varName]; toLowerCaseWord(name); if (name.length()) { name[0] = toupper(name[0]); state[varName] = name; } } string bodyWithFocus(string const & formName, string const & fieldName, string const & content) { ostringstream focus; focus << "document.forms." << formName << "." << fieldName << ".focus()"; ostringstream bodyText; bodyText << body(content).set("onLoad", focus.str()); return bodyText.str(); } string pageHeader(StateVariables & state) { ostringstream page; page << "Total easy words for " << state[varName] << ": " << b() << coloredText(state[varEasyWordsTotal], colorEasyWords) << b() << br() << "Word: " << (getUIntValue(state, varCurrentCount) + 1) << " of " << getUIntValue(state, varTotalCount) << br(); string const alreadyTried = alreadyTriedWords(state); if ( ! alreadyTried.empty()) { page << "Recent words: " << alreadyTried; string const correctSolution = state[triedSolution]; if ( ! correctSolution.empty()) { page << coloredText(correctSolution, colorWordCorrect); } } page << br() << hr(); return page.str(); } string shellEscape(string const & text) { string result; for (size_t i = 0; i != text.length(); ++i) { if ((text[i] == '\'') || (text[i] == '\"')) { result += '\\'; } result += text[i]; } return result; } string makePronunciationUrl(string const & word) { ostringstream url; url << urlPrefix << garbledWordName(word) << ".wav"; return url.str(); } class DirIterator { DIR * dir_; DirIterator(DirIterator const &); DirIterator & operator= (DirIterator const &); public: explicit DirIterator(string const & name) : dir_(opendir(name.c_str())) { if (!dir_) { ostringstream message; message << "Cannot read directory " << name; throw Error(message.str()); } } ~DirIterator() { closedir(dir_); } string const nextEntry() { struct dirent * entry = 0; do { entry = readdir(dir_); } while (entry && (entry->d_name[0] == '.')); return entry ? entry->d_name : ""; } }; typedef vector UserNames; UserNames readUserNames() { UserNames userNames; DirIterator iter(usersDir.c_str()); for (string entry = iter.nextEntry(); !entry.empty(); entry = iter.nextEntry()) { userNames.push_back(entry); } sort(userNames.begin(), userNames.end()); return userNames; } string userStats(string const & name, string const & password) { ostringstream stats; stats << p(); stats << b() << name << namePasswordSeparator << password << b() ; pair const historyInfo = readHistory(name, password); Words const & masteredWords = historyInfo.first; stats << '(' << masteredWords.size() << "): "; copy(masteredWords.begin(), masteredWords.end(), ostream_iterator(stats, " ")); stats << p(); return stats.str(); } pair splitNamePassword(string const & both) { string::size_type underScore = both.find(namePasswordSeparator); return make_pair(both.substr(0, underScore), both.substr(underScore + 1)); } PageInfo statsMain(Cgicc const &, StateVariables & /* state */) { ostringstream pageContent; UserNames const userNames = readUserNames(); for (size_t i = 0; i != userNames.size(); ++i) { pair const namePassword = splitNamePassword(userNames[i]); pageContent << userStats(namePassword.first, namePassword.second); } ostringstream page; page << body(pageContent.str()); return PageInfo("Statistics", page.str()); } PageInfo askQuestion(Cgicc const & cgi, StateVariables & state) { ostringstream page; page << pageHeader(state); // We randomize the field name to prevent the browser // from caching the earlier guesses string const randomizedVarWord = randomizeFieldName(varWord); ostringstream formContent; formContent << embedWordField(state[varDefinition], makeInputField(randomizedVarWord, state[varSolution])) << nbsp << nbsp << input().set("type", "submit").set("value", "Done") << '\n'; state.erase(varWord); addStateToForm(formContent, state); page << form(formContent.str()) .set("name", "questionForm") .set("action", makeActionUrl(cgi, "checkAnswer")) .set("method", "post"); page << br() << '\n' << (embed() .set("src", makePronunciationUrl(state[varSolution])) .set("autostart", "true") .set("loop", "false") // .set("width", "140") .set("height", "64") ); page << p() << (a(img() .set("src", "/icons/robot.jpg") // robot-talk.bmp .set("alt", "Robot Talk")) .set("href", makePronunciationUrl(state[varSolution]))) << br() << small() << "Marvin" << small(); // Ali couldn't get the 'object' way of inserting sound to the page. Why? // ostringstream objectContent; // objectContent << (param() // .set("name", "height") // .set("value", "60") // .set("valuetype", "data")) // << '\n' // << (param() // .set("name", "width") // .set("value", "60") // .set("valuetype", "data")); // page << (object(objectContent.str()) // .set("data", makePronunciationUrl(state[varSolution])) // .set("type", "audio/wav") // .set("width", "140") // .set("height", "140")); return PageInfo("Question", bodyWithFocus("questionForm", randomizedVarWord, page.str())); } PageInfo showTheSolution(Cgicc const & cgi, StateVariables & state) { state[triedSolution] = string ("(") + state[varSolution] + string(")"); ostringstream formContent; formContent << embedWordField(state[varDefinition], coloredText(state[varSolution], colorWordCorrect)) << p() << input().set("name", "show").set("type", "submit").set("value", "Continue") << p(); addStateToForm(formContent, state); ostringstream content; content << pageHeader(state); content << form(formContent.str()) .set("name", "showSolutionForm") .set("action", makeActionUrl(cgi, "theWordIsDone")) .set("method", "post"); return PageInfo("Solution", bodyWithFocus("showSolutionForm", "show", content.str())); } string randomPainter(char ch) { return coloredText(string(1, ch), randomColor()); } string colorfulLetters(string const & text) { ostringstream result; transform(text.begin(), text.end(), ostream_iterator(result, ""), randomPainter); return result.str(); } #define OBJECT_COUNT(x) (sizeof(x) / sizeof(*(x))) string randomCongratulation(string const & name) { string congrats[] = { "Terrific work", "Good job", "Way to go", "Awesome", }; ostringstream result; result << congrats[randN(OBJECT_COUNT(congrats))] << ' ' << name << '!'; return result.str(); } PageInfo showStats(Cgicc const & cgi, StateVariables & state) { decrementUIntValue(state, varCurrentCount); ostringstream formContent; formContent << input().set("name", "again").set("type", "submit").set("value", "Again!"); // This is a funny call here; what we actually need is // its side-effect of counting the mastered words: // state[varEasyWordsTotal] pair const historyInfo = readHistory(state[varName], state[varPassword]); Words const & masteredWords = historyInfo.first; state[varEasyWordsTotal] = lexical_cast(masteredWords.size()); addStateToForm(formContent, state); ostringstream content; content << pageHeader(state) << p() << h1() << b() << colorfulLetters(randomCongratulation(state[varName])) << b() << h1() << p(); content << form(formContent.str()) .set("name", "showStatsForm") .set("action", makeActionUrl(cgi, "getName")) .set("method", "post"); return PageInfo("Complete", bodyWithFocus("showStatsForm", "again", content.str())); } void makeOutputDirectory(string const & name) { errno = 0; int const ret = mkdir(name.c_str(), (S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH )); if ((ret == -1) && (errno != EEXIST)) { ostringstream errorMessage; errorMessage << p() << "Could not create directory: " << name << p(); throw Error(errorMessage.str()); } } // The behavior of the program has changed: we don't allow // any name any more. The user's directory must be already // created throug some user registration process. // void ensureUserDirExists(string const & name) // { // makeOutputDirectory(usersDir); // makeOutputDirectory(userDirName(name)); // } void updateHistory(StateVariables & state) { // ensureUserDirExists(state[varName]); ofstream file(userHistoryName(state[varName], state[varPassword]).c_str(), ios::out | ios::app); if (!file) { ostringstream errorMessage; errorMessage << "Cannot open: " << userHistoryName(state[varName], state[varPassword]); throw Error(errorMessage.str()); } else { file << state[varSolution] << ' '; for_each(state.lower_bound(triedWordPrefix), state.upper_bound(lastKeyInMap(triedWordPrefix)), ValueToStream(file)); file << state[triedCorrectPrefix] << '\n'; } } PageInfo theWordIsDone(Cgicc const & cgi, StateVariables & state) { updateHistory(state); size_t currentCount = incrementUIntValue(state, varCurrentCount); if (currentCount >= getUIntValue(state, varTotalCount)) { return showStats(cgi, state); } else { pickWord(state); return askQuestion(cgi, state); } } PageInfo tryWordAgain(Cgicc const & cgi, StateVariables & state) { size_t tryCount = incrementUIntValue(state, varTryCount); if (tryCount >= totalTries) { state[triedDefinition] = state[varDefinition]; return showTheSolution(cgi, state); } else { return askQuestion(cgi, state); } } // This and the next function are copied from a post by // James Kanze to comp.lang.c++.moderated, dated Sep 18 // 2001, 7:20 am. template< std::ctype_base::mask type , typename charT = char > class Is : public std::unary_function< charT , bool > { public: explicit Is( std::locale const& l ) : m_ctype( std::use_facet< std::ctype< charT > >( l ) ) {} bool operator()( charT ch ) const { return m_ctype.is( type , ch ) ; } private: std::ctype< charT > const& m_ctype ; }; // This and the previous function are copied from a post by // James Kanze to comp.lang.c++.moderated, dated Sep 18 // 2001, 7:20 am. template< typename stringT > stringT trim( stringT const& s , std::locale const& l = std::locale() ) { return std::find_if(s.begin() , s.end() , std::not1( Is< std::ctype_base::space , typename stringT::value_type >( l ) ) ) == s.end() ? stringT() : stringT(std::find_if(s.begin() , s.end() , std::not1( Is< std::ctype_base::space , typename stringT::value_type >( l ) ) ) , std::find_if(s.rbegin() , s.rend() , std::not1( Is< std::ctype_base::space , typename stringT::value_type >( l ) ) ).base() ) ; } // Takes the first word, and ignores the rest string firstWord(string const & word) { string::size_type const blank = word.find(' '); return ((blank == string::npos) ? word : word.substr(0, blank)); } void normalizeWord(string & word) { word = trim(word); word = firstWord(word); toLowerCaseWord(word); } PageInfo checkAnswer(Cgicc const & cgi, StateVariables & state) { string word = state[varWord]; normalizeWord(word); state[varWord] = word; if (word.empty()) { return askQuestion(cgi, state); } else { if (word == state[varSolution]) { if (state[varTryCount] == valueZero) { eraseKeyPrefix(state, triedPrefix); } state[triedCorrectPrefix] = word; return theWordIsDone(cgi, state); } else { if (state[varTryCount] == valueZero) { eraseKeyPrefix(state, triedPrefix); } state[string(triedWordPrefix) + state[varTryCount]] = word; return tryWordAgain(cgi, state); } } } typedef PageInfo (*Action)(Cgicc const &, StateVariables & state); PageInfo getName(Cgicc const & cgi, StateVariables & state) { ostringstream formContent; formContent << h1() << colorfulLetters("Hello!") << h1() << "Please use your first name and password below." << br() << "Entering your full name does not work." << br() << hr() << "What is your " << i() << "first" << i() << " name? " << (input() .set("type", "text") .set("name", varName) .set("value", state[varName]) .set("size", "10")) << br() << '\n' << "Password: " << (input() .set("type", "password") .set("name", varPassword) .set("value", "") .set("size", "10")) << br() << '\n' << "How many questions do you want? " << (input() .set("type", "text") .set("name", varTotalCount) .set("value", (state[varTotalCount].empty() ? "10" : state[varTotalCount])) .set("size", "3")) << br() << '\n' << (input() .set("type", "submit") .set("value", "Begin")) << '\n'; state[varCurrentCount] = valueZero; state[varTryCount] = valueZero; ostringstream content; content << form(formContent.str()) .set("name", "getNameForm") .set("action", makeActionUrl(cgi, "readInput")) .set("method", "post"); return PageInfo("Introduction", bodyWithFocus("getNameForm", varName, content.str())); } bool dirExists(StateVariables & state) { struct stat buf; return stat(userDirName(state[varName], state[varPassword]).c_str(), &buf) == 0; } // string userPasswordName(string const & name) // { // ostringstream fileName; // fileName << userDirName(name) << "password"; // return fileName.str(); // } string userPasswordName(string const & name, string const & password) { ostringstream fileName; fileName << userDirName(name, password); return fileName.str(); } string readPassWordFromFile(string const & name, string const & password) { ifstream file(userPasswordName(name, password).c_str()); if (!file) { ostringstream message; message << "Cannot find password for " << name; throw Error(message.str()); } // The existence of the file is good for us; the // password is in the name. // string password; // file >> password; return password; } bool validPassword(StateVariables & state) { try { return state[varPassword] == readPassWordFromFile(state[varName], state[varPassword]); } catch (exception const & error) { ostringstream errorStream; errorStream << "Invalid login: " << error.what(); logMessage(errorStream.str()); return false; } } bool validUser(StateVariables & state) { return !(state[varName].empty()) && dirExists(state); } typedef pair Range; bool validTotalCount(StateVariables & state, Range const & range) { try { size_t const totalCount = getUIntValue(state, varTotalCount); return (totalCount >= range.first) && (totalCount <= range.second); } catch (exception const & error) { return false; } } bool validateInput(ostream & content, StateVariables & state) { bool result = false; ostringstream errorStream; if (!validUser(state) || !validPassword(state)) { ostringstream failureEntry; failureEntry << "Login failure: " << state[varName] << namePasswordSeparator << state[varPassword]; logMessage(failureEntry.str()); errorStream << "Please enter a valid user name and a valid password." << br(); } Range const range = make_pair(1, 20); if (!validTotalCount(state, range)) { errorStream << "The number of questions must be within " << range.first << " to " << range.second << '.' << br(); } string const errorText = errorStream.str(); if (errorText.empty()) { result = true; } else { content << p() << coloredText(errorText, colorWordWrong) << p(); content << p() << "(You may send an e-mail to Ali at acehreli@yahoo.com for help.)" << p(); content << hr(); result = false; } return result; } void logLogin(StateVariables & state) { ostringstream message; message << "Login: " << state[varName] << " Total: " << state[varTotalCount]; logMessage(message.str()); } PageInfo readInput(Cgicc const & cgi, StateVariables & state) { normalizeUserName(state); ostringstream content; if (validateInput(content, state)) { logLogin(state); if (state[varName] == "Teacher") { return statsMain(cgi, state); } else { // ensureUserDirExists(state[varName]); // Start asking the questions pickWord(state); return askQuestion(cgi, state); } } else { // Get back to the login screen PageInfo pageInfo = getName(cgi, state); pageInfo.content = content.str() + pageInfo.content; return pageInfo; } } typedef std::map Actions; #define REGISTER_ACTION(action) actions.insert(make_pair(#action, action)); Actions initActions() { Actions actions; REGISTER_ACTION(getName); REGISTER_ACTION(readInput); REGISTER_ACTION(askQuestion); REGISTER_ACTION(checkAnswer); REGISTER_ACTION(theWordIsDone); REGISTER_ACTION(statsMain); return actions; } #undef REGISTER_ACTION Action pickAction(CgiEnvironment const & cgiEnv) { static Actions const actions = initActions(); static Action const defaultAction = getName; string const pathInfo = cgiEnv.getPathInfo(); string const actionTag = pathInfo.empty() ? "" : pathInfo.substr(1); Actions::const_iterator it = actions.find(actionTag); return (it == actions.end() ? defaultAction : it->second); } class VariableToState { StateVariables & state_; public: explicit VariableToState(StateVariables & state) : state_(state) {} void operator() (cgicc::FormEntry const & entry) { // TODO: Kludge! if (entry.getName() != "show") { state_[removeRandomization(entry.getName())] = trim(entry.getValue()); } } }; StateVariables getState(Cgicc const & cgi) { StateVariables state; for_each(cgi.getElements().begin(), cgi.getElements().end(), VariableToState(state)); return state; } int main() { srand(time(0)); try { Cgicc cgi; CgiEnvironment const & cgiEnv(cgi.getEnvironment()); StateVariables state = getState(cgi); Action const action = pickAction(cgiEnv); sendPage(action(cgi, state)); } catch(exception const & e) { ostringstream errorMessage; errorMessage << "ERROR: " << e.what(); logMessage(errorMessage.str()); return EXIT_FAILURE; } return EXIT_SUCCESS; }