1:    2:    3:    4:    5:    6:    7:    8:    9:   10:   11:   12:   13:   14:   15:   16:   17:   18:   19:   20:   21:   22:   23:   24:   25:   26:   27:   28:   29:   30:   31:   32:   33:   34:   35:   36:   37:   38:   39:   40:   41:   42:   43:   44:   45:   46:   47:   48:   49:   50:   51:   52:   53:   54:   55:   56:   57:   58:   59:   60:   61:   62:   63:   64:   65:   66:   67:   68:   69:   70:   71:   72:   73:   74:   75:   76:   77:   78:   79:   80:   81:   82:   83:   84:   85:   86:   87:   88:   89:   90:   91:   92:   93:   94:   95:   96:   97:   98:   99:  100:  101:  102:  103:  104:  105:  106:  107:  108:  109:  110:  111:  112:  113:  114:  115:  116:  117:  118:  119:  120:  121:  122:  123:  124:  125:  126:  127:  128:  129:  130:  131:  132:  133:  134:  135:  136:  137:  138:  139:  140:  141:  142:  143:  144:  145:  146:  147:  148:  149:  150:  151:  152:  153:  154:  155:  156:  157:  158:  159:  160:  161:  162:  163:  164:  165:  166:  167:  168:  169:  170:  171:  172:  173:  174:  175:  176:  177:  178:  179:  180:  181:  182:  183:  184:  185:  186:  187:  188:  189:  190:  191:  192:  193:  194:  195:  196:  197:  198:  199:  200:  201:  202:  203:  204:  205:  206:  207:  208:  209:  210:  211:  212:  213:  214:  215:  216:  217:  218:  219:  220:  221:  222:  223:  224:  225:  226:  227:  228:  229:  230:  231:  232:  233:  234:  235:  236:  237:  238:  239:  240:  241:  242:  243:  244:  245:  246:  247:  248:  249:  250:  251:  252:  253:  254:  255:  256:  257:  258:  259:  260:  261:  262:  263:  264:  265:  266:  267:  268:  269:  270:  271:  272:  273:  274:  275:  276:  277:  278:  279:  280:  281:  282:  283:  284:  285:  286:  287:  288:  289:  290:  291:  292:  293:  294:  295:  296:  297:  298:  299:  300:  301:  302:  303:  304:  305:  306:  307:  308:  309:  310:  311:  312:  313:  314:  315:  316:  317:  318:  319:  320:  321:  322:  323:  324:  325:  326:  327:  328:  329:  330:  331:  332:  333:  334:  335:  336:  337:  338:  339:  340:  341:  342:  343:  344:  345:  346:  347:  348:  349:  350:  351:  352:  353:  354:  355:  356:  357:  358:  359:  360:  361:  362:  363:  364:  365:  366:  367:  368:  369:  370:  371:  372:  373:  374:  375:  376:  377:  378:  379:  380:  381:  382:  383:  384:  385:  386:  387:  388:  389:  390:  391:  392:  393:  394:  395:  396:  397:  398:  399:  400:  401:  402:  403:  404:  405:  406:  407:  408:  409:  410:  411:  412:  413:  414:  415:  416:  417:  418:  419:  420:  421:  422:  423:  424:  425:  426:  427:  428:  429:  430:  431:  432:  433:  434:  435:  436:  437:  438:  439:  440:  441:  442:  443:  444:  445:  446:  447:  448:  449:  450:  451:  452:  453:  454:  455:  456:  457:  458:  459:  460:  461:  462:  463:  464:  465:  466:  467:  468:  469:  470:  471:  472:  473:  474:  475:  476:  477:  478:  479:  480:  481:  482:  483:  484:  485:  486:  487:  488:  489:  490:  491:  492:  493:  494:  495:  496:  497:  498:  499:  500:  501:  502:  503:  504:  505:  506:  507:  508:  509:  510:  511:  512:  513:  514:  515:  516:  517:  518:  519:  520:  521:  522:  523:  524:  525:  526:  527:  528:  529:  530:  531:  532:  533:  534:  535:  536:  537:  538:  539:  540:  541:  542:  543:  544:  545:  546:  547:  548:  549:  550:  551:  552:  553:  554:  555:  556:  557:  558:  559:  560:  561:  562:  563:  564:  565:  566:  567:  568:  569:  570:  571:  572:  573:  574:  575:  576:  577:  578:  579:  580:  581:  582:  583:  584:  585:  586:  587:  588:  589:  590:  591:  592:  593:  594:  595:  596:  597:  598:  599:  600:  601:  602:  603:  604:  605:  606:  607:  608:  609:  610:  611:  612:  613:  614:  615:  616:  617:  618:  619:  620:  621:  622:  623:  624:  625:  626:  627:  628:  629:  630:  631:  632:  633:  634:  635:  636:  637:  638:  639:  640:  641:  642:  643:  644:  645:  646:  647:  648:  649:  650:  651:  652:  653:  654:  655:  656:  657:  658:  659:  660:  661:  662:  663:  664:  665:  666:  667:  668:  669:  670:  671:  672:  673:  674:  675:  676:  677:  678:  679:  680:  681:  682:  683:  684:  685:  686:  687:  688:  689:  690:  691:  692:  693:  694:  695:  696:  697:  698:  699:  700:  701:  702:  703:  704:  705:  706:  707:  708:  709:  710:  711:  712:  713:  714:  715:  716:  717:  718:  719:  720:  721:  722:  723:  724:  725:  726:  727:  728:  729:  730:  731:  732:  733:  734:  735:  736:  737:  738:  739:  740:  741:  742:  743:  744:  745:  746:  747:  748:  749:  750:  751:  752:  753:  754:  755:  756:  757:  758:  759:  760:  761:  762:  763:  764:  765:  766:  767:  768:  769:  770:  771:  772:  773:  774:  775:  776:  777:  778:  779:  780:  781:  782:  783:  784:  785:  786:  787:  788:  789:  790:  791:  792:  793:  794:  795:  796:  797:  798:  799:  800:  801:  802:  803:  804:  805:  806:  807:  808:  809:  810:  811:  812:  813:  814:  815:  816:  817:  818:  819:  820:  821:  822:  823:  824:  825:  826:  827:  828:  829:  830:  831:  832:  833:  834:  835:  836:  837:  838:  839:  840:  841:  842:  843:  844:  845:  846:  847:  848:  849:  850:  851:  852:  853:  854:  855:  856:  857:  858:  859:  860:  861:  862:  863:  864:  865:  866:  867:  868:  869:  870:  871:  872:  873:  874:  875:  876:  877:  878:  879:  880:  881:  882:  883:  884:  885:  886:  887:  888:  889:  890:  891:  892:  893:  894:  895:  896:  897:  898:  899:  900:  901:  902:  903:  904:  905:  906:  907:  908:  909:  910:  911:  912:  913:  914:  915:  916:  917:  918:  919:  920:  921:  922:  923:  924:  925:  926:  927:  928:  929:  930:  931:  932:  933:  934:  935:  936:  937:  938:  939:  940:  941:  942:  943:  944:  945:  946:  947:  948:  949:  950:  951:  952:  953:  954:  955:  956:  957:  958:  959:  960:  961:  962:  963:  964:  965:  966:  967:  968:  969:  970:  971:  972:  973:  974:  975:  976:  977:  978:  979:  980:  981:  982:  983:  984:  985:  986:  987:  988:  989:  990:  991:  992:  993:  994:  995:  996:  997:  998:  999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029: 1030: 1031: 1032: 1033: 1034: 1035: 1036: 1037: 1038: 1039: 1040: 1041: 1042: 1043: 1044: 1045: 1046: 1047: 1048: 1049: 1050: 1051: 1052: 1053: 1054: 1055: 1056: 1057: 1058: 1059: 1060: 1061: 1062: 1063: 1064: 1065: 1066: 1067: 1068: 1069: 1070: 1071: 1072: 1073: 1074: 1075: 1076: 1077: 1078: 1079: 1080: 1081: 1082: 1083: 1084: 1085: 1086: 1087: 1088: 1089: 1090: 1091: 1092: 1093: 1094: 1095: 1096: 1097: 1098: 1099: 1100: 1101: 1102: 1103: 1104: 1105: 1106: 1107: 1108: 1109: 1110: 1111: 1112: 1113: 1114: 1115: 1116: 1117: 1118: 1119: 1120: 1121: 1122: 1123: 1124: 1125: 1126: 1127: 1128: 1129: 1130: 1131: 1132: 1133: 1134: 1135: 1136: 1137: 1138: 1139: 1140: 1141: 1142: 1143: 1144: 1145: 1146: 1147: 1148: 1149: 1150: 1151: 1152: 1153: 1154: 1155: 1156: 1157: 1158: 1159: 1160: 1161: 1162: 1163: 1164: 1165: 1166: 1167: 1168: 1169: 1170: 1171: 1172: 1173: 1174: 1175: 1176: 1177: 1178: 1179: 1180: 1181: 1182: 1183: 1184: 1185: 1186: 1187: 1188: 1189: 1190: 1191: 1192: 1193: 1194: 1195: 1196: 1197: 1198: 1199: 1200: 1201: 1202: 1203: 1204: 1205: 1206: 1207: 1208: 1209: 1210: 1211: 1212: 1213: 1214: 1215: 1216: 1217: 1218: 1219: 1220: 1221: 1222: 1223: 1224: 1225: 1226: 1227: 1228: 1229: 1230: 1231: 1232: 1233: 1234: 1235: 1236: 1237: 1238: 1239: 1240: 1241: 1242: 1243: 1244: 1245: 1246: 1247: 1248: 1249: 1250: 1251: 1252: 1253: 1254: 1255: 1256: 1257: 1258: 1259: 1260: 1261: 1262: 1263: 1264: 1265: 1266: 1267: 1268: 1269: 1270: 1271: 1272: 1273: 1274: 1275: 1276: 1277: 1278: 1279: 1280: 1281: 1282: 1283: 1284: 1285: 1286: 1287: 1288: 1289: 1290: 1291: 1292: 1293: 1294: 1295: 1296: 1297: 1298: 1299: 1300: 1301: 1302: 1303: 1304: 1305: 1306: 1307: 1308: 1309: 1310: 1311: 1312: 1313: 1314: 1315: 1316: 1317: 1318: 1319: 1320: 1321: 1322: 1323: 1324: 1325: 1326: 1327: 1328: 1329: 1330: 1331: 1332: 1333: 1334: 1335: 1336: 1337: 1338: 1339: 1340: 1341: 1342: 1343: 1344: 1345: 1346: 1347: 1348: 1349: 1350: 1351: 1352: 1353: 1354: 1355: 1356: 1357: 1358: 1359: 1360: 1361: 1362: 1363: 1364: 1365: 1366: 1367: 1368: 1369: 1370: 1371: 1372: 1373: 1374: 1375: 1376: 1377: 1378: 1379: 1380: 1381: 1382: 1383: 1384: 1385: 1386: 1387: 1388: 1389: 1390: 1391: 1392: 1393: 1394: 1395: 1396: 1397: 1398: 1399: 1400: 1401: 1402: 1403: 1404: 1405: 1406: 1407: 1408: 1409: 1410: 1411: 1412: 1413: 1414: 1415: 1416: 1417: 1418: 1419: 1420: 1421: 1422: 1423: 1424: 1425: 1426: 1427: 1428: 1429: 1430: 1431: 1432: 1433: 1434: 1435: 1436: 1437: 1438: 1439: 1440: 1441: 1442: 1443: 1444: 1445: 1446: 1447: 1448: 1449: 1450: 1451: 1452: 1453: 1454: 1455: 1456: 1457: 1458: 1459: 1460: 1461: 1462: 1463: 1464: 1465: 1466: 1467: 1468: 1469: 1470: 1471: 1472: 1473: 1474: 1475: 1476: 1477: 1478: 1479: 1480: 1481: 1482: 1483: 1484: 1485: 1486: 1487: 1488: 1489: 1490: 1491: 1492: 1493: 1494: 1495: 1496: 1497: 1498: 1499: 1500: 1501: 1502: 1503: 1504: 1505: 1506: 1507: 1508: 1509: 1510: 1511: 1512: 1513: 1514: 1515: 1516: 1517: 1518: 1519: 1520: 1521: 1522: 1523: 1524: 1525: 1526: 1527: 1528: 1529: 1530: 1531: 1532: 1533: 1534: 1535: 1536: 1537: 1538: 1539: 1540: 1541: 1542: 1543: 1544: 1545: 1546: 1547: 1548: 1549: 1550: 1551: 1552: 1553: 1554: 1555: 1556: 1557: 1558: 1559: 1560: 1561: 1562: 1563: 1564: 1565: 1566: 1567: 1568: 1569: 1570: 1571: 1572: 1573: 1574: 1575: 1576: 1577: 1578: 1579: 1580: 1581: 1582: 1583: 1584: 1585: 1586: 1587: 1588: 1589: 1590: 1591: 1592: 1593: 1594: 1595: 1596: 1597: 1598: 1599: 1600: 1601: 1602: 1603: 1604: 1605: 1606: 1607: 1608: 1609: 1610: 1611: 1612: 1613: 1614: 1615: 1616: 1617: 1618: 1619: 1620: 1621: 1622: 1623: 1624: 1625: 1626: 1627: 1628: 1629: 1630: 1631: 1632: 1633: 1634: 1635: 1636: 1637: 1638: 1639: 1640: 1641: 1642: 1643: 1644: 1645: 1646: 1647: 1648: 1649: 1650: 1651: 1652: 1653: 1654: 1655: 1656: 1657: 1658: 1659: 1660: 1661: 1662: 1663: 1664: 1665: 1666: 1667: 1668: 1669: 1670: 1671: 1672: 1673: 1674: 1675: 1676: 1677: 1678: 1679: 1680: 1681: 1682: 1683: 1684: 1685: 1686: 1687: 1688: 1689: 1690: 1691: 1692: 1693: 1694: 1695: 1696: 1697: 1698: 1699: 1700: 1701: 1702: 1703: 1704: 1705: 1706: 1707: 1708: 1709: 1710: 1711: 1712: 1713: 1714: 1715: 1716: 1717: 1718: 1719: 1720: 1721: 1722: 1723: 1724: 1725: 1726: 1727: 1728: 1729: 1730: 1731: 1732: 1733: 1734: 1735: 1736: 1737: 1738: 1739: 1740: 1741: 1742: 1743: 1744: 1745: 1746: 1747: 1748: 1749: 1750: 1751: 1752: 1753: 1754: 1755: 1756: 1757: 1758: 1759: 1760: 1761: 1762: 1763: 1764: 1765: 1766: 1767: 1768: 1769: 1770: 1771: 1772: 1773: 1774: 1775: 1776: 1777: 1778: 1779: 1780: 1781: 1782: 1783: 1784: 1785: 1786: 1787: 1788: 1789: 1790: 1791: 1792: 1793: 1794: 1795: 1796: 1797: 1798: 1799: 1800: 1801: 1802: 1803: 1804: 1805: 1806: 1807: 1808: 1809: 1810: 1811: 1812: 1813: 1814: 1815: 1816: 1817: 1818: 1819: 1820: 1821: 1822: 1823: 1824: 1825: 1826: 1827: 1828: 1829: 1830: 1831: 1832: 1833: 1834: 1835: 1836: 1837: 1838: 1839: 1840: 1841: 1842: 1843: 1844: 1845: 1846: 1847: 1848: 1849: 1850: 1851: 1852: 1853: 1854: 1855: 1856: 1857: 1858: 1859: 1860: 1861: 1862: 1863: 1864: 1865: 1866: 1867: 1868: 1869: 1870: 1871: 1872: 1873: 1874: 1875: 1876: 1877: 1878: 1879: 1880: 1881: 1882: 1883: 1884: 1885: 1886: 1887: 1888: 1889: 1890: 1891: 1892: 1893: 1894: 1895: 1896: 1897: 1898: 1899: 1900: 1901: 1902: 1903: 1904: 1905: 1906: 1907: 1908: 1909: 1910: 1911: 1912: 1913: 1914: 1915: 1916: 1917: 1918: 1919: 1920: 1921: 1922: 1923: 1924: 1925: 1926: 1927: 1928: 1929: 1930: 1931: 1932: 1933: 1934: 1935: 1936: 1937: 1938: 1939: 1940: 1941: 1942: 1943: 1944: 1945: 1946: 1947: 1948: 1949: 1950: 1951: 1952: 1953: 1954: 1955: 1956: 1957: 1958: 1959: 1960: 1961: 1962: 1963: 1964: 1965: 1966: 1967: 1968: 1969: 1970: 1971: 1972: 1973: 1974: 1975: 1976: 1977: 1978: 1979: 1980: 1981: 1982: 1983: 1984: 1985: 1986: 1987: 1988: 1989: 1990: 1991: 1992: 1993: 1994: 1995: 1996: 1997: 1998: 1999: 2000: 2001: 2002: 2003: 2004: 2005: 2006: 2007: 2008: 2009: 2010: 2011: 2012: 2013: 2014: 2015: 2016: 2017: 2018: 2019: 2020: 2021: 2022: 2023: 2024: 2025: 2026: 2027: 2028: 2029: 2030: 2031: 2032: 2033: 2034: 2035: 2036: 2037: 2038: 2039: 2040: 2041: 2042: 2043: 2044: 2045: 2046: 2047: 2048: 2049: 2050: 2051: 2052: 2053: 2054: 2055: 2056: 2057: 2058: 2059: 2060: 2061: 2062: 2063: 2064: 2065: 2066: 2067: 2068: 2069: 2070: 2071: 2072: 2073: 2074: 2075: 2076: 2077: 2078: 2079: 2080: 2081: 2082: 2083: 2084: 2085: 2086: 2087: 2088: 2089: 2090: 2091: 2092: 2093: 2094: 2095: 2096: 2097: 2098: 2099: 2100: 2101: 2102: 2103: 2104: 2105: 2106: 2107: 2108: 2109: 2110: 2111: 2112: 2113: 2114: 2115: 2116: 2117: 2118: 2119: 2120: 2121: 2122: 2123: 2124: 2125: 2126: 2127: 2128: 2129: 2130: 2131: 2132: 2133: 2134: 2135: 2136: 2137: 2138: 2139: 2140: 2141: 2142: 2143: 2144: 2145: 2146: 2147: 2148: 2149: 2150: 2151: 2152: 2153: 2154: 2155: 2156: 2157: 2158: 2159: 2160: 2161: 2162: 2163: 2164: 2165: 2166: 2167: 2168: 2169: 2170: 2171: 2172: 2173: 2174: 2175: 2176: 2177: 2178: 2179: 2180: 2181: 2182: 2183: 2184: 2185: 2186: 2187: 2188: 2189: 2190: 2191: 2192: 2193: 2194: 2195: 2196: 2197: 2198: 2199: 2200: 2201: 2202: 2203: 2204: 2205: 2206: 2207: 2208: 2209: 2210: 2211: 2212: 2213: 2214: 2215: 2216: 2217: 2218: 2219: 2220: 2221: 2222: 2223: 2224: 2225: 2226: 2227: 2228: 2229: 2230: 2231: 2232: 2233: 2234: 2235: 2236: 2237: 2238: 2239: 2240: 2241: 2242: 2243: 2244: 2245: 2246: 2247: 2248: 2249: 2250: 2251: 2252: 2253: 2254: 2255: 2256: 2257: 2258: 2259: 2260: 2261: 2262: 2263: 2264: 2265: 2266: 2267: 2268: 2269: 2270: 2271: 2272: 2273: 2274: 2275: 2276: 2277: 2278: 2279: 2280: 2281: 2282: 2283: 2284: 2285: 2286: 2287: 2288: 2289: 2290: 2291: 2292: 2293: 2294: 2295: 2296: 2297: 2298: 2299: 2300: 2301: 2302: 2303: 2304: 2305: 2306: 2307: 2308: 2309: 2310: 2311: 2312: 2313: 2314: 2315: 2316: 2317: 2318: 2319: 2320: 2321: 2322: 2323: 2324: 2325: 2326: 2327: 2328: 2329: 2330: 2331: 2332: 2333: 2334: 2335: 2336: 2337: 2338: 2339: 2340: 2341: 2342: 2343: 2344: 2345: 2346: 2347: 2348: 2349: 2350: 2351: 2352: 2353: 2354: 2355: 2356: 2357: 2358: 2359: 2360: 2361: 2362: 2363: 2364: 2365: 2366: 2367: 2368: 2369: 2370: 2371: 2372: 2373: 2374: 2375: 2376: 2377: 2378: 2379: 2380: 2381: 2382: 2383: 2384: 2385: 2386: 2387: 2388: 2389: 2390: 2391: 2392: 2393: 2394: 2395: 2396: 2397: 2398: 2399: 2400: 2401: 2402: 2403: 2404: 2405: 2406: 2407: 2408: 2409: 2410: 2411: 2412: 2413: 2414: 2415: 2416: 2417: 2418: 2419: 2420: 2421: 2422: 2423: 2424: 2425: 2426: 2427: 2428: 2429: 2430: 2431: 2432: 2433: 2434: 2435: 2436: 2437: 2438: 2439: 2440: 2441: 2442: 2443: 2444: 2445: 
<?php

