/usr/share/pyshared/MoinMoin/wikiutil.py is in python-moinmoin 1.9.3-1ubuntu2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 | # -*- coding: iso-8859-1 -*-
"""
MoinMoin - Wiki Utility Functions
@copyright: 2000-2004 Juergen Hermann <jh@web.de>,
2004 by Florian Festi,
2006 by Mikko Virkkil,
2005-2008 MoinMoin:ThomasWaldmann,
2007 MoinMoin:ReimarBauer
@license: GNU GPL, see COPYING for details.
"""
import cgi
import codecs
import os
import re
import time
import urllib
from MoinMoin import log
logging = log.getLogger(__name__)
from MoinMoin import config
from MoinMoin.util import pysupport, lock
from MoinMoin.support.python_compatibility import rsplit
from inspect import getargspec, isfunction, isclass, ismethod
from MoinMoin import web # needed so that next line works:
import werkzeug
# Exceptions
class InvalidFileNameError(Exception):
""" Called when we find an invalid file name """
pass
# constants for page names
PARENT_PREFIX = "../"
PARENT_PREFIX_LEN = len(PARENT_PREFIX)
CHILD_PREFIX = "/"
CHILD_PREFIX_LEN = len(CHILD_PREFIX)
#############################################################################
### Getting data from user/Sending data to user
#############################################################################
def decodeUnknownInput(text):
""" Decode unknown input, like text attachments
First we try utf-8 because it has special format, and it will decode
only utf-8 files. Then we try config.charset, then iso-8859-1 using
'replace'. We will never raise an exception, but may return junk
data.
WARNING: Use this function only for data that you view, not for data
that you save in the wiki.
@param text: the text to decode, string
@rtype: unicode
@return: decoded text (maybe wrong)
"""
# Shortcut for unicode input
if isinstance(text, unicode):
return text
try:
return unicode(text, 'utf-8')
except UnicodeError:
if config.charset not in ['utf-8', 'iso-8859-1']:
try:
return unicode(text, config.charset)
except UnicodeError:
pass
return unicode(text, 'iso-8859-1', 'replace')
def decodeUserInput(s, charsets=[config.charset]):
"""
Decodes input from the user.
@param s: the string to unquote
@param charsets: list of charsets to assume the string is in
@rtype: unicode
@return: the unquoted string as unicode
"""
for charset in charsets:
try:
return s.decode(charset)
except UnicodeError:
pass
raise UnicodeError('The string %r cannot be decoded.' % s)
def url_quote(s, safe='/', want_unicode=None):
""" see werkzeug.url_quote, we use a different safe param default value """
try:
assert want_unicode is None
except AssertionError:
log.exception("call with deprecated want_unicode param, please fix caller")
return werkzeug.url_quote(s, charset=config.charset, safe=safe)
def url_quote_plus(s, safe='/', want_unicode=None):
""" see werkzeug.url_quote_plus, we use a different safe param default value """
try:
assert want_unicode is None
except AssertionError:
log.exception("call with deprecated want_unicode param, please fix caller")
return werkzeug.url_quote_plus(s, charset=config.charset, safe=safe)
def url_unquote(s, want_unicode=None):
""" see werkzeug.url_unquote """
try:
assert want_unicode is None
except AssertionError:
log.exception("call with deprecated want_unicode param, please fix caller")
if isinstance(s, unicode):
s = s.encode(config.charset)
return werkzeug.url_unquote(s, charset=config.charset, errors='fallback:iso-8859-1')
def parseQueryString(qstr, want_unicode=None):
""" see werkzeug.url_decode
Please note: this returns a MultiDict, you might need to use dict() on
the result if your code expects a "normal" dict.
"""
try:
assert want_unicode is None
except AssertionError:
log.exception("call with deprecated want_unicode param, please fix caller")
return werkzeug.url_decode(qstr, charset=config.charset, errors='fallback:iso-8859-1',
decode_keys=False, include_empty=False)
def makeQueryString(qstr=None, want_unicode=None, **kw):
""" Make a querystring from arguments.
kw arguments overide values in qstr.
If a string is passed in, it's returned verbatim and keyword parameters are ignored.
See also: werkzeug.url_encode
@param qstr: dict to format as query string, using either ascii or unicode
@param kw: same as dict when using keywords, using ascii or unicode
@rtype: string
@return: query string ready to use in a url
"""
try:
assert want_unicode is None
except AssertionError:
log.exception("call with deprecated want_unicode param, please fix caller")
if qstr is None:
qstr = {}
elif isinstance(qstr, (str, unicode)):
return qstr
if isinstance(qstr, dict):
qstr.update(kw)
return werkzeug.url_encode(qstr, charset=config.charset, encode_keys=True)
else:
raise ValueError("Unsupported argument type, should be dict.")
def quoteWikinameURL(pagename, charset=config.charset):
""" Return a url encoding of filename in plain ascii
Use urllib.quote to quote any character that is not always safe.
@param pagename: the original pagename (unicode)
@param charset: url text encoding, 'utf-8' recommended. Other charset
might not be able to encode the page name and raise
UnicodeError. (default config.charset ('utf-8')).
@rtype: string
@return: the quoted filename, all unsafe characters encoded
"""
# XXX please note that urllib.quote and werkzeug.url_quote have
# XXX different defaults for safe=...
return werkzeug.url_quote(pagename, charset=charset, safe='/')
escape = werkzeug.escape
def clean_input(text, max_len=201):
""" Clean input:
replace CR, LF, TAB by whitespace
delete control chars
@param text: unicode text to clean (if we get str, we decode)
@rtype: unicode
@return: cleaned text
"""
# we only have input fields with max 200 chars, but spammers send us more
length = len(text)
if length == 0 or length > max_len:
return u''
else:
if isinstance(text, str):
# the translate() below can ONLY process unicode, thus, if we get
# str, we try to decode it using the usual coding:
text = text.decode(config.charset)
return text.translate(config.clean_input_translation_map)
def make_breakable(text, maxlen):
""" make a text breakable by inserting spaces into nonbreakable parts
"""
text = text.split(" ")
newtext = []
for part in text:
if len(part) > maxlen:
while part:
newtext.append(part[:maxlen])
part = part[maxlen:]
else:
newtext.append(part)
return " ".join(newtext)
########################################################################
### Storage
########################################################################
# Precompiled patterns for file name [un]quoting
UNSAFE = re.compile(r'[^a-zA-Z0-9_]+')
QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)')
def quoteWikinameFS(wikiname, charset=config.charset):
""" Return file system representation of a Unicode WikiName.
Warning: will raise UnicodeError if wikiname can not be encoded using
charset. The default value of config.charset, 'utf-8' can encode any
character.
@param wikiname: Unicode string possibly containing non-ascii characters
@param charset: charset to encode string
@rtype: string
@return: quoted name, safe for any file system
"""
filename = wikiname.encode(charset)
quoted = []
location = 0
for needle in UNSAFE.finditer(filename):
# append leading safe stuff
quoted.append(filename[location:needle.start()])
location = needle.end()
# Quote and append unsafe stuff
quoted.append('(')
for character in needle.group():
quoted.append('%02x' % ord(character))
quoted.append(')')
# append rest of string
quoted.append(filename[location:])
return ''.join(quoted)
def unquoteWikiname(filename, charsets=[config.charset]):
""" Return Unicode WikiName from quoted file name.
We raise an InvalidFileNameError if we find an invalid name, so the
wiki could alarm the admin or suggest the user to rename a page.
Invalid file names should never happen in normal use, but are rather
cheap to find.
This function should be used only to unquote file names, not page
names we receive from the user. These are handled in request by
urllib.unquote, decodePagename and normalizePagename.
Todo: search clients of unquoteWikiname and check for exceptions.
@param filename: string using charset and possibly quoted parts
@param charsets: list of charsets used by string
@rtype: Unicode String
@return: WikiName
"""
### Temporary fix start ###
# From some places we get called with Unicode strings
if isinstance(filename, type(u'')):
filename = filename.encode(config.charset)
### Temporary fix end ###
parts = []
start = 0
for needle in QUOTED.finditer(filename):
# append leading unquoted stuff
parts.append(filename[start:needle.start()])
start = needle.end()
# Append quoted stuff
group = needle.group(1)
# Filter invalid filenames
if (len(group) % 2 != 0):
raise InvalidFileNameError(filename)
try:
for i in range(0, len(group), 2):
byte = group[i:i+2]
character = chr(int(byte, 16))
parts.append(character)
except ValueError:
# byte not in hex, e.g 'xy'
raise InvalidFileNameError(filename)
# append rest of string
if start == 0:
wikiname = filename
else:
parts.append(filename[start:len(filename)])
wikiname = ''.join(parts)
# FIXME: This looks wrong, because at this stage "()" can be both errors
# like open "(" without close ")", or unquoted valid characters in the file name.
# Filter invalid filenames. Any left (xx) must be invalid
#if '(' in wikiname or ')' in wikiname:
# raise InvalidFileNameError(filename)
wikiname = decodeUserInput(wikiname, charsets)
return wikiname
# time scaling
def timestamp2version(ts):
""" Convert UNIX timestamp (may be float or int) to our version
(long) int.
We don't want to use floats, so we just scale by 1e6 to get
an integer in usecs.
"""
return long(ts*1000000L) # has to be long for py 2.2.x
def version2timestamp(v):
""" Convert version number to UNIX timestamp (float).
This must ONLY be used for display purposes.
"""
return v / 1000000.0
# This is the list of meta attribute names to be treated as integers.
# IMPORTANT: do not use any meta attribute names with "-" (or any other chars
# invalid in python attribute names), use e.g. _ instead.
INTEGER_METAS = ['current', 'revision', # for page storage (moin 2.0)
'data_format_revision', # for data_dir format spec (use by mig scripts)
]
class MetaDict(dict):
""" store meta informations as a dict.
"""
def __init__(self, metafilename, cache_directory):
""" create a MetaDict from metafilename """
dict.__init__(self)
self.metafilename = metafilename
self.dirty = False
lock_dir = os.path.join(cache_directory, '__metalock__')
self.rlock = lock.ReadLock(lock_dir, 60.0)
self.wlock = lock.WriteLock(lock_dir, 60.0)
if not self.rlock.acquire(3.0):
raise EnvironmentError("Could not lock in MetaDict")
try:
self._get_meta()
finally:
self.rlock.release()
def _get_meta(self):
""" get the meta dict from an arbitrary filename.
does not keep state, does uncached, direct disk access.
@param metafilename: the name of the file to read
@return: dict with all values or {} if empty or error
"""
try:
metafile = codecs.open(self.metafilename, "r", "utf-8")
meta = metafile.read() # this is much faster than the file's line-by-line iterator
metafile.close()
except IOError:
meta = u''
for line in meta.splitlines():
key, value = line.split(':', 1)
value = value.strip()
if key in INTEGER_METAS:
value = int(value)
dict.__setitem__(self, key, value)
def _put_meta(self):
""" put the meta dict into an arbitrary filename.
does not keep or modify state, does uncached, direct disk access.
@param metafilename: the name of the file to write
@param metadata: dict of the data to write to the file
"""
meta = []
for key, value in self.items():
if key in INTEGER_METAS:
value = str(value)
meta.append("%s: %s" % (key, value))
meta = '\r\n'.join(meta)
metafile = codecs.open(self.metafilename, "w", "utf-8")
metafile.write(meta)
metafile.close()
self.dirty = False
def sync(self, mtime_usecs=None):
""" No-Op except for that parameter """
if not mtime_usecs is None:
self.__setitem__('mtime', str(mtime_usecs))
# otherwise no-op
def __getitem__(self, key):
""" We don't care for cache coherency here. """
return dict.__getitem__(self, key)
def __setitem__(self, key, value):
""" Sets a dictionary entry. """
if not self.wlock.acquire(5.0):
raise EnvironmentError("Could not lock in MetaDict")
try:
self._get_meta() # refresh cache
try:
oldvalue = dict.__getitem__(self, key)
except KeyError:
oldvalue = None
if value != oldvalue:
dict.__setitem__(self, key, value)
self._put_meta() # sync cache
finally:
self.wlock.release()
# Quoting of wiki names, file names, etc. (in the wiki markup) -----------------------------------
# don't ever change this - DEPRECATED, only needed for 1.5 > 1.6 migration conversion
QUOTE_CHARS = u'"'
#############################################################################
### InterWiki
#############################################################################
INTERWIKI_PAGE = "InterWikiMap"
def generate_file_list(request):
""" generates a list of all files. for internal use. """
# order is important here, the local intermap file takes
# precedence over the shared one, and is thus read AFTER
# the shared one
intermap_files = request.cfg.shared_intermap
if not isinstance(intermap_files, list):
intermap_files = [intermap_files]
else:
intermap_files = intermap_files[:]
intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt"))
request.cfg.shared_intermap_files = [filename for filename in intermap_files
if filename and os.path.isfile(filename)]
def get_max_mtime(file_list, page):
""" Returns the highest modification time of the files in file_list and the
page page. """
timestamps = [os.stat(filename).st_mtime for filename in file_list]
if page.exists():
# exists() is cached and thus cheaper than mtime_usecs()
timestamps.append(version2timestamp(page.mtime_usecs()))
if timestamps:
return max(timestamps)
else:
return 0 # no files / pages there
def load_wikimap(request):
""" load interwiki map (once, and only on demand) """
from MoinMoin.Page import Page
now = int(time.time())
if getattr(request.cfg, "shared_intermap_files", None) is None:
generate_file_list(request)
try:
_interwiki_list = request.cfg.cache.interwiki_list
old_mtime = request.cfg.cache.interwiki_mtime
if request.cfg.cache.interwiki_ts + (1*60) < now: # 1 minutes caching time
max_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
if max_mtime > old_mtime:
raise AttributeError # refresh cache
else:
request.cfg.cache.interwiki_ts = now
except AttributeError:
_interwiki_list = {}
lines = []
for filename in request.cfg.shared_intermap_files:
f = codecs.open(filename, "r", config.charset)
lines.extend(f.readlines())
f.close()
# add the contents of the InterWikiMap page
lines += Page(request, INTERWIKI_PAGE).get_raw_body().splitlines()
for line in lines:
if not line or line[0] == '#':
continue
try:
line = "%s %s/InterWiki" % (line, request.script_root)
wikitag, urlprefix, dummy = line.split(None, 2)
except ValueError:
pass
else:
_interwiki_list[wikitag] = urlprefix
del lines
# add own wiki as "Self" and by its configured name
_interwiki_list['Self'] = request.script_root + '/'
if request.cfg.interwikiname:
_interwiki_list[request.cfg.interwikiname] = request.script_root + '/'
# save for later
request.cfg.cache.interwiki_list = _interwiki_list
request.cfg.cache.interwiki_ts = now
request.cfg.cache.interwiki_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
return _interwiki_list
def split_wiki(wikiurl):
"""
Split a wiki url.
*** DEPRECATED FUNCTION FOR OLD 1.5 SYNTAX - ONLY STILL HERE FOR THE 1.5 -> 1.6 MIGRATION ***
Use split_interwiki(), see below.
@param wikiurl: the url to split
@rtype: tuple
@return: (tag, tail)
"""
# !!! use a regex here!
try:
wikitag, tail = wikiurl.split(":", 1)
except ValueError:
try:
wikitag, tail = wikiurl.split("/", 1)
except ValueError:
wikitag, tail = 'Self', wikiurl
return wikitag, tail
def split_interwiki(wikiurl):
""" Split a interwiki name, into wikiname and pagename, e.g:
'MoinMoin:FrontPage' -> "MoinMoin", "FrontPage"
'FrontPage' -> "Self", "FrontPage"
'MoinMoin:Page with blanks' -> "MoinMoin", "Page with blanks"
'MoinMoin:' -> "MoinMoin", ""
can also be used for:
'attachment:filename with blanks.txt' -> "attachment", "filename with blanks.txt"
@param wikiurl: the url to split
@rtype: tuple
@return: (wikiname, pagename)
"""
try:
wikiname, pagename = wikiurl.split(":", 1)
except ValueError:
wikiname, pagename = 'Self', wikiurl
return wikiname, pagename
def resolve_wiki(request, wikiurl):
"""
Resolve an interwiki link.
*** DEPRECATED FUNCTION FOR OLD 1.5 SYNTAX - ONLY STILL HERE FOR THE 1.5 -> 1.6 MIGRATION ***
Use resolve_interwiki(), see below.
@param request: the request object
@param wikiurl: the InterWiki:PageName link
@rtype: tuple
@return: (wikitag, wikiurl, wikitail, err)
"""
_interwiki_list = load_wikimap(request)
# split wiki url
wikiname, pagename = split_wiki(wikiurl)
# return resolved url
if wikiname in _interwiki_list:
return (wikiname, _interwiki_list[wikiname], pagename, False)
else:
return (wikiname, request.script_root, "/InterWiki", True)
def resolve_interwiki(request, wikiname, pagename):
""" Resolve an interwiki reference (wikiname:pagename).
@param request: the request object
@param wikiname: interwiki wiki name
@param pagename: interwiki page name
@rtype: tuple
@return: (wikitag, wikiurl, wikitail, err)
"""
_interwiki_list = load_wikimap(request)
if wikiname in _interwiki_list:
return (wikiname, _interwiki_list[wikiname], pagename, False)
else:
return (wikiname, request.script_root, "/InterWiki", True)
def join_wiki(wikiurl, wikitail):
"""
Add a (url_quoted) page name to an interwiki url.
Note: We can't know what kind of URL quoting a remote wiki expects.
We just use a utf-8 encoded string with standard URL quoting.
@param wikiurl: wiki url, maybe including a $PAGE placeholder
@param wikitail: page name
@rtype: string
@return: generated URL of the page in the other wiki
"""
wikitail = url_quote(wikitail)
if '$PAGE' in wikiurl:
return wikiurl.replace('$PAGE', wikitail)
else:
return wikiurl + wikitail
#############################################################################
### Page types (based on page names)
#############################################################################
def isSystemPage(request, pagename):
""" Is this a system page?
@param request: the request object
@param pagename: the page name
@rtype: bool
@return: true if page is a system page
"""
from MoinMoin import i18n
return pagename in i18n.system_pages or isTemplatePage(request, pagename)
def isTemplatePage(request, pagename):
""" Is this a template page?
@param pagename: the page name
@rtype: bool
@return: true if page is a template page
"""
return request.cfg.cache.page_template_regexact.search(pagename) is not None
def isGroupPage(pagename, cfg):
""" Is this a name of group page?
@param pagename: the page name
@rtype: bool
@return: true if page is a form page
"""
return cfg.cache.page_group_regexact.search(pagename) is not None
def filterCategoryPages(request, pagelist):
""" Return category pages in pagelist
WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use
getPageList with a filter function.
If you pass a list with a single pagename, either that is returned
or an empty list, thus you can use this function like a `isCategoryPage`
one.
@param pagelist: a list of pages
@rtype: list
@return: only the category pages of pagelist
"""
func = request.cfg.cache.page_category_regexact.search
return [pn for pn in pagelist if func(pn)]
def getLocalizedPage(request, pagename): # was: getSysPage
""" Get a system page according to user settings and available translations.
We include some special treatment for the case that <pagename> is the
currently rendered page, as this is the case for some pages used very
often, like FrontPage, RecentChanges etc. - in that case we reuse the
already existing page object instead creating a new one.
@param request: the request object
@param pagename: the name of the page
@rtype: Page object
@return: the page object of that system page, using a translated page,
if it exists
"""
from MoinMoin.Page import Page
i18n_name = request.getText(pagename)
pageobj = None
if i18n_name != pagename:
if request.page and i18n_name == request.page.page_name:
# do not create new object for current page
i18n_page = request.page
if i18n_page.exists():
pageobj = i18n_page
else:
i18n_page = Page(request, i18n_name)
if i18n_page.exists():
pageobj = i18n_page
# if we failed getting a translated version of <pagename>,
# we fall back to english
if not pageobj:
if request.page and pagename == request.page.page_name:
# do not create new object for current page
pageobj = request.page
else:
pageobj = Page(request, pagename)
return pageobj
def getFrontPage(request):
""" Convenience function to get localized front page
@param request: current request
@rtype: Page object
@return localized page_front_page, if there is a translation
"""
return getLocalizedPage(request, request.cfg.page_front_page)
def getHomePage(request, username=None):
"""
Get a user's homepage, or return None for anon users and
those who have not created a homepage.
DEPRECATED - try to use getInterwikiHomePage (see below)
@param request: the request object
@param username: the user's name
@rtype: Page
@return: user's homepage object - or None
"""
from MoinMoin.Page import Page
# default to current user
if username is None and request.user.valid:
username = request.user.name
# known user?
if username:
# Return home page
page = Page(request, username)
if page.exists():
return page
return None
def getInterwikiHomePage(request, username=None):
"""
Get a user's homepage.
cfg.user_homewiki influences behaviour of this:
'Self' does mean we store user homepage in THIS wiki.
When set to our own interwikiname, it behaves like with 'Self'.
'SomeOtherWiki' means we store user homepages in another wiki.
@param request: the request object
@param username: the user's name
@rtype: tuple (or None for anon users)
@return: (wikiname, pagename)
"""
# default to current user
if username is None and request.user.valid:
username = request.user.name
if not username:
return None # anon user
homewiki = request.cfg.user_homewiki
if homewiki == request.cfg.interwikiname:
homewiki = u'Self'
return homewiki, username
def AbsPageName(context, pagename):
"""
Return the absolute pagename for a (possibly) relative pagename.
@param context: name of the page where "pagename" appears on
@param pagename: the (possibly relative) page name
@rtype: string
@return: the absolute page name
"""
if pagename.startswith(PARENT_PREFIX):
while context and pagename.startswith(PARENT_PREFIX):
context = '/'.join(context.split('/')[:-1])
pagename = pagename[PARENT_PREFIX_LEN:]
pagename = '/'.join(filter(None, [context, pagename, ]))
elif pagename.startswith(CHILD_PREFIX):
if context:
pagename = context + '/' + pagename[CHILD_PREFIX_LEN:]
else:
pagename = pagename[CHILD_PREFIX_LEN:]
return pagename
def RelPageName(context, pagename):
"""
Return the relative pagename for some context.
@param context: name of the page where "pagename" appears on
@param pagename: the absolute page name
@rtype: string
@return: the relative page name
"""
if context == '':
# special case, context is some "virtual root" page with name == ''
# every page is a subpage of this virtual root
return CHILD_PREFIX + pagename
elif pagename.startswith(context + CHILD_PREFIX):
# simple child
return pagename[len(context):]
else:
# some kind of sister/aunt
context_frags = context.split('/') # A, B, C, D, E
pagename_frags = pagename.split('/') # A, B, C, F
# first throw away common parents:
common = 0
for cf, pf in zip(context_frags, pagename_frags):
if cf == pf:
common += 1
else:
break
context_frags = context_frags[common:] # D, E
pagename_frags = pagename_frags[common:] # F
go_up = len(context_frags)
return PARENT_PREFIX * go_up + '/'.join(pagename_frags)
def pagelinkmarkup(pagename, text=None):
""" return markup that can be used as link to page <pagename> """
from MoinMoin.parser.text_moin_wiki import Parser
if re.match(Parser.word_rule + "$", pagename, re.U|re.X) and \
(text is None or text == pagename):
return pagename
else:
if text is None or text == pagename:
text = ''
else:
text = '|%s' % text
return u'[[%s%s]]' % (pagename, text)
#############################################################################
### mimetype support
#############################################################################
import mimetypes
MIMETYPES_MORE = {
# OpenOffice 2.x & other open document stuff
'.odt': 'application/vnd.oasis.opendocument.text',
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
'.odp': 'application/vnd.oasis.opendocument.presentation',
'.odg': 'application/vnd.oasis.opendocument.graphics',
'.odc': 'application/vnd.oasis.opendocument.chart',
'.odf': 'application/vnd.oasis.opendocument.formula',
'.odb': 'application/vnd.oasis.opendocument.database',
'.odi': 'application/vnd.oasis.opendocument.image',
'.odm': 'application/vnd.oasis.opendocument.text-master',
'.ott': 'application/vnd.oasis.opendocument.text-template',
'.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
'.otp': 'application/vnd.oasis.opendocument.presentation-template',
'.otg': 'application/vnd.oasis.opendocument.graphics-template',
# some systems (like Mac OS X) don't have some of these:
'.patch': 'text/x-diff',
'.diff': 'text/x-diff',
'.py': 'text/x-python',
'.cfg': 'text/plain',
'.conf': 'text/plain',
'.irc': 'text/plain',
'.md5': 'text/plain',
'.csv': 'text/csv',
'.flv': 'video/x-flv',
'.wmv': 'video/x-ms-wmv',
'.swf': 'application/x-shockwave-flash',
'.moin': 'text/moin-wiki',
'.creole': 'text/creole',
}
# add all mimetype patterns of pygments
import pygments.lexers
for name, short, patterns, mime in pygments.lexers.get_all_lexers():
for pattern in patterns:
if pattern.startswith('*.') and mime:
MIMETYPES_MORE[pattern[1:]] = mime[0]
[mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
MIMETYPES_sanitize_mapping = {
# this stuff is text, but got application/* for unknown reasons
('application', 'docbook+xml'): ('text', 'docbook'),
('application', 'x-latex'): ('text', 'latex'),
('application', 'x-tex'): ('text', 'tex'),
('application', 'javascript'): ('text', 'javascript'),
}
MIMETYPES_spoil_mapping = {} # inverse mapping of above
for _key, _value in MIMETYPES_sanitize_mapping.items():
MIMETYPES_spoil_mapping[_value] = _key
class MimeType(object):
""" represents a mimetype like text/plain """
def __init__(self, mimestr=None, filename=None):
self.major = self.minor = None # sanitized mime type and subtype
self.params = {} # parameters like "charset" or others
self.charset = None # this stays None until we know for sure!
self.raw_mimestr = mimestr
if mimestr:
self.parse_mimetype(mimestr)
elif filename:
self.parse_filename(filename)
def parse_filename(self, filename):
mtype, encoding = mimetypes.guess_type(filename)
if mtype is None:
mtype = 'application/octet-stream'
self.parse_mimetype(mtype)
def parse_mimetype(self, mimestr):
""" take a string like used in content-type and parse it into components,
alternatively it also can process some abbreviated string like "wiki"
"""
parameters = mimestr.split(";")
parameters = [p.strip() for p in parameters]
mimetype, parameters = parameters[0], parameters[1:]
mimetype = mimetype.split('/')
if len(mimetype) >= 2:
major, minor = mimetype[:2] # we just ignore more than 2 parts
else:
major, minor = self.parse_format(mimetype[0])
self.major = major.lower()
self.minor = minor.lower()
for param in parameters:
key, value = param.split('=')
if value[0] == '"' and value[-1] == '"': # remove quotes
value = value[1:-1]
self.params[key.lower()] = value
if 'charset' in self.params:
self.charset = self.params['charset'].lower()
self.sanitize()
def parse_format(self, format):
""" maps from what we currently use on-page in a #format xxx processing
instruction to a sanitized mimetype major, minor tuple.
can also be user later for easier entry by the user, so he can just
type "wiki" instead of "text/moin-wiki".
"""
format = format.lower()
if format in config.parser_text_mimetype:
mimetype = 'text', format
else:
mapping = {
'wiki': ('text', 'moin-wiki'),
'irc': ('text', 'irssi'),
}
try:
mimetype = mapping[format]
except KeyError:
mimetype = 'text', 'x-%s' % format
return mimetype
def sanitize(self):
""" convert to some representation that makes sense - this is not necessarily
conformant to /etc/mime.types or IANA listing, but if something is
readable text, we will return some text/* mimetype, not application/*,
because we need text/plain as fallback and not application/octet-stream.
"""
self.major, self.minor = MIMETYPES_sanitize_mapping.get((self.major, self.minor), (self.major, self.minor))
def spoil(self):
""" this returns something conformant to /etc/mime.type or IANA as a string,
kind of inverse operation of sanitize(), but doesn't change self
"""
major, minor = MIMETYPES_spoil_mapping.get((self.major, self.minor), (self.major, self.minor))
return self.content_type(major, minor)
def content_type(self, major=None, minor=None, charset=None, params=None):
""" return a string suitable for Content-Type header
"""
major = major or self.major
minor = minor or self.minor
params = params or self.params or {}
if major == 'text':
charset = charset or self.charset or params.get('charset', config.charset)
params['charset'] = charset
mimestr = "%s/%s" % (major, minor)
params = ['%s="%s"' % (key.lower(), value) for key, value in params.items()]
params.insert(0, mimestr)
return "; ".join(params)
def mime_type(self):
""" return a string major/minor only, no params """
return "%s/%s" % (self.major, self.minor)
def module_name(self):
""" convert this mimetype to a string useable as python module name,
we yield the exact module name first and then proceed to shorter
module names (useful for falling back to them, if the more special
module is not found) - e.g. first "text_python", next "text".
Finally, we yield "application_octet_stream" as the most general
mimetype we have.
Hint: the fallback handler module for text/* should be implemented
in module "text" (not "text_plain")
"""
mimetype = self.mime_type()
modname = mimetype.replace("/", "_").replace("-", "_").replace(".", "_")
fragments = modname.split('_')
for length in range(len(fragments), 1, -1):
yield "_".join(fragments[:length])
yield self.raw_mimestr
yield fragments[0]
yield "application_octet_stream"
#############################################################################
### Plugins
#############################################################################
class PluginError(Exception):
""" Base class for plugin errors """
class PluginMissingError(PluginError):
""" Raised when a plugin is not found """
class PluginAttributeError(PluginError):
""" Raised when plugin does not contain an attribtue """
def importPlugin(cfg, kind, name, function="execute"):
""" Import wiki or builtin plugin
Returns <function> attr from a plugin module <name>.
If <function> attr is missing, raise PluginAttributeError.
If <function> is None, return the whole module object.
If <name> plugin can not be imported, raise PluginMissingError.
kind may be one of 'action', 'formatter', 'macro', 'parser' or any other
directory that exist in MoinMoin or data/plugin.
Wiki plugins will always override builtin plugins. If you want
specific plugin, use either importWikiPlugin or importBuiltinPlugin
directly.
@param cfg: wiki config instance
@param kind: what kind of module we want to import
@param name: the name of the module
@param function: the function name
@rtype: any object
@return: "function" of module "name" of kind "kind", or None
"""
try:
return importWikiPlugin(cfg, kind, name, function)
except PluginMissingError:
return importBuiltinPlugin(kind, name, function)
def importWikiPlugin(cfg, kind, name, function="execute"):
""" Import plugin from the wiki data directory
See importPlugin docstring.
"""
plugins = wikiPlugins(kind, cfg)
modname = plugins.get(name, None)
if modname is None:
raise PluginMissingError()
moduleName = '%s.%s' % (modname, name)
return importNameFromPlugin(moduleName, function)
def importBuiltinPlugin(kind, name, function="execute"):
""" Import builtin plugin from MoinMoin package
See importPlugin docstring.
"""
if not name in builtinPlugins(kind):
raise PluginMissingError()
moduleName = 'MoinMoin.%s.%s' % (kind, name)
return importNameFromPlugin(moduleName, function)
def importNameFromPlugin(moduleName, name):
""" Return <name> attr from <moduleName> module,
raise PluginAttributeError if name does not exist.
If name is None, return the <moduleName> module object.
"""
if name is None:
fromlist = []
else:
fromlist = [name]
module = __import__(moduleName, globals(), {}, fromlist)
if fromlist:
# module has the obj for module <moduleName>
try:
return getattr(module, name)
except AttributeError:
raise PluginAttributeError
else:
# module now has the toplevel module of <moduleName> (see __import__ docs!)
components = moduleName.split('.')
for comp in components[1:]:
module = getattr(module, comp)
return module
def builtinPlugins(kind):
""" Gets a list of modules in MoinMoin.'kind'
@param kind: what kind of modules we look for
@rtype: list
@return: module names
"""
modulename = "MoinMoin." + kind
return pysupport.importName(modulename, "modules")
def wikiPlugins(kind, cfg):
"""
Gets a dict containing the names of all plugins of @kind
as the key and the containing module name as the value.
@param kind: what kind of modules we look for
@rtype: dict
@return: plugin name to containing module name mapping
"""
# short-cut if we've loaded the dict already
# (or already failed to load it)
cache = cfg._site_plugin_lists
if kind in cache:
result = cache[kind]
else:
result = {}
for modname in cfg._plugin_modules:
try:
module = pysupport.importName(modname, kind)
packagepath = os.path.dirname(module.__file__)
plugins = pysupport.getPluginModules(packagepath)
for p in plugins:
if not p in result:
result[p] = '%s.%s' % (modname, kind)
except AttributeError:
pass
cache[kind] = result
return result
def getPlugins(kind, cfg):
""" Gets a list of plugin names of kind
@param kind: what kind of modules we look for
@rtype: list
@return: module names
"""
# Copy names from builtin plugins - so we dont destroy the value
all_plugins = builtinPlugins(kind)[:]
# Add extension plugins without duplicates
for plugin in wikiPlugins(kind, cfg):
if plugin not in all_plugins:
all_plugins.append(plugin)
return all_plugins
def searchAndImportPlugin(cfg, type, name, what=None):
type2classname = {"parser": "Parser",
"formatter": "Formatter",
}
if what is None:
what = type2classname[type]
mt = MimeType(name)
plugin = None
for module_name in mt.module_name():
try:
plugin = importPlugin(cfg, type, module_name, what)
break
except PluginMissingError:
pass
else:
raise PluginMissingError("Plugin not found! (%r %r %r)" % (type, name, what))
return plugin
#############################################################################
### Parsers
#############################################################################
def getParserForExtension(cfg, extension):
"""
Returns the Parser class of the parser fit to handle a file
with the given extension. The extension should be in the same
format as os.path.splitext returns it (i.e. with the dot).
Returns None if no parser willing to handle is found.
The dict of extensions is cached in the config object.
@param cfg: the Config instance for the wiki in question
@param extension: the filename extension including the dot
@rtype: class, None
@returns: the parser class or None
"""
if not hasattr(cfg.cache, 'EXT_TO_PARSER'):
etp, etd = {}, None
parser_plugins = getPlugins('parser', cfg)
# force the 'highlight' parser to be the first entry in the list
# this makes it possible to overwrite some mapping entries later, so that
# moin will use some "better" parser for some filename extensions
parser_plugins.remove('highlight')
parser_plugins = ['highlight'] + parser_plugins
for pname in parser_plugins:
try:
Parser = importPlugin(cfg, 'parser', pname, 'Parser')
except PluginMissingError:
continue
if hasattr(Parser, 'extensions'):
exts = Parser.extensions
if isinstance(exts, list):
for ext in exts:
etp[ext] = Parser
elif str(exts) == '*':
etd = Parser
cfg.cache.EXT_TO_PARSER = etp
cfg.cache.EXT_TO_PARSER_DEFAULT = etd
return cfg.cache.EXT_TO_PARSER.get(extension, cfg.cache.EXT_TO_PARSER_DEFAULT)
#############################################################################
### Parameter parsing
#############################################################################
class BracketError(Exception):
pass
class BracketUnexpectedCloseError(BracketError):
def __init__(self, bracket):
self.bracket = bracket
BracketError.__init__(self, "Unexpected closing bracket %s" % bracket)
class BracketMissingCloseError(BracketError):
def __init__(self, bracket):
self.bracket = bracket
BracketError.__init__(self, "Missing closing bracket %s" % bracket)
class ParserPrefix:
"""
Trivial container-class holding a single character for
the possible prefixes for parse_quoted_separated_ext
and implementing rich equal comparison.
"""
def __init__(self, prefix):
self.prefix = prefix
def __eq__(self, other):
return isinstance(other, ParserPrefix) and other.prefix == self.prefix
def __repr__(self):
return '<ParserPrefix(%s)>' % self.prefix.encode('utf-8')
def parse_quoted_separated_ext(args, separator=None, name_value_separator=None,
brackets=None, seplimit=0, multikey=False,
prefixes=None, quotes='"'):
"""
Parses the given string according to the other parameters.
Items can be quoted with any character from the quotes parameter
and each quote can be escaped by doubling it, the separator and
name_value_separator can both be quoted, when name_value_separator
is set then the name can also be quoted.
Values that are not given are returned as None, while the
empty string as a value can be achieved by quoting it.
If a name or value does not start with a quote, then the quote
looses its special meaning for that name or value, unless it
starts with one of the given prefixes (the parameter is unicode
containing all allowed prefixes.) The prefixes will be returned
as ParserPrefix() instances in the first element of the tuple
for that particular argument.
If multiple separators follow each other, this is treated as
having None arguments inbetween, that is also true for when
space is used as separators (when separator is None), filter
them out afterwards.
The function can also do bracketing, i.e. parse expressions
that contain things like
"(a (a b))" to ['(', 'a', ['(', 'a', 'b']],
in this case, as in this example, the returned list will
contain sub-lists and the brackets parameter must be a list
of opening and closing brackets, e.g.
brackets = ['()', '<>']
Each sub-list's first item is the opening bracket used for
grouping.
Nesting will be observed between the different types of
brackets given. If bracketing doesn't match, a BracketError
instance is raised with a 'bracket' property indicating the
type of missing or unexpected bracket, the instance will be
either of the class BracketMissingCloseError or of the class
BracketUnexpectedCloseError.
If multikey is True (along with setting name_value_separator),
then the returned tuples for (key, value) pairs can also have
multiple keys, e.g.
"a=b=c" -> ('a', 'b', 'c')
@param args: arguments to parse
@param separator: the argument separator, defaults to None, meaning any
space separates arguments
@param name_value_separator: separator for name=value, default '=',
name=value keywords not parsed if evaluates to False
@param brackets: a list of two-character strings giving
opening and closing brackets
@param seplimit: limits the number of parsed arguments
@param multikey: multiple keys allowed for a single value
@rtype: list
@returns: list of unicode strings and tuples containing
unicode strings, or lists containing the same for
bracketing support
"""
idx = 0
assert name_value_separator is None or name_value_separator != separator
assert name_value_separator is None or len(name_value_separator) == 1
if not isinstance(args, unicode):
raise TypeError('args must be unicode')
max = len(args)
result = [] # result list
cur = [None] # current item
quoted = None # we're inside quotes, indicates quote character used
skipquote = 0 # next quote is a quoted quote
noquote = False # no quotes expected because word didn't start with one
seplimit_reached = False # number of separators exhausted
separator_count = 0 # number of separators encountered
SPACE = [' ', '\t', ]
nextitemsep = [separator] # used for skipping trailing space
SPACE = [' ', '\t', ]
if separator is None:
nextitemsep = SPACE[:]
separators = SPACE
else:
nextitemsep = [separator] # used for skipping trailing space
separators = [separator]
if name_value_separator:
nextitemsep.append(name_value_separator)
# bracketing support
opening = []
closing = []
bracketstack = []
matchingbracket = {}
if brackets:
for o, c in brackets:
assert not o in opening
opening.append(o)
assert not c in closing
closing.append(c)
matchingbracket[o] = c
def additem(result, cur, separator_count, nextitemsep):
if len(cur) == 1:
result.extend(cur)
elif cur:
result.append(tuple(cur))
cur = [None]
noquote = False
separator_count += 1
seplimit_reached = False
if seplimit and separator_count >= seplimit:
seplimit_reached = True
nextitemsep = [n for n in nextitemsep if n in separators]
return cur, noquote, separator_count, seplimit_reached, nextitemsep
while idx < max:
char = args[idx]
next = None
if idx + 1 < max:
next = args[idx+1]
if skipquote:
skipquote -= 1
if not separator is None and not quoted and char in SPACE:
spaces = ''
# accumulate all space
while char in SPACE and idx < max - 1:
spaces += char
idx += 1
char = args[idx]
# remove space if args end with it
if char in SPACE and idx == max - 1:
break
# remove space at end of argument
if char in nextitemsep:
continue
idx -= 1
if len(cur) and cur[-1]:
cur[-1] = cur[-1] + spaces
elif not quoted and char == name_value_separator:
if multikey or len(cur) == 1:
cur.append(None)
else:
if not multikey:
if cur[-1] is None:
cur[-1] = ''
cur[-1] += name_value_separator
else:
cur.append(None)
noquote = False
elif not quoted and not seplimit_reached and char in separators:
(cur, noquote, separator_count, seplimit_reached,
nextitemsep) = additem(result, cur, separator_count, nextitemsep)
elif not quoted and not noquote and char in quotes:
if len(cur) and cur[-1] is None:
del cur[-1]
cur.append(u'')
quoted = char
elif char == quoted and not skipquote:
if next == quoted:
skipquote = 2 # will be decremented right away
else:
quoted = None
elif not quoted and char in opening:
while len(cur) and cur[-1] is None:
del cur[-1]
(cur, noquote, separator_count, seplimit_reached,
nextitemsep) = additem(result, cur, separator_count, nextitemsep)
bracketstack.append((matchingbracket[char], result))
result = [char]
elif not quoted and char in closing:
while len(cur) and cur[-1] is None:
del cur[-1]
(cur, noquote, separator_count, seplimit_reached,
nextitemsep) = additem(result, cur, separator_count, nextitemsep)
cur = []
if not bracketstack:
raise BracketUnexpectedCloseError(char)
expected, oldresult = bracketstack[-1]
if not expected == char:
raise BracketUnexpectedCloseError(char)
del bracketstack[-1]
oldresult.append(result)
result = oldresult
elif not quoted and prefixes and char in prefixes and cur == [None]:
cur = [ParserPrefix(char)]
cur.append(None)
else:
if len(cur):
if cur[-1] is None:
cur[-1] = char
else:
cur[-1] += char
else:
cur.append(char)
noquote = True
idx += 1
if bracketstack:
raise BracketMissingCloseError(bracketstack[-1][0])
if quoted:
if len(cur):
if cur[-1] is None:
cur[-1] = quoted
else:
cur[-1] = quoted + cur[-1]
else:
cur.append(quoted)
additem(result, cur, separator_count, nextitemsep)
return result
def parse_quoted_separated(args, separator=',', name_value=True, seplimit=0):
result = []
positional = result
if name_value:
name_value_separator = '='
trailing = []
keywords = {}
else:
name_value_separator = None
l = parse_quoted_separated_ext(args, separator=separator,
name_value_separator=name_value_separator,
seplimit=seplimit)
for item in l:
if isinstance(item, tuple):
key, value = item
if key is None:
key = u''
keywords[key] = value
positional = trailing
else:
positional.append(item)
if name_value:
return result, keywords, trailing
return result
def get_bool(request, arg, name=None, default=None):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return a boolean from a unicode string.
Valid input is 'true'/'false', 'yes'/'no' and '1'/'0' or None for
the default value.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param default: default value if arg is None
@rtype: boolean or None
@returns: the boolean value of the string according to above rules
(or default value)
"""
_ = request.getText
assert default is None or isinstance(default, bool)
if arg is None:
return default
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
arg = arg.lower()
if arg in [u'0', u'false', u'no']:
return False
elif arg in [u'1', u'true', u'yes']:
return True
else:
if name:
raise ValueError(
_('Argument "%s" must be a boolean value, not "%s"') % (
name, arg))
else:
raise ValueError(
_('Argument must be a boolean value, not "%s"') % arg)
def get_int(request, arg, name=None, default=None):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return an integer from a unicode string
containing the decimal representation of a number.
None is a valid input and yields the default value.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param default: default value if arg is None
@rtype: int or None
@returns: the integer value of the string (or default value)
"""
_ = request.getText
assert default is None or isinstance(default, (int, long))
if arg is None:
return default
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
try:
return int(arg)
except ValueError:
if name:
raise ValueError(
_('Argument "%s" must be an integer value, not "%s"') % (
name, arg))
else:
raise ValueError(
_('Argument must be an integer value, not "%s"') % arg)
def get_float(request, arg, name=None, default=None):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return a float from a unicode string.
None is a valid input and yields the default value.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param default: default return value if arg is None
@rtype: float or None
@returns: the float value of the string (or default value)
"""
_ = request.getText
assert default is None or isinstance(default, (int, long, float))
if arg is None:
return default
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
try:
return float(arg)
except ValueError:
if name:
raise ValueError(
_('Argument "%s" must be a floating point value, not "%s"') % (
name, arg))
else:
raise ValueError(
_('Argument must be a floating point value, not "%s"') % arg)
def get_complex(request, arg, name=None, default=None):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return a complex from a unicode string.
None is a valid input and yields the default value.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param default: default return value if arg is None
@rtype: complex or None
@returns: the complex value of the string (or default value)
"""
_ = request.getText
assert default is None or isinstance(default, (int, long, float, complex))
if arg is None:
return default
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
try:
# allow writing 'i' instead of 'j'
arg = arg.replace('i', 'j').replace('I', 'j')
return complex(arg)
except ValueError:
if name:
raise ValueError(
_('Argument "%s" must be a complex value, not "%s"') % (
name, arg))
else:
raise ValueError(
_('Argument must be a complex value, not "%s"') % arg)
def get_unicode(request, arg, name=None, default=None):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return a unicode string from a unicode string.
None is a valid input and yields the default value.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param default: default return value if arg is None;
@rtype: unicode or None
@returns: the unicode string (or default value)
"""
assert default is None or isinstance(default, unicode)
if arg is None:
return default
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
return arg
def get_choice(request, arg, name=None, choices=[None], default_none=False):
"""
For use with values returned from parse_quoted_separated or given
as macro parameters, return a unicode string that must be in the
choices given. None is a valid input and yields first of the valid
choices.
@param request: A request instance
@param arg: The argument, may be None or a unicode string
@param name: Name of the argument, for error messages
@param choices: the possible choices
@param default_none: If False (default), get_choice returns first available
choice if arg is None. If True, get_choice returns
None if arg is None. This is useful if some arg value
is required (no default choice).
@rtype: unicode or None
@returns: the unicode string (or default value)
"""
assert isinstance(choices, (tuple, list))
if arg is None:
if default_none:
return None
else:
return choices[0]
elif not isinstance(arg, unicode):
raise TypeError('Argument must be None or unicode')
elif not arg in choices:
_ = request.getText
if name:
raise ValueError(
_('Argument "%s" must be one of "%s", not "%s"') % (
name, '", "'.join([repr(choice) for choice in choices]),
arg))
else:
raise ValueError(
_('Argument must be one of "%s", not "%s"') % (
'", "'.join([repr(choice) for choice in choices]), arg))
return arg
class IEFArgument:
"""
Base class for new argument parsers for
invoke_extension_function.
"""
def __init__(self):
pass
def parse_argument(self, s):
"""
Parse the argument given in s (a string) and return
the argument for the extension function.
"""
raise NotImplementedError
def get_default(self):
"""
Return the default for this argument.
"""
raise NotImplementedError
class UnitArgument(IEFArgument):
"""
Argument class for invoke_extension_function that forces
having any of the specified units given for a value.
Note that the default unit is "mm".
Use, for example, "UnitArgument('7mm', float, ['%', 'mm'])".
If the defaultunit parameter is given, any argument that
can be converted into the given argtype is assumed to have
the default unit. NOTE: This doesn't work with a choice
(tuple or list) argtype.
"""
def __init__(self, default, argtype, units=['mm'], defaultunit=None):
"""
Initialise a UnitArgument giving the default,
argument type and the permitted units.
"""
IEFArgument.__init__(self)
self._units = list(units)
self._units.sort(lambda x, y: len(y) - len(x))
self._type = argtype
self._defaultunit = defaultunit
assert defaultunit is None or defaultunit in units
if default is not None:
self._default = self.parse_argument(default)
else:
self._default = None
def parse_argument(self, s):
for unit in self._units:
if s.endswith(unit):
ret = (self._type(s[:len(s) - len(unit)]), unit)
return ret
if self._defaultunit is not None:
try:
return (self._type(s), self._defaultunit)
except ValueError:
pass
units = ', '.join(self._units)
## XXX: how can we translate this?
raise ValueError("Invalid unit in value %s (allowed units: %s)" % (s, units))
def get_default(self):
return self._default
class required_arg:
"""
Wrap a type in this class and give it as default argument
for a function passed to invoke_extension_function() in
order to get generic checking that the argument is given.
"""
def __init__(self, argtype):
"""
Initialise a required_arg
@param argtype: the type the argument should have
"""
if not (argtype in (bool, int, long, float, complex, unicode) or
isinstance(argtype, (IEFArgument, tuple, list))):
raise TypeError("argtype must be a valid type")
self.argtype = argtype
def invoke_extension_function(request, function, args, fixed_args=[]):
"""
Parses arguments for an extension call and calls the extension
function with the arguments.
If the macro function has a default value that is a bool,
int, long, float or unicode object, then the given value
is converted to the type of that default value before passing
it to the macro function. That way, macros need not call the
wikiutil.get_* functions for any arguments that have a default.
@param request: the request object
@param function: the function to invoke
@param args: unicode string with arguments (or evaluating to False)
@param fixed_args: fixed arguments to pass as the first arguments
@returns: the return value from the function called
"""
def _convert_arg(request, value, default, name=None):
"""
Using the get_* functions, convert argument to the type of the default
if that is any of bool, int, long, float or unicode; if the default
is the type itself then convert to that type (keeps None) or if the
default is a list require one of the list items.
In other cases return the value itself.
"""
# if extending this, extend required_arg as well!
if isinstance(default, bool):
return get_bool(request, value, name, default)
elif isinstance(default, (int, long)):
return get_int(request, value, name, default)
elif isinstance(default, float):
return get_float(request, value, name, default)
elif isinstance(default, complex):
return get_complex(request, value, name, default)
elif isinstance(default, unicode):
return get_unicode(request, value, name, default)
elif isinstance(default, (tuple, list)):
return get_choice(request, value, name, default)
elif default is bool:
return get_bool(request, value, name)
elif default is int or default is long:
return get_int(request, value, name)
elif default is float:
return get_float(request, value, name)
elif default is complex:
return get_complex(request, value, name)
elif isinstance(default, IEFArgument):
# defaults handled later
if value is None:
return None
return default.parse_argument(value)
elif isinstance(default, required_arg):
if isinstance(default.argtype, (tuple, list)):
# treat choice specially and return None if no choice
# is given in the value
return get_choice(request, value, name, list(default.argtype),
default_none=True)
else:
return _convert_arg(request, value, default.argtype, name)
return value
assert isinstance(fixed_args, (list, tuple))
_ = request.getText
kwargs = {}
kwargs_to_pass = {}
trailing_args = []
if args:
assert isinstance(args, unicode)
positional, keyword, trailing = parse_quoted_separated(args)
for kw in keyword:
try:
kwargs[str(kw)] = keyword[kw]
except UnicodeEncodeError:
kwargs_to_pass[kw] = keyword[kw]
trailing_args.extend(trailing)
else:
positional = []
if isfunction(function) or ismethod(function):
argnames, varargs, varkw, defaultlist = getargspec(function)
elif isclass(function):
(argnames, varargs,
varkw, defaultlist) = getargspec(function.__init__.im_func)
else:
raise TypeError('function must be a function, method or class')
# self is implicit!
if ismethod(function) or isclass(function):
argnames = argnames[1:]
fixed_argc = len(fixed_args)
argnames = argnames[fixed_argc:]
argc = len(argnames)
if not defaultlist:
defaultlist = []
# if the fixed parameters have defaults too...
if argc < len(defaultlist):
defaultlist = defaultlist[fixed_argc:]
defstart = argc - len(defaultlist)
defaults = {}
# reverse to be able to pop() things off
positional.reverse()
allow_kwargs = False
allow_trailing = False
# convert all arguments to keyword arguments,
# fill all arguments that weren't given with None
for idx in range(argc):
argname = argnames[idx]
if argname == '_kwargs':
allow_kwargs = True
continue
if argname == '_trailing_args':
allow_trailing = True
continue
if positional:
kwargs[argname] = positional.pop()
if not argname in kwargs:
kwargs[argname] = None
if idx >= defstart:
defaults[argname] = defaultlist[idx - defstart]
if positional:
if not allow_trailing:
raise ValueError(_('Too many arguments'))
trailing_args.extend(positional)
if trailing_args:
if not allow_trailing:
raise ValueError(_('Cannot have arguments without name following'
' named arguments'))
kwargs['_trailing_args'] = trailing_args
# type-convert all keyword arguments to the type
# that the default value indicates
for argname in kwargs.keys()[:]:
if argname in defaults:
# the value of 'argname' from kwargs will be put into the
# macro's 'argname' argument, so convert that giving the
# name to the converter so the user is told which argument
# went wrong (if it does)
kwargs[argname] = _convert_arg(request, kwargs[argname],
defaults[argname], argname)
if kwargs[argname] is None:
if isinstance(defaults[argname], required_arg):
raise ValueError(_('Argument "%s" is required') % argname)
if isinstance(defaults[argname], IEFArgument):
kwargs[argname] = defaults[argname].get_default()
if not argname in argnames:
# move argname into _kwargs parameter
kwargs_to_pass[argname] = kwargs[argname]
del kwargs[argname]
if kwargs_to_pass:
kwargs['_kwargs'] = kwargs_to_pass
if not allow_kwargs:
raise ValueError(_(u'No argument named "%s"') % (
kwargs_to_pass.keys()[0]))
return function(*fixed_args, **kwargs)
def parseAttributes(request, attrstring, endtoken=None, extension=None):
"""
Parse a list of attributes and return a dict plus a possible
error message.
If extension is passed, it has to be a callable that returns
a tuple (found_flag, msg). found_flag is whether it did find and process
something, msg is '' when all was OK or any other string to return an error
message.
@param request: the request object
@param attrstring: string containing the attributes to be parsed
@param endtoken: token terminating parsing
@param extension: extension function -
gets called with the current token, the parser and the dict
@rtype: dict, msg
@return: a dict plus a possible error message
"""
import shlex, StringIO
_ = request.getText
parser = shlex.shlex(StringIO.StringIO(attrstring))
parser.commenters = ''
msg = None
attrs = {}
while not msg:
try:
key = parser.get_token()
except ValueError, err:
msg = str(err)
break
if not key:
break
if endtoken and key == endtoken:
break
# call extension function with the current token, the parser, and the dict
if extension:
found_flag, msg = extension(key, parser, attrs)
#logging.debug("%r = extension(%r, parser, %r)" % (msg, key, attrs))
if found_flag:
continue
elif msg:
break
#else (we found nothing, but also didn't have an error msg) we just continue below:
try:
eq = parser.get_token()
except ValueError, err:
msg = str(err)
break
if eq != "=":
msg = _('Expected "=" to follow "%(token)s"') % {'token': key}
break
try:
val = parser.get_token()
except ValueError, err:
msg = str(err)
break
if not val:
msg = _('Expected a value for key "%(token)s"') % {'token': key}
break
key = escape(key) # make sure nobody cheats
# safely escape and quote value
if val[0] in ["'", '"']:
val = escape(val)
else:
val = '"%s"' % escape(val, 1)
attrs[key.lower()] = val
return attrs, msg or ''
class ParameterParser:
""" MoinMoin macro parameter parser
Parses a given parameter string, separates the individual parameters
and detects their type.
Possible parameter types are:
Name | short | example
----------------------------
Integer | i | -374
Float | f | 234.234 23.345E-23
String | s | 'Stri\'ng'
Boolean | b | 0 1 True false
Name | | case_sensitive | converted to string
So say you want to parse three things, name, age and if the
person is male or not:
The pattern will be: %(name)s%(age)i%(male)b
As a result, the returned dict will put the first value into
male, second into age etc. If some argument is missing, it will
get None as its value. This also means that all the identifiers
in the pattern will exist in the dict, they will just have the
value None if they were not specified by the caller.
So if we call it with the parameters as follows:
("John Smith", 18)
this will result in the following dict:
{"name": "John Smith", "age": 18, "male": None}
Another way of calling would be:
("John Smith", male=True)
this will result in the following dict:
{"name": "John Smith", "age": None, "male": True}
"""
def __init__(self, pattern):
# parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)"
int_re = r"(?P<int>-?\d+)"
bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))"
float_re = r"(?P<float>-?\d+\.\d+([eE][+-]?\d+)?)"
string_re = (r"(?P<string>('([^']|(\'))*?')|" +
r'("([^"]|(\"))*?"))')
name_re = name % "name"
name_param_re = name % "name_param"
param_re = r"\s*(\s*%s\s*=\s*)?(%s|%s|%s|%s|%s)\s*(,|$)" % (
name_re, float_re, int_re, bool_re, string_re, name_param_re)
self.param_re = re.compile(param_re, re.U)
self._parse_pattern(pattern)
def _parse_pattern(self, pattern):
param_re = r"(%(?P<name>\(.*?\))?(?P<type>[ibfs]{1,3}))|\|"
i = 0
# TODO: Optionals aren't checked.
self.optional = []
named = False
self.param_list = []
self.param_dict = {}
for match in re.finditer(param_re, pattern):
if match.group() == "|":
self.optional.append(i)
continue
self.param_list.append(match.group('type'))
if match.group('name'):
named = True
self.param_dict[match.group('name')[1:-1]] = i
elif named:
raise ValueError("Named parameter expected")
i += 1
def __str__(self):
return "%s, %s, optional:%s" % (self.param_list, self.param_dict,
self.optional)
def parse_parameters(self, params):
# Default list/dict entries to None
parameter_list = [None] * len(self.param_list)
parameter_dict = dict([(key, None) for key in self.param_dict])
check_list = [0] * len(self.param_list)
i = 0
start = 0
fixed_count = 0
named = False
while start < len(params):
match = re.match(self.param_re, params[start:])
if not match:
raise ValueError("malformed parameters")
start += match.end()
if match.group("int"):
pvalue = int(match.group("int"))
ptype = 'i'
elif match.group("bool"):
pvalue = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
ptype = 'b'
elif match.group("float"):
pvalue = float(match.group("float"))
ptype = 'f'
elif match.group("string"):
pvalue = match.group("string")[1:-1]
ptype = 's'
elif match.group("name_param"):
pvalue = match.group("name_param")
ptype = 'n'
else:
raise ValueError("Parameter parser code does not fit param_re regex")
name = match.group("name")
if name:
if name not in self.param_dict:
# TODO we should think on inheritance of parameters
raise ValueError("unknown parameter name '%s'" % name)
nr = self.param_dict[name]
if check_list[nr]:
raise ValueError("parameter '%s' specified twice" % name)
else:
check_list[nr] = 1
pvalue = self._check_type(pvalue, ptype, self.param_list[nr])
parameter_dict[name] = pvalue
parameter_list[nr] = pvalue
named = True
elif named:
raise ValueError("only named parameters allowed after first named parameter")
else:
nr = i
if nr not in self.param_dict.values():
fixed_count = nr + 1
parameter_list[nr] = self._check_type(pvalue, ptype, self.param_list[nr])
# Let's populate and map our dictionary to what's been found
for name in self.param_dict:
tmp = self.param_dict[name]
parameter_dict[name] = parameter_list[tmp]
i += 1
for i in range(fixed_count):
parameter_dict[i] = parameter_list[i]
return fixed_count, parameter_dict
def _check_type(self, pvalue, ptype, format):
if ptype == 'n' and 's' in format: # n as s
return pvalue
if ptype in format:
return pvalue # x -> x
if ptype == 'i':
if 'f' in format:
return float(pvalue) # i -> f
elif 'b' in format:
return pvalue != 0 # i -> b
elif ptype == 's':
if 'b' in format:
if pvalue.lower() == 'false':
return False # s-> b
elif pvalue.lower() == 'true':
return True # s-> b
else:
raise ValueError('%r does not match format %r' % (pvalue, format))
if 's' in format: # * -> s
return str(pvalue)
raise ValueError('%r does not match format %r' % (pvalue, format))
#############################################################################
### Misc
#############################################################################
def normalize_pagename(name, cfg):
""" Normalize page name
Prevent creating page names with invisible characters or funny
whitespace that might confuse the users or abuse the wiki, or
just does not make sense.
Restrict even more group pages, so they can be used inside acl lines.
@param name: page name, unicode
@rtype: unicode
@return: decoded and sanitized page name
"""
# Strip invalid characters
name = config.page_invalid_chars_regex.sub(u'', name)
# Split to pages and normalize each one
pages = name.split(u'/')
normalized = []
for page in pages:
# Ignore empty or whitespace only pages
if not page or page.isspace():
continue
# Cleanup group pages.
# Strip non alpha numeric characters, keep white space
if isGroupPage(page, cfg):
page = u''.join([c for c in page
if c.isalnum() or c.isspace()])
# Normalize white space. Each name can contain multiple
# words separated with only one space. Split handle all
# 30 unicode spaces (isspace() == True)
page = u' '.join(page.split())
normalized.append(page)
# Assemble components into full pagename
name = u'/'.join(normalized)
return name
def taintfilename(basename):
"""
Make a filename that is supposed to be a plain name secure, i.e.
remove any possible path components that compromise our system.
@param basename: (possibly unsafe) filename
@rtype: string
@return: (safer) filename
"""
for x in (os.pardir, ':', '/', '\\', '<', '>'):
basename = basename.replace(x, '_')
return basename
def drawing2fname(drawing):
config.drawing_extensions = ['.tdraw', '.adraw',
'.svg',
'.png', '.jpg', '.jpeg', '.gif',
]
fname, ext = os.path.splitext(drawing)
# note: do not just check for empty extension or stuff like drawing:foo.bar
# will fail, instead of being expanded to foo.bar.tdraw
if ext not in config.drawing_extensions:
# for backwards compatibility, twikidraw is the default:
drawing += '.tdraw'
return drawing
def mapURL(request, url):
"""
Map URLs according to 'cfg.url_mappings'.
@param url: a URL
@rtype: string
@return: mapped URL
"""
# check whether we have to map URLs
if request.cfg.url_mappings:
# check URL for the configured prefixes
for prefix in request.cfg.url_mappings:
if url.startswith(prefix):
# substitute prefix with replacement value
return request.cfg.url_mappings[prefix] + url[len(prefix):]
# return unchanged url
return url
def getUnicodeIndexGroup(name):
"""
Return a group letter for `name`, which must be a unicode string.
Currently supported: Hangul Syllables (U+AC00 - U+D7AF)
@param name: a string
@rtype: string
@return: group letter or None
"""
c = name[0]
if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables
return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588)
else:
return c.upper() # we put lower and upper case words into the same index group
def isStrictWikiname(name, word_re=re.compile(ur"^(?:[%(u)s][%(l)s]+){2,}$" % {'u': config.chars_upper, 'l': config.chars_lower})):
"""
Check whether this is NOT an extended name.
@param name: the wikiname in question
@rtype: bool
@return: true if name matches the word_re
"""
return word_re.match(name)
def is_URL(arg, schemas=config.url_schemas):
""" Return True if arg is a URL (with a schema given in the schemas list).
Note: there are not that many requirements for generic URLs, basically
the only mandatory requirement is the ':' between schema and rest.
Schema itself could be anything, also the rest (but we only support some
schemas, as given in config.url_schemas, so it is a bit less ambiguous).
"""
if ':' not in arg:
return False
for schema in schemas:
if arg.startswith(schema + ':'):
return True
return False
def isPicture(url):
"""
Is this a picture's url?
@param url: the url in question
@rtype: bool
@return: true if url points to a picture
"""
extpos = url.rfind(".") + 1
return extpos > 1 and url[extpos:].lower() in config.browser_supported_images
def link_tag(request, params, text=None, formatter=None, on=None, **kw):
""" Create a link.
TODO: cleanup css_class
@param request: the request object
@param params: parameter string appended to the URL after the scriptname/
@param text: text / inner part of the <a>...</a> link - does NOT get
escaped, so you can give HTML here and it will be used verbatim
@param formatter: the formatter object to use
@param on: opening/closing tag only
@keyword attrs: additional attrs (HTMLified string) (removed in 1.5.3)
@rtype: string
@return: formatted link tag
"""
if formatter is None:
formatter = request.html_formatter
if 'css_class' in kw:
css_class = kw['css_class']
del kw['css_class'] # one time is enough
else:
css_class = None
id = kw.get('id', None)
name = kw.get('name', None)
if text is None:
text = params # default
if formatter:
url = "%s/%s" % (request.script_root, params)
# formatter.url will escape the url part
if on is not None:
tag = formatter.url(on, url, css_class, **kw)
else:
tag = (formatter.url(1, url, css_class, **kw) +
formatter.rawHTML(text) +
formatter.url(0))
else: # this shouldn't be used any more:
if on is not None and not on:
tag = '</a>'
else:
attrs = ''
if css_class:
attrs += ' class="%s"' % css_class
if id:
attrs += ' id="%s"' % id
if name:
attrs += ' name="%s"' % name
tag = '<a%s href="%s/%s">' % (attrs, request.script_root, params)
if not on:
tag = "%s%s</a>" % (tag, text)
logging.warning("wikiutil.link_tag called without formatter and without request.html_formatter. tag=%r" % (tag, ))
return tag
def containsConflictMarker(text):
""" Returns true if there is a conflict marker in the text. """
return "/!\\ '''Edit conflict" in text
def pagediff(request, pagename1, rev1, pagename2, rev2, **kw):
"""
Calculate the "diff" between two page contents.
@param pagename1: name of first page
@param rev1: revision of first page
@param pagename2: name of second page
@param rev2: revision of second page
@keyword ignorews: if 1: ignore pure-whitespace changes.
@rtype: list
@return: lines of diff output
"""
from MoinMoin.Page import Page
from MoinMoin.util import diff_text
lines1 = Page(request, pagename1, rev=rev1).getlines()
lines2 = Page(request, pagename2, rev=rev2).getlines()
lines = diff_text.diff(lines1, lines2, **kw)
return lines
def anchor_name_from_text(text):
'''
Generate an anchor name from the given text.
This function generates valid HTML IDs matching: [A-Za-z][A-Za-z0-9:_.-]*
Note: this transformation has a special feature: when you feed it with a
valid ID/name, it will return it without modification (identity
transformation).
'''
quoted = urllib.quote_plus(text.encode('utf-7'), safe=':')
res = quoted.replace('%', '.').replace('+', '_')
if not res[:1].isalpha():
return 'A%s' % res
return res
def split_anchor(pagename):
"""
Split a pagename that (optionally) has an anchor into the real pagename
and the anchor part. If there is no anchor, it returns an empty string
for the anchor.
Note: if pagename contains a # (as part of the pagename, not as anchor),
you can use a trick to make it work nevertheless: just append a
# at the end:
"C##" returns ("C#", "")
"Problem #1#" returns ("Problem #1", "")
TODO: We shouldn't deal with composite pagename#anchor strings, but keep
it separate.
Current approach: [[pagename#anchor|label|attr=val,&qarg=qval]]
Future approach: [[pagename|label|attr=val,&qarg=qval,#anchor]]
The future approach will avoid problems when there is a # in the
pagename part (and no anchor). Also, we need to append #anchor
at the END of the generated URL (AFTER the query string).
"""
parts = rsplit(pagename, '#', 1)
if len(parts) == 2:
return parts
else:
return pagename, ""
########################################################################
### Tickets - usually used in forms to make sure that form submissions
### are in response to a form the same user got from moin for the same
### action and same page.
########################################################################
def createTicket(request, tm=None, action=None, pagename=None):
""" Create a ticket using a configured secret
@param tm: unix timestamp (optional, uses current time if not given)
@param action: action name (optional, uses current action if not given)
Note: if you create a ticket for a form that calls another
action than the current one, you MUST specify the
action you call when posting the form.
@param pagename: page name (optional, uses current page name if not given)
Note: if you create a ticket for a form that posts to another
page than the current one, you MUST specify the
page name you use when posting the form.
"""
from MoinMoin.support.python_compatibility import hmac_new
if tm is None:
# for age-check of ticket
tm = "%010x" % time.time()
# make the ticket very specific:
if pagename is None:
try:
pagename = request.page.page_name
except:
pagename = ''
if action is None:
action = request.action
if request.session:
# either a user is logged in or we have a anon session -
# if session times out, ticket will get invalid
sid = request.session.sid
else:
sid = ''
if request.user.valid:
uid = request.user.id
else:
uid = ''
hmac_data = []
for value in [tm, pagename, action, sid, uid, ]:
if isinstance(value, unicode):
value = value.encode('utf-8')
hmac_data.append(value)
hmac = hmac_new(request.cfg.secrets['wikiutil/tickets'],
''.join(hmac_data))
return "%s.%s" % (tm, hmac.hexdigest())
def checkTicket(request, ticket):
"""Check validity of a previously created ticket"""
try:
timestamp_str = ticket.split('.')[0]
timestamp = int(timestamp_str, 16)
except ValueError:
# invalid or empty ticket
logging.debug("checkTicket: invalid or empty ticket %r" % ticket)
return False
now = time.time()
if timestamp < now - 10 * 3600:
# we don't accept tickets older than 10h
logging.debug("checkTicket: too old ticket, timestamp %r" % timestamp)
return False
# Note: if the session timed out, that will also invalidate the ticket,
# if the ticket was created within a session.
ourticket = createTicket(request, timestamp_str)
logging.debug("checkTicket: returning %r, got %r, expected %r" % (ticket == ourticket, ticket, ourticket))
return ticket == ourticket
def renderText(request, Parser, text):
"""executes raw wiki markup with all page elements"""
import StringIO
out = StringIO.StringIO()
request.redirect(out)
wikiizer = Parser(text, request)
wikiizer.format(request.formatter, inhibit_p=True)
result = out.getvalue()
request.redirect()
del out
return result
def get_processing_instructions(body):
""" Extract the processing instructions / acl / etc. at the beginning of a page's body.
Hint: if you have a Page object p, you already have the result of this function in
p.meta and (even better) parsed/processed stuff in p.pi.
Returns a list of (pi, restofline) tuples and a string with the rest of the body.
"""
pi = []
while body.startswith('#'):
try:
line, body = body.split('\n', 1) # extract first line
except ValueError:
line = body
body = ''
# end parsing on empty (invalid) PI
if line == "#":
body = line + '\n' + body
break
if line[1] == '#':# two hash marks are a comment
comment = line[2:]
if not comment.startswith(' '):
# we don't require a blank after the ##, so we put one there
comment = ' ' + comment
line = '##%s' % comment
verb, args = (line[1:] + ' ').split(' ', 1) # split at the first blank
pi.append((verb.lower(), args.strip()))
return pi, body
class Version(tuple):
"""
Version objects store versions like 1.2.3-4.5alpha6 in a structured
way and support version comparisons and direct version component access.
1: major version (digits only)
2: minor version (digits only)
3: (maintenance) release version (digits only)
4.5alpha6: optional additional version specification (str)
You can create a Version instance either by giving the components, like:
Version(1,2,3,'4.5alpha6')
or by giving the composite version string, like:
Version(version="1.2.3-4.5alpha6").
Version subclasses tuple, so comparisons to tuples should work.
Also, we inherit all the comparison logic from tuple base class.
"""
VERSION_RE = re.compile(
r"""(?P<major>\d+)
\.
(?P<minor>\d+)
\.
(?P<release>\d+)
(-
(?P<additional>.+)
)?""",
re.VERBOSE)
@classmethod
def parse_version(cls, version):
match = cls.VERSION_RE.match(version)
if match is None:
raise ValueError("Unexpected version string format: %r" % version)
v = match.groupdict()
return int(v['major']), int(v['minor']), int(v['release']), str(v['additional'] or '')
def __new__(cls, major=0, minor=0, release=0, additional='', version=None):
if version:
major, minor, release, additional = cls.parse_version(version)
return tuple.__new__(cls, (major, minor, release, additional))
# properties for easy access of version components
major = property(lambda self: self[0])
minor = property(lambda self: self[1])
release = property(lambda self: self[2])
additional = property(lambda self: self[3])
def __str__(self):
version_str = "%d.%d.%d" % (self.major, self.minor, self.release)
if self.additional:
version_str += "-%s" % self.additional
return version_str
|