/**
 * This file contains those functions specific to the editing box and is
 * generally used for WYSIWYG type functionality.
 *
 * Simple Machines Forum (SMF)
 *
 * @package SMF
 * @author Simple Machines http://www.simplemachines.org
 * @copyright 2019 Simple Machines and individual contributors
 * @license http://www.simplemachines.org/about/smf/license.php BSD
 *
 * @version 2.1 RC1
 */

if (!defined('SMF'))
    die('No direct access...');

/**
 * As of SMF 2.1, this is unused. But it is available if any mod wants to use it.
 * Convert only the BBC that can be edited in HTML mode for the (old) editor.
 *
 * @deprecated since version 2.1
 * @param string $text The text with bbcode in it
 * @param boolean $compat_mode Whether to actually convert the text
 * @return string The text
 */
function bbc_to_html($text, $compat_mode = false)
{
    global $modSettings;

    if (!$compat_mode)
        return $text;

    // Turn line breaks back into br's.
    $text = strtr($text, array("\r" => '', "\n" => '<br>'));

    // Prevent conversion of all bbcode inside these bbcodes.
    // @todo Tie in with bbc permissions ?
    foreach (array('code', 'php', 'nobbc') as $code)
    {
        if (strpos($text, '[' . $code) !== false)
        {
            $parts = preg_split('~(\[/' . $code . '\]|\[' . $code . '(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);

            // Only mess with stuff inside tags.
            for ($i = 0, $n = count($parts); $i < $n; $i++)
            {
                // Value of 2 means we're inside the tag.
                if ($i % 4 == 2)
                    $parts[$i] = strtr($parts[$i], array('[' => '&#91;', ']' => '&#93;', "'" => "'"));
            }
            // Put our humpty dumpty message back together again.
            $text = implode('', $parts);
        }
    }

    // What tags do we allow?
    $allowed_tags = array('b', 'u', 'i', 's', 'hr', 'list', 'li', 'font', 'size', 'color', 'img', 'left', 'center', 'right', 'url', 'email', 'ftp', 'sub', 'sup');

    $text = parse_bbc($text, true, '', $allowed_tags);

    // Fix for having a line break then a thingy.
    $text = strtr($text, array('<br><div' => '<div', "\n" => '', "\r" => ''));

    // Note that IE doesn't understand spans really - make them something "legacy"
    $working_html = array(
        '~<del>(.+?)</del>~i' => '<strike>$1</strike>',
        '~<span\sclass="bbc_u">(.+?)</span>~i' => '<u>$1</u>',
        '~<span\sstyle="color:\s*([#\d\w]+);" class="bbc_color">(.+?)</span>~i' => '<font color="$1">$2</font>',
        '~<span\sstyle="font-family:\s*([#\d\w\s]+);" class="bbc_font">(.+?)</span>~i' => '<font face="$1">$2</font>',
        '~<div\sstyle="text-align:\s*(left|right);">(.+?)</div>~i' => '<p align="$1">$2</p>',
    );
    $text = preg_replace(array_keys($working_html), array_values($working_html), $text);

    // Parse unique ID's and disable javascript into the smileys - using the double space.
    $i = 1;
    $text = preg_replace_callback('~(?:\s|&nbsp;)?<(img\ssrc="' . preg_quote($modSettings['smileys_url'], '~') . '/[^<>]+?/([^<>]+?)"\s*)[^<>]*?class="smiley">~',
        function($m) use (&$i)
        {
            return '<' . stripslashes($m[1]) . 'alt="" title="" onresizestart="return false;" id="smiley_' . $i++ . '_' . $m[2] . '" style="padding: 0 3px 0 3px;">';
        }, $text);

    return $text;
}

/**
 * Converts HTML to BBC
 * As of SMF 2.1, only used by ManageBoards.php (and possibly mods)
 *
 * @param string $text Text containing HTML
 * @return string The text with html converted to bbc
 */
function html_to_bbc($text)
{
    global $modSettings, $smcFunc, $scripturl, $context;

    // Replace newlines with spaces, as that's how browsers usually interpret them.
    $text = preg_replace("~\s*[\r\n]+\s*~", ' ', $text);

    // Though some of us love paragraphs, the parser will do better with breaks.
    $text = preg_replace('~</p>\s*?<p~i', '</p><br><p', $text);
    $text = preg_replace('~</p>\s*(?!<)~i', '</p><br>', $text);

    // Safari/webkit wraps lines in Wysiwyg in <div>'s.
    if (isBrowser('webkit'))
        $text = preg_replace(array('~<div(?:\s(?:[^<>]*?))?' . '>~i', '</div>'), array('<br>', ''), $text);

    // If there's a trailing break get rid of it - Firefox tends to add one.
    $text = preg_replace('~<br\s?/?' . '>$~i', '', $text);

    // Remove any formatting within code tags.
    if (strpos($text, '[code') !== false)
    {
        $text = preg_replace('~<br\s?/?' . '>~i', '#smf_br_spec_grudge_cool!#', $text);
        $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);

        // Only mess with stuff outside [code] tags.
        for ($i = 0, $n = count($parts); $i < $n; $i++)
        {
            // Value of 2 means we're inside the tag.
            if ($i % 4 == 2)
                $parts[$i] = strip_tags($parts[$i]);
        }

        $text = strtr(implode('', $parts), array('#smf_br_spec_grudge_cool!#' => '<br>'));
    }

    // Remove scripts, style and comment blocks.
    $text = preg_replace('~<script[^>]*[^/]?' . '>.*?</script>~i', '', $text);
    $text = preg_replace('~<style[^>]*[^/]?' . '>.*?</style>~i', '', $text);
    $text = preg_replace('~\\<\\!--.*?-->~i', '', $text);
    $text = preg_replace('~\\<\\!\\[CDATA\\[.*?\\]\\]\\>~i', '', $text);

    // Do the smileys ultra first!
    preg_match_all('~<img\b[^>]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(?:\s)?~i', $text, $matches);
    if (!empty($matches[0]))
    {
        // Get all our actual smiley codes
        $request = $smcFunc['db_query']('', '
            SELECT code
            FROM {db_prefix}smileys
            WHERE code IN ({array_string:smiley_codes})
            ORDER BY LENGTH(code) DESC',
            array(
                'smiley_codes' => $smiley_codes,
            )
        );
        $smiley_codes = $smcFunc['db_fetch_all']($request);
        $smcFunc['db_free_result']($request);

        foreach ($matches[1] as $k => $possible_code)
        {
            $possible_code = un_htmlspecialchars($possible_code);

            if (in_array($possible_code, $smiley_codes))
                $matches[1][$k] = '-[]-smf_smily_start#|#' . $possible_code . '-[]-smf_smily_end#|#';
            else
                $matches[1][$k] = $matches[0][$k];
        }

        // Replace the tags!
        $text = str_replace($matches[0], $matches[1], $text);

        // Now sort out spaces
        $text = str_replace(array('-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'), ' ', $text);
    }

    // Only try to buy more time if the client didn't quit.
    if (connection_aborted() && $context['server']['is_apache'])
        @apache_reset_timeout();

    $parts = preg_split('~(<[A-Za-z]+\s*[^<>]*?style="?[^<>"]+"?[^<>]*?(?:/?)>|</[A-Za-z]+>)~', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    $replacement = '';
    $stack = array();

    foreach ($parts as $part)
    {
        if (preg_match('~(<([A-Za-z]+)\s*[^<>]*?)style="?([^<>"]+)"?([^<>]*?(/?)>)~', $part, $matches) === 1)
        {
            // If it's being closed instantly, we can't deal with it...yet.
            if ($matches[5] === '/')
                continue;
            else
            {
                // Get an array of styles that apply to this element. (The strtr is there to combat HTML generated by Word.)
                $styles = explode(';', strtr($matches[3], array('&quot;' => '')));
                $curElement = $matches[2];
                $precedingStyle = $matches[1];
                $afterStyle = $matches[4];
                $curCloseTags = '';
                $extra_attr = '';

                foreach ($styles as $type_value_pair)
                {
                    // Remove spaces and convert uppercase letters.
                    $clean_type_value_pair = strtolower(strtr(trim($type_value_pair), '=', ':'));

                    // Something like 'font-weight: bold' is expected here.
                    if (strpos($clean_type_value_pair, ':') === false)
                        continue;

                    // Capture the elements of a single style item (e.g. 'font-weight' and 'bold').
                    list ($style_type, $style_value) = explode(':', $type_value_pair);

                    $style_value = trim($style_value);

                    switch (trim($style_type))
                    {
                        case 'font-weight':
                            if ($style_value === 'bold')
                            {
                                $curCloseTags .= '[/b]';
                                $replacement .= '[b]';
                            }
                            break;

                        case 'text-decoration':
                            if ($style_value == 'underline')
                            {
                                $curCloseTags .= '[/u]';
                                $replacement .= '[u]';
                            }
                            elseif ($style_value == 'line-through')
                            {
                                $curCloseTags .= '[/s]';
                                $replacement .= '[s]';
                            }
                            break;

                        case 'text-align':
                            if ($style_value == 'left')
                            {
                                $curCloseTags .= '[/left]';
                                $replacement .= '[left]';
                            }
                            elseif ($style_value == 'center')
                            {
                                $curCloseTags .= '[/center]';
                                $replacement .= '[center]';
                            }
                            elseif ($style_value == 'right')
                            {
                                $curCloseTags .= '[/right]';
                                $replacement .= '[right]';
                            }
                            break;

                        case 'font-style':
                            if ($style_value == 'italic')
                            {
                                $curCloseTags .= '[/i]';
                                $replacement .= '[i]';
                            }
                            break;

                        case 'color':
                            $curCloseTags .= '[/color]';
                            $replacement .= '[color=' . $style_value . ']';
                            break;

                        case 'font-size':
                            // Sometimes people put decimals where decimals should not be.
                            if (preg_match('~(\d)+\.\d+(p[xt])~i', $style_value, $dec_matches) === 1)
                                $style_value = $dec_matches[1] . $dec_matches[2];

                            $curCloseTags .= '[/size]';
                            $replacement .= '[size=' . $style_value . ']';
                            break;

                        case 'font-family':
                            // Only get the first freaking font if there's a list!
                            if (strpos($style_value, ',') !== false)
                                $style_value = substr($style_value, 0, strpos($style_value, ','));

                            $curCloseTags .= '[/font]';
                            $replacement .= '[font=' . strtr($style_value, array("'" => '')) . ']';
                            break;

                        // This is a hack for images with dimensions embedded.
                        case 'width':
                        case 'height':
                            if (preg_match('~[1-9]\d*~i', $style_value, $dimension) === 1)
                                $extra_attr .= ' ' . $style_type . '="' . $dimension[0] . '"';
                            break;

                        case 'list-style-type':
                            if (preg_match('~none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha~i', $style_value, $listType) === 1)
                                $extra_attr .= ' listtype="' . $listType[0] . '"';
                            break;
                    }
                }

                // Preserve some tags stripping the styling.
                if (in_array($matches[2], array('a', 'font', 'td')))
                {
                    $replacement .= $precedingStyle . $afterStyle;
                    $curCloseTags = '</' . $matches[2] . '>' . $curCloseTags;
                }

                // If there's something that still needs closing, push it to the stack.
                if (!empty($curCloseTags))
                    array_push($stack, array(
                            'element' => strtolower($curElement),
                            'closeTags' => $curCloseTags
                        )
                    );
                elseif (!empty($extra_attr))
                    $replacement .= $precedingStyle . $extra_attr . $afterStyle;
            }
        }

        elseif (preg_match('~</([A-Za-z]+)>~', $part, $matches) === 1)
        {
            // Is this the element that we've been waiting for to be closed?
            if (!empty($stack) && strtolower($matches[1]) === $stack[count($stack) - 1]['element'])
            {
                $byebyeTag = array_pop($stack);
                $replacement .= $byebyeTag['closeTags'];
            }

            // Must've been something else.
            else
                $replacement .= $part;
        }
        // In all other cases, just add the part to the replacement.
        else
            $replacement .= $part;
    }

    // Now put back the replacement in the text.
    $text = $replacement;

    // We are not finished yet, request more time.
    if (connection_aborted() && $context['server']['is_apache'])
        @apache_reset_timeout();

    // Let's pull out any legacy alignments.
    while (preg_match('~<([A-Za-z]+)\s+[^<>]*?(align="*(left|center|right)"*)[^<>]*?(/?)>~i', $text, $matches) === 1)
    {
        // Find the position in the text of this tag over again.
        $start_pos = strpos($text, $matches[0]);
        if ($start_pos === false)
            break;

        // End tag?
        if ($matches[4] != '/' && strpos($text, '</' . $matches[1] . '>', $start_pos) !== false)
        {
            $end_pos = strpos($text, '</' . $matches[1] . '>', $start_pos);

            // Remove the align from that tag so it's never checked again.
            $tag = substr($text, $start_pos, strlen($matches[0]));
            $content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
            $tag = str_replace($matches[2], '', $tag);

            // Put the tags back into the body.
            $text = substr($text, 0, $start_pos) . $tag . '[' . $matches[3] . ']' . $content . '[/' . $matches[3] . ']' . substr($text, $end_pos);
        }
        else
        {
            // Just get rid of this evil tag.
            $text = substr($text, 0, $start_pos) . substr($text, $start_pos + strlen($matches[0]));
        }
    }

    // Let's do some special stuff for fonts - cause we all love fonts.
    while (preg_match('~<font\s+([^<>]*)>~i', $text, $matches) === 1)
    {
        // Find the position of this again.
        $start_pos = strpos($text, $matches[0]);
        $end_pos = false;
        if ($start_pos === false)
            break;

        // This must have an end tag - and we must find the right one.
        $lower_text = strtolower($text);

        $start_pos_test = $start_pos + 4;
        // How many starting tags must we find closing ones for first?
        $start_font_tag_stack = 0;
        while ($start_pos_test < strlen($text))
        {
            // Where is the next starting font?
            $next_start_pos = strpos($lower_text, '<font', $start_pos_test);
            $next_end_pos = strpos($lower_text, '</font>', $start_pos_test);

            // Did we past another starting tag before an end one?
            if ($next_start_pos !== false && $next_start_pos < $next_end_pos)
            {
                $start_font_tag_stack++;
                $start_pos_test = $next_start_pos + 4;
            }
            // Otherwise we have an end tag but not the right one?
            elseif ($start_font_tag_stack)
            {
                $start_font_tag_stack--;
                $start_pos_test = $next_end_pos + 4;
            }
            // Otherwise we're there!
            else
            {
                $end_pos = $next_end_pos;
                break;
            }
        }
        if ($end_pos === false)
            break;

        // Now work out what the attributes are.
        $attribs = fetchTagAttributes($matches[1]);
        $tags = array();
        $sizes_equivalence = array(1 => '8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt');
        foreach ($attribs as $s => $v)
        {
            if ($s == 'size')
            {
                // Cast before empty chech because casting a string results in a 0 and we don't have zeros in the array! ;)
                $v = (int) trim($v);
                $v = empty($v) ? 1 : $v;
                $tags[] = array('[size=' . $sizes_equivalence[$v] . ']', '[/size]');
            }
            elseif ($s == 'face')
                $tags[] = array('[font=' . trim(strtolower($v)) . ']', '[/font]');
            elseif ($s == 'color')
                $tags[] = array('[color=' . trim(strtolower($v)) . ']', '[/color]');
        }

        // As before add in our tags.
        $before = $after = '';
        foreach ($tags as $tag)
        {
            $before .= $tag[0];
            if (isset($tag[1]))
                $after = $tag[1] . $after;
        }

        // Remove the tag so it's never checked again.
        $content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));

        // Put the tags back into the body.
        $text = substr($text, 0, $start_pos) . $before . $content . $after . substr($text, $end_pos + 7);
    }

    // Almost there, just a little more time.
    if (connection_aborted() && $context['server']['is_apache'])
        @apache_reset_timeout();

    if (count($parts = preg_split('~<(/?)(li|ol|ul)([^>]*)>~i', $text, null, PREG_SPLIT_DELIM_CAPTURE)) > 1)
    {
        // A toggle that dermines whether we're directly under a <ol> or <ul>.
        $inList = false;

        // Keep track of the number of nested list levels.
        $listDepth = 0;

        // Map what we can expect from the HTML to what is supported by SMF.
        $listTypeMapping = array(
            '1' => 'decimal',
            'A' => 'upper-alpha',
            'a' => 'lower-alpha',
            'I' => 'upper-roman',
            'i' => 'lower-roman',
            'disc' => 'disc',
            'square' => 'square',
            'circle' => 'circle',
        );

        // $i: text, $i + 1: '/', $i + 2: tag, $i + 3: tail.
        for ($i = 0, $numParts = count($parts) - 1; $i < $numParts; $i += 4)
        {
            $tag = strtolower($parts[$i + 2]);
            $isOpeningTag = $parts[$i + 1] === '';

            if ($isOpeningTag)
            {
                switch ($tag)
                {
                    case 'ol':
                    case 'ul':

                        // We have a problem, we're already in a list.
                        if ($inList)
                        {
                            // Inject a list opener, we'll deal with the ol/ul next loop.
                            array_splice($parts, $i, 0, array(
                                '',
                                '',
                                str_repeat("\t", $listDepth) . '[li]',
                                '',
                            ));
                            $numParts = count($parts) - 1;

                            // The inlist status changes a bit.
                            $inList = false;
                        }

                        // Just starting a new list.
                        else
                        {
                            $inList = true;

                            if ($tag === 'ol')
                                $listType = 'decimal';
                            elseif (preg_match('~type="?(' . implode('|', array_keys($listTypeMapping)) . ')"?~', $parts[$i + 3], $match) === 1)
                                $listType = $listTypeMapping[$match[1]];
                            else
                                $listType = null;

                            $listDepth++;

                            $parts[$i + 2] = '[list' . ($listType === null ? '' : ' type=' . $listType) . ']' . "\n";
                            $parts[$i + 3] = '';
                        }
                        break;

                    case 'li':

                        // This is how it should be: a list item inside the list.
                        if ($inList)
                        {
                            $parts[$i + 2] = str_repeat("\t", $listDepth) . '[li]';
                            $parts[$i + 3] = '';

                            // Within a list item, it's almost as if you're outside.
                            $inList = false;
                        }

                        // The li is no direct child of a list.
                        else
                        {
                            // We are apparently in a list item.
                            if ($listDepth > 0)
                            {
                                $parts[$i + 2] = '[/li]' . "\n" . str_repeat("\t", $listDepth) . '[li]';
                                $parts[$i + 3] = '';
                            }

                            // We're not even near a list.
                            else
                            {
                                // Quickly create a list with an item.
                                $listDepth++;

                                $parts[$i + 2] = '[list]' . "\n\t" . '[li]';
                                $parts[$i + 3] = '';
                            }
                        }

                        break;
                }
            }

            // Handle all the closing tags.
            else
            {
                switch ($tag)
                {
                    case 'ol':
                    case 'ul':

                        // As we expected it, closing the list while we're in it.
                        if ($inList)
                        {
                            $inList = false;

                            $listDepth--;

                            $parts[$i + 1] = '';
                            $parts[$i + 2] = str_repeat("\t", $listDepth) . '[/list]';
                            $parts[$i + 3] = '';
                        }

                        else
                        {
                            // We're in a list item.
                            if ($listDepth > 0)
                            {
                                // Inject closure for this list item first.
                                // The content of $parts[$i] is left as is!
                                array_splice($parts, $i + 1, 0, array(
                                    '', // $i + 1
                                    '[/li]' . "\n", // $i + 2
                                    '', // $i + 3
                                    '', // $i + 4
                                ));
                                $numParts = count($parts) - 1;

                                // Now that we've closed the li, we're in list space.
                                $inList = true;
                            }

                            // We're not even in a list, ignore
                            else
                            {
                                $parts[$i + 1] = '';
                                $parts[$i + 2] = '';
                                $parts[$i + 3] = '';
                            }
                        }
                        break;

                    case 'li':

                        if ($inList)
                        {
                            // There's no use for a </li> after <ol> or <ul>, ignore.
                            $parts[$i + 1] = '';
                            $parts[$i + 2] = '';
                            $parts[$i + 3] = '';
                        }

                        else
                        {
                            // Remove the trailing breaks from the list item.
                            $parts[$i] = preg_replace('~\s*<br\s*' . '/?' . '>\s*$~', '', $parts[$i]);
                            $parts[$i + 1] = '';
                            $parts[$i + 2] = '[/li]' . "\n";
                            $parts[$i + 3] = '';

                            // And we're back in the [list] space.
                            $inList = true;
                        }

                        break;
                }
            }

            // If we're in the [list] space, no content is allowed.
            if ($inList && trim(preg_replace('~\s*<br\s*' . '/?' . '>\s*~', '', $parts[$i + 4])) !== '')
            {
                // Fix it by injecting an extra list item.
                array_splice($parts, $i + 4, 0, array(
                    '', // No content.
                    '', // Opening tag.
                    'li', // It's a <li>.
                    '', // No tail.
                ));
                $numParts = count($parts) - 1;
            }
        }

        $text = implode('', $parts);

        if ($inList)
        {
            $listDepth--;
            $text .= str_repeat("\t", $listDepth) . '[/list]';
        }

        for ($i = $listDepth; $i > 0; $i--)
            $text .= '[/li]' . "\n" . str_repeat("\t", $i - 1) . '[/list]';
    }

    // I love my own image...
    while (preg_match('~<img\s+([^<>]*)/*>~i', $text, $matches) === 1)
    {
        // Find the position of the image.
        $start_pos = strpos($text, $matches[0]);
        if ($start_pos === false)
            break;
        $end_pos = $start_pos + strlen($matches[0]);

        $params = '';
        $src = '';

        $attrs = fetchTagAttributes($matches[1]);
        foreach ($attrs as $attrib => $value)
        {
            if (in_array($attrib, array('width', 'height')))
                $params .= ' ' . $attrib . '=' . (int) $value;
            elseif ($attrib == 'alt' && trim($value) != '')
                $params .= ' alt=' . trim($value);
            elseif ($attrib == 'src')
                $src = trim($value);
        }

        $tag = '';
        if (!empty($src))
        {
            // Attempt to fix the path in case it's not present.
            if (preg_match('~^https?://~i', $src) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
            {
                $baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);

                if (substr($src, 0, 1) === '/')
                    $src = $baseURL . $src;
                else
                    $src = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $src;
            }

            $tag = '[img' . $params . ']' . $src . '[/img]';
        }

        // Replace the tag
        $text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
    }

    // The final bits are the easy ones - tags which map to tags which map to tags - etc etc.
    $tags = array(
        '~<b(\s(.)*?)*?' . '>~i' => function()
        {
            return '[b]';
        },
        '~</b>~i' => function()
        {
            return '[/b]';
        },
        '~<i(\s(.)*?)*?' . '>~i' => function()
        {
            return '[i]';
        },
        '~</i>~i' => function()
        {
            return '[/i]';
        },
        '~<u(\s(.)*?)*?' . '>~i' => function()
        {
            return '[u]';
        },
        '~</u>~i' => function()
        {
            return '[/u]';
        },
        '~<strong(\s(.)*?)*?' . '>~i' => function()
        {
            return '[b]';
        },
        '~</strong>~i' => function()
        {
            return '[/b]';
        },
        '~<em(\s(.)*?)*?' . '>~i' => function()
        {
            return '[i]';
        },
        '~</em>~i' => function()
        {
            return '[i]';
        },
        '~<s(\s(.)*?)*?' . '>~i' => function()
        {
            return "[s]";
        },
        '~</s>~i' => function()
        {
            return "[/s]";
        },
        '~<strike(\s(.)*?)*?' . '>~i' => function()
        {
            return '[s]';
        },
        '~</strike>~i' => function()
        {
            return '[/s]';
        },
        '~<del(\s(.)*?)*?' . '>~i' => function()
        {
            return '[s]';
        },
        '~</del>~i' => function()
        {
            return '[/s]';
        },
        '~<center(\s(.)*?)*?' . '>~i' => function()
        {
            return '[center]';
        },
        '~</center>~i' => function()
        {
            return '[/center]';
        },
        '~<pre(\s(.)*?)*?' . '>~i' => function()
        {
            return '[pre]';
        },
        '~</pre>~i' => function()
        {
            return '[/pre]';
        },
        '~<sub(\s(.)*?)*?' . '>~i' => function()
        {
            return '[sub]';
        },
        '~</sub>~i' => function()
        {
            return '[/sub]';
        },
        '~<sup(\s(.)*?)*?' . '>~i' => function()
        {
            return '[sup]';
        },
        '~</sup>~i' => function()
        {
            return '[/sup]';
        },
        '~<tt(\s(.)*?)*?' . '>~i' => function()
        {
            return '[tt]';
        },
        '~</tt>~i' => function()
        {
            return '[/tt]';
        },
        '~<table(\s(.)*?)*?' . '>~i' => function()
        {
            return '[table]';
        },
        '~</table>~i' => function()
        {
            return '[/table]';
        },
        '~<tr(\s(.)*?)*?' . '>~i' => function()
        {
            return '[tr]';
        },
        '~</tr>~i' => function()
        {
            return '[/tr]';
        },
        '~<(td|th)\s[^<>]*?colspan="?(\d{1,2})"?.*?' . '>~i' => function($matches)
        {
            return str_repeat('[td][/td]', $matches[2] - 1) . '[td]';
        },
        '~<(td|th)(\s(.)*?)*?' . '>~i' => function()
        {
            return '[td]';
        },
        '~</(td|th)>~i' => function()
        {
            return '[/td]';
        },
        '~<br(?:\s[^<>]*?)?' . '>~i' => function()
        {
            return "\n";
        },
        '~<hr[^<>]*>(\n)?~i' => function($matches)
        {
            return "[hr]\n" . $matches[0];
        },
        '~(\n)?\\[hr\\]~i' => function()
        {
            return "\n[hr]";
        },
        '~^\n\\[hr\\]~i' => function()
        {
            return "[hr]";
        },
        '~<blockquote(\s(.)*?)*?' . '>~i' => function()
        {
            return "&lt;blockquote&gt;";
        },
        '~</blockquote>~i' => function()
        {
            return "&lt;/blockquote&gt;";
        },
        '~<ins(\s(.)*?)*?' . '>~i' => function()
        {
            return "&lt;ins&gt;";
        },
        '~</ins>~i' => function()
        {
            return "&lt;/ins&gt;";
        },
    );

    foreach ($tags as $tag => $replace)
        $text = preg_replace_callback($tag, $replace, $text);

    // Please give us just a little more time.
    if (connection_aborted() && $context['server']['is_apache'])
        @apache_reset_timeout();

    // What about URL's - the pain in the ass of the tag world.
    while (preg_match('~<a\s+([^<>]*)>([^<>]*)</a>~i', $text, $matches) === 1)
    {
        // Find the position of the URL.
        $start_pos = strpos($text, $matches[0]);
        if ($start_pos === false)
            break;
        $end_pos = $start_pos + strlen($matches[0]);

        $tag_type = 'url';
        $href = '';

        $attrs = fetchTagAttributes($matches[1]);
        foreach ($attrs as $attrib => $value)
        {
            if ($attrib == 'href')
            {
                $href = trim($value);

                // Are we dealing with an FTP link?
                if (preg_match('~^ftps?://~', $href) === 1)
                    $tag_type = 'ftp';

                // Or is this a link to an email address?
                elseif (substr($href, 0, 7) == 'mailto:')
                {
                    $tag_type = 'email';
                    $href = substr($href, 7);
                }

                // No http(s), so attempt to fix this potential relative URL.
                elseif (preg_match('~^https?://~i', $href) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
                {
                    $baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);

                    if (substr($href, 0, 1) === '/')
                        $href = $baseURL . $href;
                    else
                        $href = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $href;
                }
            }

            // External URL?
            if ($attrib == 'target' && $tag_type == 'url')
            {
                if (trim($value) == '_blank')
                    $tag_type == 'iurl';
            }
        }

        $tag = '';
        if ($href != '')
        {
            if ($matches[2] == $href)
                $tag = '[' . $tag_type . ']' . $href . '[/' . $tag_type . ']';
            else
                $tag = '[' . $tag_type . '=' . $href . ']' . $matches[2] . '[/' . $tag_type . ']';
        }

        // Replace the tag
        $text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
    }

    $text = strip_tags($text);

    // Some tags often end up as just dummy tags - remove those.
    $text = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $text);

    // Fix up entities.
    $text = preg_replace('~&#38;~i', '&#38;#38;', $text);

    $text = legalise_bbc($text);

    return $text;
}

/**
 * Returns an array of attributes associated with a tag.
 *
 * @param string $text A tag
 * @return array An array of attributes
 */
function fetchTagAttributes($text)
{
    $attribs = array();
    $key = $value = '';
    $tag_state = 0; // 0 = key, 1 = attribute with no string, 2 = attribute with string
    for ($i = 0; $i < strlen($text); $i++)
    {
        // We're either moving from the key to the attribute or we're in a string and this is fine.
        if ($text[$i] == '=')
        {
            if ($tag_state == 0)
                $tag_state = 1;
            elseif ($tag_state == 2)
                $value .= '=';
        }
        // A space is either moving from an attribute back to a potential key or in a string is fine.
        elseif ($text[$i] == ' ')
        {
            if ($tag_state == 2)
                $value .= ' ';
            elseif ($tag_state == 1)
            {
                $attribs[$key] = $value;
                $key = $value = '';
                $tag_state = 0;
            }
        }
        // A quote?
        elseif ($text[$i] == '"')
        {
            // Must be either going into or out of a string.
            if ($tag_state == 1)
                $tag_state = 2;
            else
                $tag_state = 1;
        }
        // Otherwise it's fine.
        else
        {
            if ($tag_state == 0)
                $key .= $text[$i];
            else
                $value .= $text[$i];
        }
    }

    // Anything left?
    if ($key != '' && $value != '')
        $attribs[$key] = $value;

    return $attribs;
}

/**
 * Attempt to clean up illegal BBC caused by browsers like Opera which don't obey the rules
 *
 * @param string $text Text
 * @return string Cleaned up text
 */
function legalise_bbc($text)
{
    global $modSettings;

    // Don't care about the texts that are too short.
    if (strlen($text) < 3)
        return $text;

    // A list of tags that's disabled by the admin.
    $disabled = empty($modSettings['disabledBBC']) ? array() : array_flip(explode(',', strtolower($modSettings['disabledBBC'])));

    // Get a list of all the tags that are not disabled.
    $all_tags = parse_bbc(false);
    $valid_tags = array();
    $self_closing_tags = array();
    foreach ($all_tags as $tag)
    {
        if (!isset($disabled[$tag['tag']]))
            $valid_tags[$tag['tag']] = !empty($tag['block_level']);
        if (isset($tag['type']) && $tag['type'] == 'closed')
            $self_closing_tags[] = $tag['tag'];
    }

    // Right - we're going to start by going through the whole lot to make sure we don't have align stuff crossed as this happens load and is stupid!
    $align_tags = array('left', 'center', 'right', 'pre');

    // Remove those align tags that are not valid.
    $align_tags = array_intersect($align_tags, array_keys($valid_tags));

    // These keep track of where we are!
    if (!empty($align_tags) && count($matches = preg_split('~(\\[/?(?:' . implode('|', $align_tags) . ')\\])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
    {
        // The first one is never a tag.
        $isTag = false;

        // By default we're not inside a tag too.
        $insideTag = null;

        foreach ($matches as $i => $match)
        {
            // We're only interested in tags, not text.
            if ($isTag)
            {
                $isClosingTag = substr($match, 1, 1) === '/';
                $tagName = substr($match, $isClosingTag ? 2 : 1, -1);

                // We're closing the exact same tag that we opened.
                if ($isClosingTag && $insideTag === $tagName)
                    $insideTag = null;

                // We're opening a tag and we're not yet inside one either
                elseif (!$isClosingTag && $insideTag === null)
                    $insideTag = $tagName;

                // In all other cases, this tag must be invalid
                else
                    unset($matches[$i]);
            }

            // The next one is gonna be the other one.
            $isTag = !$isTag;
        }

        // We're still inside a tag and had no chance for closure?
        if ($insideTag !== null)
            $matches[] = '[/' . $insideTag . ']';

        // And a complete text string again.
        $text = implode('', $matches);
    }

    // Quickly remove any tags which are back to back.
    $backToBackPattern = '~\\[(' . implode('|', array_diff(array_keys($valid_tags), array('td', 'anchor'))) . ')[^<>\\[\\]]*\\]\s*\\[/\\1\\]~';
    $lastlen = 0;
    while (strlen($text) !== $lastlen)
        $lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));

    // Need to sort the tags by name length.
    uksort($valid_tags, function($a, $b)
    {
        return strlen($a) < strlen($b) ? 1 : -1;
    });

    // These inline tags can compete with each other regarding style.
    $competing_tags = array(
        'color',
        'size',
    );

    // These keep track of where we are!
    if (count($parts = preg_split(sprintf('~(\\[)(/?)(%1$s)((?:[\\s=][^\\]\\[]*)?\\])~', implode('|', array_keys($valid_tags))), $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
    {
        // Start outside [nobbc] or [code] blocks.
        $inCode = false;
        $inNoBbc = false;

        // A buffer containing all opened inline elements.
        $inlineElements = array();

        // A buffer containing all opened block elements.
        $blockElements = array();

        // A buffer containing the opened inline elements that might compete.
        $competingElements = array();

        // $i: text, $i + 1: '[', $i + 2: '/', $i + 3: tag, $i + 4: tag tail.
        for ($i = 0, $n = count($parts) - 1; $i < $n; $i += 5)
        {
            $tag = $parts[$i + 3];
            $isOpeningTag = $parts[$i + 2] === '';
            $isClosingTag = $parts[$i + 2] === '/';
            $isBlockLevelTag = isset($valid_tags[$tag]) && $valid_tags[$tag] && !in_array($tag, $self_closing_tags);
            $isCompetingTag = in_array($tag, $competing_tags);

            // Check if this might be one of those cleaned out tags.
            if ($tag === '')
                continue;

            // Special case: inside [code] blocks any code is left untouched.
            elseif ($tag === 'code')
            {
                // We're inside a code block and closing it.
                if ($inCode && $isClosingTag)
                {
                    $inCode = false;

                    // Reopen tags that were closed before the code block.
                    if (!empty($inlineElements))
                        $parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
                }

                // We're outside a coding and nobbc block and opening it.
                elseif (!$inCode && !$inNoBbc && $isOpeningTag)
                {
                    // If there are still inline elements left open, close them now.
                    if (!empty($inlineElements))
                    {
                        $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
                        //$inlineElements = array();
                    }

                    $inCode = true;
                }

                // Nothing further to do.
                continue;
            }

            // Special case: inside [nobbc] blocks any BBC is left untouched.
            elseif ($tag === 'nobbc')
            {
                // We're inside a nobbc block and closing it.
                if ($inNoBbc && $isClosingTag)
                {
                    $inNoBbc = false;

                    // Some inline elements might've been closed that need reopening.
                    if (!empty($inlineElements))
                        $parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
                }

                // We're outside a nobbc and coding block and opening it.
                elseif (!$inNoBbc && !$inCode && $isOpeningTag)
                {
                    // Can't have inline elements still opened.
                    if (!empty($inlineElements))
                    {
                        $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
                        //$inlineElements = array();
                    }

                    $inNoBbc = true;
                }

                continue;
            }

            // So, we're inside one of the special blocks: ignore any tag.
            elseif ($inCode || $inNoBbc)
                continue;

            // We're dealing with an opening tag.
            if ($isOpeningTag)
            {
                // Everyting inside the square brackets of the opening tag.
                $elementContent = $parts[$i + 3] . substr($parts[$i + 4], 0, -1);

                // A block level opening tag.
                if ($isBlockLevelTag)
                {
                    // Are there inline elements still open?
                    if (!empty($inlineElements))
                    {
                        // Close all the inline tags, a block tag is coming...
                        $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';

                        // Now open them again, we're inside the block tag now.
                        $parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
                    }

                    $blockElements[] = $tag;
                }

                // Inline opening tag.
                elseif (!in_array($tag, $self_closing_tags))
                {
                    // Can't have two opening elements with the same contents!
                    if (isset($inlineElements[$elementContent]))
                    {
                        // Get rid of this tag.
                        $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';

                        // Now try to find the corresponding closing tag.
                        $curLevel = 1;
                        for ($j = $i + 5, $m = count($parts) - 1; $j < $m; $j += 5)
                        {
                            // Find the tags with the same tagname
                            if ($parts[$j + 3] === $tag)
                            {
                                // If it's an opening tag, increase the level.
                                if ($parts[$j + 2] === '')
                                    $curLevel++;

                                // A closing tag, decrease the level.
                                else
                                {
                                    $curLevel--;

                                    // Gotcha! Clean out this closing tag gone rogue.
                                    if ($curLevel === 0)
                                    {
                                        $parts[$j + 1] = $parts[$j + 2] = $parts[$j + 3] = $parts[$j + 4] = '';
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    // Otherwise, add this one to the list.
                    else
                    {
                        if ($isCompetingTag)
                        {
                            if (!isset($competingElements[$tag]))
                                $competingElements[$tag] = array();

                            $competingElements[$tag][] = $parts[$i + 4];

                            if (count($competingElements[$tag]) > 1)
                                $parts[$i] .= '[/' . $tag . ']';
                        }

                        $inlineElements[$elementContent] = $tag;
                    }
                }
            }

            // Closing tag.
            else
            {
                // Closing the block tag.
                if ($isBlockLevelTag)
                {
                    // Close the elements that should've been closed by closing this tag.
                    if (!empty($blockElements))
                    {
                        $addClosingTags = array();
                        while ($element = array_pop($blockElements))
                        {
                            if ($element === $tag)
                                break;

                            // Still a block tag was open not equal to this tag.
                            $addClosingTags[] = $element['type'];
                        }

                        if (!empty($addClosingTags))
                            $parts[$i + 1] = '[/' . implode('][/', array_reverse($addClosingTags)) . ']' . $parts[$i + 1];

                        // Apparently the closing tag was not found on the stack.
                        if (!is_string($element) || $element !== $tag)
                        {
                            // Get rid of this particular closing tag, it was never opened.
                            $parts[$i + 1] = substr($parts[$i + 1], 0, -1);
                            $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
                            continue;
                        }
                    }
                    else
                    {
                        // Get rid of this closing tag!
                        $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
                        continue;
                    }

                    // Inline elements are still left opened?
                    if (!empty($inlineElements))
                    {
                        // Close them first..
                        $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';

                        // Then reopen them.
                        $parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
                    }
                }
                // Inline tag.
                else
                {
                    // Are we expecting this tag to end?
                    if (in_array($tag, $inlineElements))
                    {
                        foreach (array_reverse($inlineElements, true) as $tagContentToBeClosed => $tagToBeClosed)
                        {
                            // Closing it one way or the other.
                            unset($inlineElements[$tagContentToBeClosed]);

                            // Was this the tag we were looking for?
                            if ($tagToBeClosed === $tag)
                                break;

                            // Nope, close it and look further!
                            else
                                $parts[$i] .= '[/' . $tagToBeClosed . ']';
                        }

                        if ($isCompetingTag && !empty($competingElements[$tag]))
                        {
                            array_pop($competingElements[$tag]);

                            if (count($competingElements[$tag]) > 0)
                                $parts[$i + 5] = '[' . $tag . $competingElements[$tag][count($competingElements[$tag]) - 1] . $parts[$i + 5];
                        }
                    }

                    // Unexpected closing tag, ex-ter-mi-nate.
                    else
                        $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
                }
            }
        }

        // Close the code tags.
        if ($inCode)
            $parts[$i] .= '[/code]';

        // The same for nobbc tags.
        elseif ($inNoBbc)
            $parts[$i] .= '[/nobbc]';

        // Still inline tags left unclosed? Close them now, better late than never.
        elseif (!empty($inlineElements))
            $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';

        // Now close the block elements.
        if (!empty($blockElements))
            $parts[$i] .= '[/' . implode('][/', array_reverse($blockElements)) . ']';

        $text = implode('', $parts);
    }

    // Final clean up of back to back tags.
    $lastlen = 0;
    while (strlen($text) !== $lastlen)
        $lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));

    return $text;
}

/**
 * Creates the javascript code for localization of the editor (SCEditor)
 */
function loadLocale()
{
    global $context, $txt, $editortxt, $modSettings;

    loadLanguage('Editor');

    $context['template_layers'] = array();
    // Lets make sure we aren't going to output anything nasty.
    @ob_end_clean();
    if (!empty($modSettings['enableCompressedOutput']))
        @ob_start('ob_gzhandler');
    else
        @ob_start();

    // If we don't have any locale better avoid broken js
    if (empty($txt['lang_locale']))
        die();

    $file_data = '(function ($) {
    \'use strict\';

    $.sceditor.locale[' . JavaScriptEscape($txt['lang_locale']) . '] = {';
    foreach ($editortxt as $key => $val)
        $file_data .= '
        ' . JavaScriptEscape($key) . ': ' . JavaScriptEscape($val) . ',';

    $file_data .= '
        dateFormat: "day.month.year"
    }
})(jQuery);';

    // Make sure they know what type of file we are.
    header('content-type: text/javascript');
    echo $file_data;
    obExit(false);
}

/**
 * Retrieves a list of message icons.
 * - Based on the settings, the array will either contain a list of default
 *   message icons or a list of custom message icons retrieved from the database.
 * - The board_id is needed for the custom message icons (which can be set for
 *   each board individually).
 *
 * @param int $board_id The ID of the board
 * @return array An array of info about available icons
 */
function getMessageIcons($board_id)
{
    global $modSettings, $txt, $settings, $smcFunc;

    if (empty($modSettings['messageIcons_enable']))
    {
        loadLanguage('Post');

        $icons = array(
            array('value' => 'xx', 'name' => $txt['standard']),
            array('value' => 'thumbup', 'name' => $txt['thumbs_up']),
            array('value' => 'thumbdown', 'name' => $txt['thumbs_down']),
            array('value' => 'exclamation', 'name' => $txt['exclamation_point']),
            array('value' => 'question', 'name' => $txt['question_mark']),
            array('value' => 'lamp', 'name' => $txt['lamp']),
            array('value' => 'smiley', 'name' => $txt['icon_smiley']),
            array('value' => 'angry', 'name' => $txt['icon_angry']),
            array('value' => 'cheesy', 'name' => $txt['icon_cheesy']),
            array('value' => 'grin', 'name' => $txt['icon_grin']),
            array('value' => 'sad', 'name' => $txt['icon_sad']),
            array('value' => 'wink', 'name' => $txt['icon_wink']),
            array('value' => 'poll', 'name' => $txt['icon_poll']),
        );

        foreach ($icons as $k => $dummy)
        {
            $icons[$k]['url'] = $settings['images_url'] . '/post/' . $dummy['value'] . '.png';
            $icons[$k]['is_last'] = false;
        }
    }
    // Otherwise load the icons, and check we give the right image too...
    else
    {
        if (($temp = cache_get_data('posting_icons-' . $board_id, 480)) == null)
        {
            $request = $smcFunc['db_query']('', '
                SELECT title, filename
                FROM {db_prefix}message_icons
                WHERE id_board IN (0, {int:board_id})
                ORDER BY icon_order',
                array(
                    'board_id' => $board_id,
                )
            );
            $icon_data = array();
            while ($row = $smcFunc['db_fetch_assoc']($request))
                $icon_data[] = $row;
            $smcFunc['db_free_result']($request);

            $icons = array();
            foreach ($icon_data as $icon)
            {
                $icons[$icon['filename']] = array(
                    'value' => $icon['filename'],
                    'name' => $icon['title'],
                    'url' => $settings[file_exists($settings['theme_dir'] . '/images/post/' . $icon['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $icon['filename'] . '.png',
                    'is_last' => false,
                );
            }

            cache_put_data('posting_icons-' . $board_id, $icons, 480);
        }
        else
            $icons = $temp;
    }
    call_integration_hook('integrate_load_message_icons', array(&$icons));

    return array_values($icons);
}

/**
 * Creates a box that can be used for richedit stuff like BBC, Smileys etc.
 *
 * @param array $editorOptions Various options for the editor
 */
function create_control_richedit($editorOptions)
{
    global $txt, $modSettings, $options, $smcFunc, $editortxt;
    global $context, $settings, $user_info, $scripturl;

    // Load the Post language file... for the moment at least.
    loadLanguage('Post');
    loadLanguage('Editor');

    // Every control must have a ID!
    assert(isset($editorOptions['id']));
    assert(isset($editorOptions['value']));

    // Is this the first richedit - if so we need to ensure some template stuff is initialised.
    if (empty($context['controls']['richedit']))
    {
        // Some general stuff.
        $settings['smileys_url'] = $modSettings['smileys_url'] . '/' . $user_info['smiley_set'];
        if (!empty($context['drafts_autosave']))
            $context['drafts_autosave_frequency'] = empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000;

        // This really has some WYSIWYG stuff.
        loadCSSFile('jquery.sceditor.css', array('force_current' => false, 'validate' => true), 'smf_jquery_sceditor');
        loadTemplate('GenericControls');

        // JS makes the editor go round
        loadJavaScriptFile('editor.js', array('minimize' => true), 'smf_editor');
        loadJavaScriptFile('jquery.sceditor.bbcode.min.js', array(), 'smf_sceditor_bbcode');
        loadJavaScriptFile('jquery.sceditor.smf.js', array('minimize' => true), 'smf_sceditor_smf');
        addInlineJavaScript('
        var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
        var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
        var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
        var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';');
        // editor language file
        if (!empty($txt['lang_locale']) && $txt['lang_locale'] != 'en_US')
            loadJavaScriptFile($scripturl . '?action=loadeditorlocale', array('external' => true), 'sceditor_language');

        $context['shortcuts_text'] = $txt['shortcuts' . (!empty($context['drafts_save']) ? '_drafts' : '') . (stripos($_SERVER['HTTP_USER_AGENT'], 'Macintosh') !== false ? '_mac' : (isBrowser('is_firefox') ? '_firefox' : ''))];
        $context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_character_set'] == 'UTF-8' || function_exists('iconv'))));
        if ($context['show_spellchecking'])
        {
            loadJavaScriptFile('spellcheck.js', array('minimize' => true), 'smf_spellcheck');

            // Some hidden information is needed in order to make the spell checking work.
            if (!isset($_REQUEST['xml']))
                $context['insert_after_template'] .= '
        <form name="spell_form" id="spell_form" method="post" accept-charset="' . $context['character_set'] . '" target="spellWindow" action="' . $scripturl . '?action=spellcheck">
            <input type="hidden" name="spellstring" value="">
        </form>';
        }
    }

    // Start off the editor...
    $context['controls']['richedit'][$editorOptions['id']] = array(
        'id' => $editorOptions['id'],
        'value' => $editorOptions['value'],
        'rich_value' => $editorOptions['value'], // 2.0 editor compatibility
        'rich_active' => empty($modSettings['disable_wysiwyg']) && (!empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode'])),
        'disable_smiley_box' => !empty($editorOptions['disable_smiley_box']),
        'columns' => isset($editorOptions['columns']) ? $editorOptions['columns'] : 60,
        'rows' => isset($editorOptions['rows']) ? $editorOptions['rows'] : 18,
        'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '70%',
        'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '250px',
        'form' => isset($editorOptions['form']) ? $editorOptions['form'] : 'postmodify',
        'bbc_level' => !empty($editorOptions['bbc_level']) ? $editorOptions['bbc_level'] : 'full',
        'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1,
        'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : array(),
        'locale' => !empty($txt['lang_locale']) && substr($txt['lang_locale'], 0, 5) != 'en_US' ? $txt['lang_locale'] : '',
        'required' => !empty($editorOptions['required']),
    );

    if (empty($context['bbc_tags']))
    {
        // The below array makes it dead easy to add images to this control. Add it to the array and everything else is done for you!
        // Note: 'before' and 'after' are deprecated as of SMF 2.1. Instead, use a separate JS file to configure the functionality of your toolbar buttons.
        /*
            array(
                'code' => 'b', // Required
                'description' => $editortxt['bold'], // Required
                'image' => 'bold', // Optional
                'before' => '[b]', // Deprecated
                'after' => '[/b]', // Deprecated
            ),
        */
        $context['bbc_tags'] = array();
        $context['bbc_tags'][] = array(
            array(
                'code' => 'bold',
                'description' => $editortxt['bold'],
            ),
            array(
                'code' => 'italic',
                'description' => $editortxt['italic'],
            ),
            array(
                'code' => 'underline',
                'description' => $editortxt['underline']
            ),
            array(
                'code' => 'strike',
                'description' => $editortxt['strikethrough']
            ),
            array(
                'code' => 'superscript',
                'description' => $editortxt['superscript']
            ),
            array(
                'code' => 'subscript',
                'description' => $editortxt['subscript']
            ),
            array(),
            array(
                'code' => 'pre',
                'description' => $editortxt['preformatted_text']
            ),
            array(
                'code' => 'left',
                'description' => $editortxt['align_left']
            ),
            array(
                'code' => 'center',
                'description' => $editortxt['center']
            ),
            array(
                'code' => 'right',
                'description' => $editortxt['align_right']
            ),
            array(
                'code' => 'justify',
                'description' => $editortxt['justify']
            ),
            array(),
            array(
                'code' => 'font',
                'description' => $editortxt['font_name']
            ),
            array(
                'code' => 'size',
                'description' => $editortxt['font_size']
            ),
            array(
                'code' => 'color',
                'description' => $editortxt['font_color']
            ),
        );
        if (empty($modSettings['disable_wysiwyg']))
        {
            $context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
                'code' => 'removeformat',
                'description' => $editortxt['remove_formatting'],
            );
        }
        $context['bbc_tags'][] = array(
            array(
                'code' => 'floatleft',
                'description' => $editortxt['float_left']
            ),
            array(
                'code' => 'floatright',
                'description' => $editortxt['float_right']
            ),
            array(),
            array(
                'code' => 'youtube',
                'description' => $editortxt['insert_youtube_video']
            ),
            array(
                'code' => 'image',
                'description' => $editortxt['insert_image']
            ),
            array(
                'code' => 'link',
                'description' => $editortxt['insert_link']
            ),
            array(
                'code' => 'email',
                'description' => $editortxt['insert_email']
            ),
            array(),
            array(
                'code' => 'table',
                'description' => $editortxt['insert_table']
            ),
            array(
                'code' => 'code',
                'description' => $editortxt['code']
            ),
            array(
                'code' => 'quote',
                'description' => $editortxt['insert_quote']
            ),
            array(),
            array(
                'code' => 'bulletlist',
                'description' => $editortxt['bullet_list']
            ),
            array(
                'code' => 'orderedlist',
                'description' => $editortxt['numbered_list']
            ),
            array(
                'code' => 'horizontalrule',
                'description' => $editortxt['insert_horizontal_rule']
            ),
            array(),
            array(
                'code' => 'maximize',
                'description' => $editortxt['maximize']
            ),
        );
        if (empty($modSettings['disable_wysiwyg']))
        {
            $context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
                'code' => 'source',
                'description' => $editortxt['view_source'],
            );
        }

        $editor_tag_map = array(
            'b' => 'bold',
            'i' => 'italic',
            'u' => 'underline',
            's' => 'strike',
            'img' => 'image',
            'url' => 'link',
            'sup' => 'superscript',
            'sub' => 'subscript',
            'hr' => 'horizontalrule',
        );

        // Allow mods to modify BBC buttons.
        // Note: passing the array here is not necessary and is deprecated, but it is kept for backward compatibility with 2.0
        call_integration_hook('integrate_bbc_buttons', array(&$context['bbc_tags'], &$editor_tag_map));

        // Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this.
        $disabled_tags = array();
        if (!empty($modSettings['disabledBBC']))
            $disabled_tags = explode(',', $modSettings['disabledBBC']);

        foreach ($disabled_tags as $tag)
        {
            $tag = trim($tag);

            if ($tag === 'list')
            {
                $context['disabled_tags']['bulletlist'] = true;
                $context['disabled_tags']['orderedlist'] = true;
            }

            foreach ($editor_tag_map as $thisTag => $tagNameBBC)
                if ($tag === $thisTag)
                    $context['disabled_tags'][$tagNameBBC] = true;

            $context['disabled_tags'][$tag] = true;
        }

        $bbcodes_styles = '';
        $context['bbcodes_handlers'] = '';
        $context['bbc_toolbar'] = array();

        foreach ($context['bbc_tags'] as $row => $tagRow)
        {
            if (!isset($context['bbc_toolbar'][$row]))
                $context['bbc_toolbar'][$row] = array();

            $tagsRow = array();

            foreach ($tagRow as $tag)
            {
                if ((!empty($tag['code'])) && empty($context['disabled_tags'][$tag['code']]))
                {
                    $tagsRow[] = $tag['code'];

                    // If we have a custom button image, set it now.
                    if (isset($tag['image']))
                    {
                        $bbcodes_styles .= '
                        .sceditor-button-' . $tag['code'] . ' div {
                            background: url(\'' . $settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\');
                        }';
                    }

                    // Set the tooltip and possibly the command info
                    $context['bbcodes_handlers'] .= '
                        sceditor.command.set(' . JavaScriptEscape($tag['code']) . ', {
                            tooltip: ' . JavaScriptEscape(isset($tag['description']) ? $tag['description'] : $tag['code']);

                    // Legacy support for 2.0 BBC mods
                    if (isset($tag['before']))
                    {
                        $context['bbcodes_handlers'] .= ',
                            exec: function () {
                                this.insert(' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ');
                            },
                            txtExec: [' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ']';
                    }

                    $context['bbcodes_handlers'] .= '
                        });';
                }
                else
                {
                    $context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
                    $tagsRow = array();
                }
            }

            if (!empty($tagsRow))
                $context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
        }

        if (!empty($bbcodes_styles))
            addInlineCss($bbcodes_styles);
    }

    // Initialize smiley array... if not loaded before.
    if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box']))
    {
        $context['smileys'] = array(
            'postform' => array(),
            'popup' => array(),
        );

        if ($user_info['smiley_set'] != 'none')
        {
            // Cache for longer when customized smiley codes aren't enabled
            $cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;

            if (($temp = cache_get_data('posting_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
            {
                $request = $smcFunc['db_query']('', '
                    SELECT s.code, f.filename, s.description, s.smiley_row, s.hidden
                    FROM {db_prefix}smileys AS s
                        JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
                    WHERE s.hidden IN (0, 2)
                        AND f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
                        AND s.code IN ({array_string:default_codes})' : '') . '
                    ORDER BY s.smiley_row, s.smiley_order',
                    array(
                        'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
                        'smiley_set' => $user_info['smiley_set'],
                    )
                );
                while ($row = $smcFunc['db_fetch_assoc']($request))
                {
                    $row['description'] = !empty($txt['icon_' . strtolower($row['description'])]) ? $smcFunc['htmlspecialchars']($txt['icon_' . strtolower($row['description'])]) : $smcFunc['htmlspecialchars']($row['description']);

                    $context['smileys'][empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row;
                }
                $smcFunc['db_free_result']($request);

                foreach ($context['smileys'] as $section => $smileyRows)
                {
                    foreach ($smileyRows as $rowIndex => $smileys)
                        $context['smileys'][$section][$rowIndex]['smileys'][count($smileys['smileys']) - 1]['isLast'] = true;

                    if (!empty($smileyRows))
                        $context['smileys'][$section][count($smileyRows) - 1]['isLast'] = true;
                }

                cache_put_data('posting_smileys_' . $user_info['smiley_set'], $context['smileys'], $cache_time);
            }
            else
                $context['smileys'] = $temp;
        }
    }

    // Set a flag so the sub template knows what to do...
    $context['show_bbc'] = !empty($modSettings['enableBBC']);

    // Set up the SCEditor options
    $sce_options = array(
        'style' => $settings['default_theme_url'] . '/css/jquery.sceditor.default.css',
        'emoticonsCompat' => true,
        'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple',
        'format' => 'bbcode',
        'plugins' => '',
        'bbcodeTrim' => true,
    );
    if (!empty($context['controls']['richedit'][$editorOptions['id']]['locale']))
        $sce_options['locale'] = $context['controls']['richedit'][$editorOptions['id']]['locale'];
    if (!empty($context['right_to_left']))
        $sce_options['rtl'] = true;
    if ($editorOptions['id'] != 'quickReply')
        $sce_options['autofocus'] = true;

    $sce_options['emoticons'] = array();
    $sce_options['emoticonsDescriptions'] = array();
    $sce_options['emoticonsEnabled'] = false;
    if ((!empty($context['smileys']['postform']) || !empty($context['smileys']['popup'])) && !$context['controls']['richedit'][$editorOptions['id']]['disable_smiley_box'])
    {
        $sce_options['emoticonsEnabled'] = true;
        $sce_options['emoticons']['dropdown'] = array();
        $sce_options['emoticons']['popup'] = array();

        $countLocations = count($context['smileys']);
        foreach ($context['smileys'] as $location => $smileyRows)
        {
            $countLocations--;

            unset($smiley_location);
            if ($location == 'postform')
                $smiley_location = &$sce_options['emoticons']['dropdown'];
            elseif ($location == 'popup')
                $smiley_location = &$sce_options['emoticons']['popup'];

            $numRows = count($smileyRows);

            // This is needed because otherwise the editor will remove all the duplicate (empty) keys and leave only 1 additional line
            $emptyPlaceholder = 0;
            foreach ($smileyRows as $smileyRow)
            {
                foreach ($smileyRow['smileys'] as $smiley)
                {
                    $smiley_location[$smiley['code']] = $settings['smileys_url'] . '/' . $smiley['filename'];
                    $sce_options['emoticonsDescriptions'][$smiley['code']] = $smiley['description'];
                }

                if (empty($smileyRow['isLast']) && $numRows != 1)
                    $smiley_location['-' . $emptyPlaceholder++] = '';
            }
        }
    }

    $sce_options['toolbar'] = '';
    if ($context['show_bbc'])
    {
        $count_tags = count($context['bbc_tags']);
        foreach ($context['bbc_toolbar'] as $i => $buttonRow)
        {
            $sce_options['toolbar'] .= implode('|', $buttonRow);

            $count_tags--;

            if (!empty($count_tags))
                $sce_options['toolbar'] .= '||';
        }
    }

    // Allow mods to change $sce_options. Usful if, e.g., a mod wants to add an SCEditor plugin.
    call_integration_hook('integrate_sceditor_options', array(&$sce_options));

    $context['controls']['richedit'][$editorOptions['id']]['sce_options'] = $sce_options;
}

/**
 * Create a anti-bot verification control?
 *
 * @param array &$verificationOptions Options for the verification control
 * @param bool $do_test Whether to check to see if the user entered the code correctly
 * @return bool|array False if there's nothing to show, true if everything went well or an array containing error indicators if the test failed
 */
function create_control_verification(&$verificationOptions, $do_test = false)
{
    global $modSettings, $smcFunc;
    global $context, $user_info, $scripturl, $language;

    // First verification means we need to set up some bits...
    if (empty($context['controls']['verification']))
    {
        // The template
        loadTemplate('GenericControls');

        // Some javascript ma'am?
        if (!empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])))
            loadJavaScriptFile('captcha.js', array('minimize' => true), 'smf_captcha');

        $context['use_graphic_library'] = in_array('gd', get_loaded_extensions());

        // Skip I, J, L, O, Q, S and Z.
        $context['standard_captcha_range'] = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y'));
    }

    // Always have an ID.
    assert(isset($verificationOptions['id']));
    $isNew = !isset($context['controls']['verification'][$verificationOptions['id']]);

    // Log this into our collection.
    if ($isNew)
        $context['controls']['verification'][$verificationOptions['id']] = array(
            'id' => $verificationOptions['id'],
            'empty_field' => empty($verificationOptions['no_empty_field']),
            'show_visual' => !empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])),
            'number_questions' => isset($verificationOptions['override_qs']) ? $verificationOptions['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0),
            'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3,
            'image_href' => $scripturl . '?action=verificationcode;vid=' . $verificationOptions['id'] . ';rand=' . md5(mt_rand()),
            'text_value' => '',
            'questions' => array(),
            'can_recaptcha' => !empty($modSettings['recaptcha_enabled']) && !empty($modSettings['recaptcha_site_key']) && !empty($modSettings['recaptcha_secret_key']),
        );
    $thisVerification = &$context['controls']['verification'][$verificationOptions['id']];

    // Is there actually going to be anything?
    if (empty($thisVerification['show_visual']) && empty($thisVerification['number_questions']) && empty($thisVerification['can_recaptcha']))
        return false;
    elseif (!$isNew && !$do_test)
        return true;

    // Sanitize reCAPTCHA fields?
    if ($thisVerification['can_recaptcha'])
    {
        // Only allow 40 alphanumeric, underscore and dash characters.
        $thisVerification['recaptcha_site_key'] = preg_replace('/(0-9a-zA-Z_){40}/', '$1', $modSettings['recaptcha_site_key']);

        // Light or dark theme...
        $thisVerification['recaptcha_theme'] = preg_replace('/(light|dark)/', '$1', $modSettings['recaptcha_theme']);
    }

    // Add javascript for the object.
    if ($context['controls']['verification'][$verificationOptions['id']]['show_visual'])
        $context['insert_after_template'] .= '
            <script>
                var verification' . $verificationOptions['id'] . 'Handle = new smfCaptcha("' . $thisVerification['image_href'] . '", "' . $verificationOptions['id'] . '", ' . ($context['use_graphic_library'] ? 1 : 0) . ');
            </script>';

    // If we want questions do we have a cache of all the IDs?
    if (!empty($thisVerification['number_questions']) && empty($modSettings['question_id_cache']))
    {
        if (($modSettings['question_id_cache'] = cache_get_data('verificationQuestions', 300)) == null)
        {
            $request = $smcFunc['db_query']('', '
                SELECT id_question, lngfile, question, answers
                FROM {db_prefix}qanda',
                array()
            );
            $modSettings['question_id_cache'] = array(
                'questions' => array(),
                'langs' => array(),
            );
            // This is like Captain Kirk climbing a mountain in some ways. This is L's fault, mkay? :P
            while ($row = $smcFunc['db_fetch_assoc']($request))
            {
                $id_question = $row['id_question'];
                unset ($row['id_question']);
                // Make them all lowercase. We can't directly use $smcFunc['strtolower'] with array_walk, so do it manually, eh?
                $row['answers'] = $smcFunc['json_decode']($row['answers'], true);
                foreach ($row['answers'] as $k => $v)
                    $row['answers'][$k] = $smcFunc['strtolower']($v);

                $modSettings['question_id_cache']['questions'][$id_question] = $row;
                $modSettings['question_id_cache']['langs'][$row['lngfile']][] = $id_question;
            }
            $smcFunc['db_free_result']($request);

            cache_put_data('verificationQuestions', $modSettings['question_id_cache'], 300);
        }
    }

    if (!isset($_SESSION[$verificationOptions['id'] . '_vv']))
        $_SESSION[$verificationOptions['id'] . '_vv'] = array();

    // Do we need to refresh the verification?
    if (!$do_test && (!empty($_SESSION[$verificationOptions['id'] . '_vv']['did_pass']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) || $_SESSION[$verificationOptions['id'] . '_vv']['count'] > 3) && empty($verificationOptions['dont_refresh']))
        $force_refresh = true;
    else
        $force_refresh = false;

    // This can also force a fresh, although unlikely.
    if (($thisVerification['show_visual'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['code'])) || ($thisVerification['number_questions'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['q'])))
        $force_refresh = true;

    $verification_errors = array();
    // Start with any testing.
    if ($do_test)
    {
        // This cannot happen!
        if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count']))
            fatal_lang_error('no_access', false);
        // ... nor this!
        if ($thisVerification['number_questions'] && (!isset($_SESSION[$verificationOptions['id'] . '_vv']['q']) || !isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'])))
            fatal_lang_error('no_access', false);
        // Hmm, it's requested but not actually declared. This shouldn't happen.
        if ($thisVerification['empty_field'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
            fatal_lang_error('no_access', false);
        // While we're here, did the user do something bad?
        if ($thisVerification['empty_field'] && !empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']) && !empty($_REQUEST[$_SESSION[$verificationOptions['id'] . '_vv']['empty_field']]))
            $verification_errors[] = 'wrong_verification_answer';

        if ($thisVerification['can_recaptcha'])
        {
            $reCaptcha = new \ReCaptcha\ReCaptcha($modSettings['recaptcha_secret_key'], new \ReCaptcha\RequestMethod\SocketPost());

            // Was there a reCAPTCHA response?
            if (isset($_POST['g-recaptcha-response']))
            {
                $resp = $reCaptcha->verify($_POST['g-recaptcha-response'], $user_info['ip']);

                if (!$resp->isSuccess())
                    $verification_errors[] = 'wrong_verification_code';
            }
            else
                $verification_errors[] = 'wrong_verification_code';
        }
        if ($thisVerification['show_visual'] && (empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['code']) || strtoupper($_REQUEST[$verificationOptions['id'] . '_vv']['code']) !== $_SESSION[$verificationOptions['id'] . '_vv']['code']))
            $verification_errors[] = 'wrong_verification_code';
        if ($thisVerification['number_questions'])
        {
            $incorrectQuestions = array();
            foreach ($_SESSION[$verificationOptions['id'] . '_vv']['q'] as $q)
            {
                // We don't have this question any more, thus no answers.
                if (!isset($modSettings['question_id_cache']['questions'][$q]))
                    continue;
                // This is quite complex. We have our question but it might have multiple answers.
                // First, did they actually answer this question?
                if (!isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) || trim($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) == '')
                {
                    $incorrectQuestions[] = $q;
                    continue;
                }
                // Second, is their answer in the list of possible answers?
                else
                {
                    $given_answer = trim($smcFunc['htmlspecialchars'](strtolower($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q])));
                    if (!in_array($given_answer, $modSettings['question_id_cache']['questions'][$q]['answers']))
                        $incorrectQuestions[] = $q;
                }
            }

            if (!empty($incorrectQuestions))
                $verification_errors[] = 'wrong_verification_answer';
        }
    }

    // Any errors means we refresh potentially.
    if (!empty($verification_errors))
    {
        if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors']))
            $_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
        // Too many errors?
        elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors'])
            $force_refresh = true;

        // Keep a track of these.
        $_SESSION[$verificationOptions['id'] . '_vv']['errors']++;
    }

    // Are we refreshing then?
    if ($force_refresh)
    {
        // Assume nothing went before.
        $_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0;
        $_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
        $_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false;
        $_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
        $_SESSION[$verificationOptions['id'] . '_vv']['code'] = '';

        // Make our magic empty field.
        if ($thisVerification['empty_field'])
        {
            // We're building a field that lives in the template, that we hope to be empty later. But at least we give it a believable name.
            $terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier');
            $second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value');
            $start = mt_rand(0, 27);
            $hash = substr(md5(time()), $start, 4);
            $_SESSION[$verificationOptions['id'] . '_vv']['empty_field'] = $terms[array_rand($terms)] . '-' . $second_terms[array_rand($second_terms)] . '-' . $hash;
        }

        // Generating a new image.
        if ($thisVerification['show_visual'])
        {
            // Are we overriding the range?
            $character_range = !empty($verificationOptions['override_range']) ? $verificationOptions['override_range'] : $context['standard_captcha_range'];

            for ($i = 0; $i < 6; $i++)
                $_SESSION[$verificationOptions['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)];
        }

        // Getting some new questions?
        if ($thisVerification['number_questions'])
        {
            // Attempt to try the current page's language, followed by the user's preference, followed by the site default.
            $possible_langs = array();
            if (isset($_SESSION['language']))
                $possible_langs[] = strtr($_SESSION['language'], array('-utf8' => ''));
            if (!empty($user_info['language']))
                $possible_langs[] = $user_info['language'];

            $possible_langs[] = $language;

            $questionIDs = array();
            foreach ($possible_langs as $lang)
            {
                $lang = strtr($lang, array('-utf8' => ''));
                if (isset($modSettings['question_id_cache']['langs'][$lang]))
                {
                    // If we find questions for this, grab the ids from this language's ones, randomize the array and take just the number we need.
                    $questionIDs = $modSettings['question_id_cache']['langs'][$lang];
                    shuffle($questionIDs);
                    $questionIDs = array_slice($questionIDs, 0, $thisVerification['number_questions']);
                    break;
                }
            }
        }
    }
    else
    {
        // Same questions as before.
        $questionIDs = !empty($_SESSION[$verificationOptions['id'] . '_vv']['q']) ? $_SESSION[$verificationOptions['id'] . '_vv']['q'] : array();
        $thisVerification['text_value'] = !empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['code']) : '';
    }

    // If we do have an empty field, it would be nice to hide it from legitimate users who shouldn't be populating it anyway.
    if (!empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
    {
        if (!isset($context['html_headers']))
            $context['html_headers'] = '';
        $context['html_headers'] .= '<style>.vv_special { display:none; }</style>';
    }

    // Have we got some questions to load?
    if (!empty($questionIDs))
    {
        $_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
        foreach ($questionIDs as $q)
        {
            // Bit of a shortcut this.
            $row = &$modSettings['question_id_cache']['questions'][$q];
            $thisVerification['questions'][] = array(
                'id' => $q,
                'q' => parse_bbc($row['question']),
                'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions),
                // Remember a previous submission?
                'a' => isset($_REQUEST[$verificationOptions['id'] . '_vv'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) : '',
            );
            $_SESSION[$verificationOptions['id'] . '_vv']['q'][] = $q;
        }
    }

    $_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1;

    // Return errors if we have them.
    if (!empty($verification_errors))
        return $verification_errors;
    // If we had a test that one, make a note.
    elseif ($do_test)
        $_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true;

    // Say that everything went well chaps.
    return true;
}

/**
 * This keeps track of all registered handling functions for auto suggest functionality and passes execution to them.
 *
 * @param bool $checkRegistered If set to something other than null, checks whether the callback function is registered
 * @return void|bool Returns whether the callback function is registered if $checkRegistered isn't null
 */
function AutoSuggestHandler($checkRegistered = null)
{
    global $smcFunc, $context;

    // These are all registered types.
    $searchTypes = array(
        'member' => 'Member',
        'membergroups' => 'MemberGroups',
        'versions' => 'SMFVersions',
    );

    call_integration_hook('integrate_autosuggest', array(&$searchTypes));

    // If we're just checking the callback function is registered return true or false.
    if ($checkRegistered != null)
        return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . $checkRegistered);

    checkSession('get');
    loadTemplate('Xml');

    // Any parameters?
    $context['search_param'] = isset($_REQUEST['search_param']) ? $smcFunc['json_decode'](base64_decode($_REQUEST['search_param']), true) : array();

    if (isset($_REQUEST['suggest_type'], $_REQUEST['search']) && isset($searchTypes[$_REQUEST['suggest_type']]))
    {
        $function = 'AutoSuggest_Search_' . $searchTypes[$_REQUEST['suggest_type']];
        $context['sub_template'] = 'generic_xml';
        $context['xml_data'] = $function();
    }
}

/**
 * Search for a member - by real_name or member_name by default.
 *
 * @return array An array of information for displaying the suggestions
 */
function AutoSuggest_Search_Member()
{
    global $user_info, $smcFunc, $context;

    $_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
    $_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));

    // Find the member.
    $request = $smcFunc['db_query']('', '
        SELECT id_member, real_name
        FROM {db_prefix}members
        WHERE {raw:real_name} LIKE {string:search}' . (!empty($context['search_param']['buddies']) ? '
            AND id_member IN ({array_int:buddy_list})' : '') . '
            AND is_activated IN (1, 11)
        LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'),
        array(
            'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name',
            'buddy_list' => $user_info['buddies'],
            'search' => $_REQUEST['search'],
        )
    );
    $xml_data = array(
        'items' => array(
            'identifier' => 'item',
            'children' => array(),
        ),
    );
    while ($row = $smcFunc['db_fetch_assoc']($request))
    {
        $row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));

        $xml_data['items']['children'][] = array(
            'attributes' => array(
                'id' => $row['id_member'],
            ),
            'value' => $row['real_name'],
        );
    }
    $smcFunc['db_free_result']($request);

    return $xml_data;
}

/**
 * Search for a membergroup by name
 *
 * @return array An array of information for displaying the suggestions
 */
function AutoSuggest_Search_MemberGroups()
{
    global $smcFunc;

    $_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
    $_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));

    // Find the group.
    // Only return groups which are not post-based and not "Hidden", but not the "Administrators" or "Moderators" groups.
    $request = $smcFunc['db_query']('', '
        SELECT id_group, group_name
        FROM {db_prefix}membergroups
        WHERE {raw:group_name} LIKE {string:search}
            AND min_posts = {int:min_posts}
            AND id_group NOT IN ({array_int:invalid_groups})
            AND hidden != {int:hidden}',
        array(
            'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name}' : 'group_name',
            'min_posts' => -1,
            'invalid_groups' => array(1, 3),
            'hidden' => 2,
            'search' => $_REQUEST['search'],
        )
    );
    $xml_data = array(
        'items' => array(
            'identifier' => 'item',
            'children' => array(),
        ),
    );
    while ($row = $smcFunc['db_fetch_assoc']($request))
    {
        $row['group_name'] = strtr($row['group_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));

        $xml_data['items']['children'][] = array(
            'attributes' => array(
                'id' => $row['id_group'],
            ),
            'value' => $row['group_name'],
        );
    }
    $smcFunc['db_free_result']($request);

    return $xml_data;
}

/**
 * Provides a list of possible SMF versions to use in emulation
 *
 * @return array An array of data for displaying the suggestions
 */
function AutoSuggest_Search_SMFVersions()
{
    global $smcFunc;

    $xml_data = array(
        'items' => array(
            'identifier' => 'item',
            'children' => array(),
        ),
    );

    // First try and get it from the database.
    $versions = array();
    $request = $smcFunc['db_query']('', '
        SELECT data
        FROM {db_prefix}admin_info_files
        WHERE filename = {string:latest_versions}
            AND path = {string:path}',
        array(
            'latest_versions' => 'latest-versions.txt',
            'path' => '/smf/',
        )
    );
    if (($smcFunc['db_num_rows']($request) > 0) && ($row = $smcFunc['db_fetch_assoc']($request)) && !empty($row['data']))
    {
        // The file can be either Windows or Linux line endings, but let's ensure we clean it as best we can.
        $possible_versions = explode("\n", $row['data']);
        foreach ($possible_versions as $ver)
        {
            $ver = trim($ver);
            if (strpos($ver, 'SMF') === 0)
                $versions[] = $ver;
        }
    }
    $smcFunc['db_free_result']($request);

    // Just in case we don't have ANYthing.
    if (empty($versions))
        $versions = array('SMF 2.0');

    foreach ($versions as $id => $version)
        if (strpos($version, strtoupper($_REQUEST['search'])) !== false)
            $xml_data['items']['children'][] = array(
                'attributes' => array(
                    'id' => $id,
                ),
                'value' => $version,
            );

    return $xml_data;
}

?>