Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Changes In Branch ticket-enhancements Excluding Merge-Ins
This is equivalent to a diff from fe453a4893 to 7575b52e15
2012-11-27
| ||
16:26 | Enhancements to ticket processing. There are now two tables: TICKET and TICKETCHNG. There is one row in TICKETCHNG for each ticket artifact. Fields from ticket artifacts go into either or both of TICKET and TICKETCHNG, whichever contain matching column names. Default ticket edit and viewing scripts are updated to use TICKETCHNG. The TH1 scripti... check-in: 4f8c8975bc user: drh tags: trunk | |
15:32 | Fix some HTML markup irregularities. Improvements to the default ticket viewer. Closed-Leaf check-in: 7575b52e15 user: drh tags: ticket-enhancements | |
2012-11-26
| ||
21:30 | Fix a string-quoting error in the previous commit. check-in: 3c8195c876 user: drh tags: ticket-enhancements | |
2012-11-23
| ||
19:33 | some unnecessary spacing check-in: d13143eb3b user: jan.nijtmans tags: trunk | |
15:57 | All markup of the form ... with an options "links" or "links=BOOLEAN" attribute. Improved TH1 tracing and error reporting capabilities. Improved documentation on how reports work. check-in: 23c75abde4 user: drh tags: ticket-enhancements | |
11:29 | merge trunk "filename contains illegal characters" is now a warning check-in: d3bee356ba user: jan.nijtmans tags: ticket-d17d6e5b17 | |
10:35 | Disallow invalid unicode characters Closed-Leaf check-in: 9242c09ff9 user: jan.nijtmans tags: invalid-unicode | |
01:50 | When db_open_config() is called with the useAttach parameter set to non-zero, it may need to close and reopen the database using ATTACH if that was not done previously. check-in: fe453a4893 user: drh tags: trunk | |
2012-11-22
| ||
23:35 | Be consistent about display of check-in comments as either text/plain or text/x-fossil-wiki. When the user configures text/plain, use that format everywhere. check-in: 2c6fa9c3b0 user: drh tags: trunk | |
10:16 | Modify db_open_config() and associated routines to make their internal state more consistent and discoverable. Closed-Leaf check-in: 52a6868700 user: mistachkin tags: dbOpenConfig | |
Changes to src/attach.c.
528 528 output_text_with_line_numbers(z, zLn); 529 529 }else{ 530 530 @ <pre> 531 531 @ %h(z) 532 532 @ </pre> 533 533 } 534 534 }else if( strncmp(zMime, "image/", 6)==0 ){ 535 - @ <img src="%R/raw?name=%s(zSrc)&m=%s(zMime)"></img> 535 + @ <img src="%R/raw/%S(zSrc)?m=%s(zMime)"></img> 536 + style_submenu_element("Image", "Image", "%R/raw/%S?m=%s", zSrc, zMime); 536 537 }else{ 537 538 int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc); 538 539 @ <i>(file is %d(sz) bytes of binary data)</i> 539 540 } 540 541 @ </blockquote> 541 542 manifest_destroy(pAttach); 542 543 blob_reset(&attach);
Changes to src/cgi.c.
781 781 }else{ /* generic error message */ 782 782 json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 ); 783 783 } 784 784 fossil_exit( g.isHTTP ? 0 : 1); 785 785 } 786 786 #endif /* FOSSIL_ENABLE_JSON */ 787 787 788 +/* 789 +** Log HTTP traffic to a file. Begin the log on first use. Close the log 790 +** when the argument is NULL. 791 +*/ 792 +void cgi_trace(const char *z){ 793 + static FILE *pLog = 0; 794 + if( g.fHttpTrace==0 ) return; 795 + if( z==0 ){ 796 + if( pLog ) fclose(pLog); 797 + pLog = 0; 798 + return; 799 + } 800 + if( pLog==0 ){ 801 + char zFile[50]; 802 + unsigned r; 803 + sqlite3_randomness(sizeof(r), &r); 804 + sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%08x.txt", r); 805 + pLog = fossil_fopen(zFile, "wb"); 806 + if( pLog ){ 807 + fprintf(stderr, "# open log on %s\n", zFile); 808 + }else{ 809 + fprintf(stderr, "# failed to open %s\n", zFile); 810 + return; 811 + } 812 + } 813 + fputs(z, pLog); 814 +} 815 + 788 816 789 817 /* 790 818 ** Initialize the query parameter database. Information is pulled from 791 819 ** the QUERY_STRING environment variable (if it exists), from standard 792 820 ** input if there is POST data, and from HTTP_COOKIE. 793 821 */ 794 822 void cgi_init(void){ ................................................................................ 823 851 if( len>0 && zType ){ 824 852 blob_zero(&g.cgiIn); 825 853 if( fossil_strcmp(zType,"application/x-www-form-urlencoded")==0 826 854 || strncmp(zType,"multipart/form-data",19)==0 ){ 827 855 z = fossil_malloc( len+1 ); 828 856 len = fread(z, 1, len, g.httpIn); 829 857 z[len] = 0; 858 + cgi_trace(z); 830 859 if( zType[0]=='a' ){ 831 860 add_param_list(z, '&'); 832 861 }else{ 833 862 process_multipart_form_data(z, len); 834 863 } 835 864 }else if( fossil_strcmp(zType, "application/x-fossil")==0 ){ 836 865 blob_read_from_channel(&g.cgiIn, g.httpIn, len); ................................................................................ 1143 1172 struct sockaddr_in remoteName; 1144 1173 socklen_t size = sizeof(struct sockaddr_in); 1145 1174 char zLine[2000]; /* A single line of input. */ 1146 1175 g.fullHttpReply = 1; 1147 1176 if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ 1148 1177 malformed_request(); 1149 1178 } 1179 + cgi_trace(zLine); 1150 1180 zToken = extract_token(zLine, &z); 1151 1181 if( zToken==0 ){ 1152 1182 malformed_request(); 1153 1183 } 1154 1184 if( fossil_strcmp(zToken,"GET")!=0 && fossil_strcmp(zToken,"POST")!=0 1155 1185 && fossil_strcmp(zToken,"HEAD")!=0 ){ 1156 1186 malformed_request(); ................................................................................ 1179 1209 1180 1210 /* Get all the optional fields that follow the first line. 1181 1211 */ 1182 1212 while( fgets(zLine,sizeof(zLine),g.httpIn) ){ 1183 1213 char *zFieldName; 1184 1214 char *zVal; 1185 1215 1216 + cgi_trace(zLine); 1186 1217 zFieldName = extract_token(zLine,&zVal); 1187 1218 if( zFieldName==0 || *zFieldName==0 ) break; 1188 1219 while( fossil_isspace(*zVal) ){ zVal++; } 1189 1220 i = strlen(zVal); 1190 1221 while( i>0 && fossil_isspace(zVal[i-1]) ){ i--; } 1191 1222 zVal[i] = 0; 1192 1223 for(i=0; zFieldName[i]; i++){ ................................................................................ 1210 1241 }else if( fossil_strcmp(zFieldName,"referer:")==0 ){ 1211 1242 cgi_setenv("HTTP_REFERER", zVal); 1212 1243 #endif 1213 1244 }else if( fossil_strcmp(zFieldName,"user-agent:")==0 ){ 1214 1245 cgi_setenv("HTTP_USER_AGENT", zVal); 1215 1246 } 1216 1247 } 1217 - 1218 1248 cgi_init(); 1249 + cgi_trace(0); 1219 1250 } 1220 1251 1221 1252 #if INTERFACE 1222 1253 /* 1223 1254 ** Bitmap values for the flags parameter to cgi_http_server(). 1224 1255 */ 1225 1256 #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */
Changes to src/db.c.
1310 1310 " VALUES('server-code', lower(hex(randomblob(20))),now());" 1311 1311 "INSERT INTO config(name,value,mtime)" 1312 1312 " VALUES('project-code', lower(hex(randomblob(20))),now());" 1313 1313 ); 1314 1314 } 1315 1315 if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0); 1316 1316 if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0); 1317 + if( !db_is_global("timeline-plaintext") ){ 1318 + db_set_int("timeline-plaintext", 1, 0); 1319 + } 1317 1320 db_create_default_users(0, zDefaultUser); 1318 1321 user_select(); 1319 1322 1320 1323 if( zTemplate ){ 1321 1324 /* 1322 1325 ** Copy all settings from the supplied template repository. 1323 1326 */
Changes to src/info.c.
1109 1109 @ [annotate]</a> 1110 1110 } 1111 1111 cnt++; 1112 1112 if( pDownloadName && blob_size(pDownloadName)==0 ){ 1113 1113 blob_append(pDownloadName, zName, -1); 1114 1114 } 1115 1115 } 1116 - @ </ul></ul> 1116 + @ </ul> 1117 1117 free(prevName); 1118 1118 db_finalize(&q); 1119 1119 db_prepare(&q, 1120 1120 "SELECT substr(tagname, 6, 10000), datetime(event.mtime)," 1121 1121 " coalesce(event.euser, event.user)" 1122 1122 " FROM tagxref, tag, event" 1123 1123 " WHERE tagxref.rid=%d" ................................................................................ 1652 1652 output_text_with_line_numbers(z, zLn); 1653 1653 }else{ 1654 1654 @ <pre> 1655 1655 @ %h(z) 1656 1656 @ </pre> 1657 1657 } 1658 1658 }else if( strncmp(zMime, "image/", 6)==0 ){ 1659 - @ <img src="%s(g.zTop)/raw?name=%s(zUuid)&m=%s(zMime)"></img> 1659 + @ <img src="%R/raw/%S(zUuid)?m=%s(zMime)" /> 1660 + style_submenu_element("Image", "Image", 1661 + "%R/raw/%S?m=%s", zUuid, zMime); 1660 1662 }else{ 1661 1663 @ <i>(file is %d(blob_size(&content)) bytes of binary data)</i> 1662 1664 } 1663 1665 @ </blockquote> 1664 1666 } 1665 1667 style_footer(); 1666 1668 } ................................................................................ 1709 1711 } 1710 1712 } 1711 1713 style_header("Ticket Change Details"); 1712 1714 style_submenu_element("Raw", "Raw", "%R/artifact/%S", zUuid); 1713 1715 style_submenu_element("History", "History", "%R/tkthistory/%s", zTktName); 1714 1716 style_submenu_element("Page", "Page", "%R/tktview/%t", zTktName); 1715 1717 style_submenu_element("Timeline", "Timeline", "%R/tkttimeline/%t", zTktName); 1718 + if( P("plaintext") ){ 1719 + style_submenu_element("Formatted", "Formatted", "%R/info/%S", zUuid); 1720 + }else{ 1721 + style_submenu_element("Plaintext", "Plaintext", 1722 + "%R/info/%S?plaintext", zUuid); 1723 + } 1716 1724 1717 1725 @ <div class="section">Overview</div> 1718 1726 @ <p><table class="label-value"> 1719 1727 @ <tr><th>Artifact ID:</th> 1720 1728 @ <td>%z(href("%R/artifact/%s",zUuid))%s(zUuid)</a> 1721 1729 if( g.perm.Setup ){ 1722 1730 @ (%d(rid)) ................................................................................ 1745 1753 @ <input type="submit" value="Submit"> 1746 1754 @ </form> 1747 1755 @ </blockquote> 1748 1756 } 1749 1757 1750 1758 @ <div class="section">Changes</div> 1751 1759 @ <p> 1752 - ticket_output_change_artifact(pTktChng); 1760 + ticket_output_change_artifact(pTktChng, 0); 1753 1761 manifest_destroy(pTktChng); 1754 1762 style_footer(); 1755 1763 } 1756 1764 1757 1765 1758 1766 /* 1759 1767 ** WEBPAGE: info
Changes to src/main.c.
1630 1630 fossil_binary_mode(g.httpIn); 1631 1631 g.cgiOutput = 1; 1632 1632 blob_read_from_file(&config, zFile); 1633 1633 while( blob_line(&config, &line) ){ 1634 1634 if( !blob_token(&line, &key) ) continue; 1635 1635 if( blob_buffer(&key)[0]=='#' ) continue; 1636 1636 if( blob_eq(&key, "debug:") && blob_token(&line, &value) ){ 1637 - g.fDebug = fossil_fopen(blob_str(&value), "a"); 1637 + g.fDebug = fossil_fopen(blob_str(&value), "ab"); 1638 1638 blob_reset(&value); 1639 1639 continue; 1640 1640 } 1641 1641 if( blob_eq(&key, "HOME:") && blob_token(&line, &value) ){ 1642 1642 cgi_setenv("HOME", blob_str(&value)); 1643 1643 blob_reset(&value); 1644 1644 continue; ................................................................................ 1848 1848 /* 1849 1849 ** Note that the following command is used by ssh:// processing. 1850 1850 ** 1851 1851 ** COMMAND: test-http 1852 1852 ** Works like the http command but gives setup permission to all users. 1853 1853 */ 1854 1854 void cmd_test_http(void){ 1855 + g.thTrace = find_option("th-trace", 0, 0)!=0; 1856 + if( g.thTrace ){ 1857 + blob_zero(&g.thLog); 1858 + } 1855 1859 login_set_capabilities("sx", 0); 1856 1860 g.useLocalauth = 1; 1857 1861 cgi_set_parameter("REMOTE_ADDR", "127.0.0.1"); 1858 1862 g.httpIn = stdin; 1859 1863 g.httpOut = stdout; 1860 1864 find_server_repository(0); 1861 1865 g.cgiOutput = 1;
Changes to src/manifest.c.
1550 1550 if( !isNew ){ 1551 1551 for(i=0; i<pManifest->nField; i++){ 1552 1552 if( fossil_strcmp(pManifest->aField[i].zName, zStatusColumn)==0 ){ 1553 1553 zNewStatus = pManifest->aField[i].zValue; 1554 1554 } 1555 1555 } 1556 1556 if( zNewStatus ){ 1557 - blob_appendf(&comment, "%h ticket [%.10s]: <i>%s</i>", 1557 + blob_appendf(&comment, "%h ticket [%.10s]: <i>%h</i>", 1558 1558 zNewStatus, pManifest->zTicketUuid, zTitle 1559 1559 ); 1560 1560 if( pManifest->nField>1 ){ 1561 1561 blob_appendf(&comment, " plus %d other change%s", 1562 1562 pManifest->nField-1, pManifest->nField==2 ? "" : "s"); 1563 1563 } 1564 1564 blob_appendf(&brief, "%h ticket [%.10s].", 1565 1565 zNewStatus, pManifest->zTicketUuid); 1566 1566 }else{ 1567 1567 zNewStatus = db_text("unknown", 1568 1568 "SELECT %s FROM ticket WHERE tkt_uuid='%s'", 1569 1569 zStatusColumn, pManifest->zTicketUuid 1570 1570 ); 1571 - blob_appendf(&comment, "Ticket [%.10s] <i>%s</i> status still %h with " 1571 + blob_appendf(&comment, "Ticket [%.10s] <i>%h</i> status still %h with " 1572 1572 "%d other change%s", 1573 1573 pManifest->zTicketUuid, zTitle, zNewStatus, pManifest->nField, 1574 1574 pManifest->nField==1 ? "" : "s" 1575 1575 ); 1576 1576 free(zNewStatus); 1577 1577 blob_appendf(&brief, "Ticket [%.10s]: %d change%s", 1578 1578 pManifest->zTicketUuid, pManifest->nField, ................................................................................ 1866 1866 p->zAttachTarget, p->zAttachName 1867 1867 ); 1868 1868 if( strlen(p->zAttachTarget)!=UUID_SIZE 1869 1869 || !validate16(p->zAttachTarget, UUID_SIZE) 1870 1870 ){ 1871 1871 char *zComment; 1872 1872 if( p->zAttachSrc && p->zAttachSrc[0] ){ 1873 - zComment = mprintf("Add attachment \"%h\" to wiki page [%h]", 1874 - p->zAttachName, p->zAttachTarget); 1873 + zComment = mprintf( 1874 + "Add attachment [%R/artifact/%S|%h] to wiki page [%h]", 1875 + p->zAttachSrc, p->zAttachName, p->zAttachTarget); 1875 1876 }else{ 1876 1877 zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]", 1877 1878 p->zAttachName, p->zAttachTarget); 1878 1879 } 1879 1880 db_multi_exec( 1880 1881 "REPLACE INTO event(type,mtime,objid,user,comment)" 1881 1882 "VALUES('w',%.17g,%d,%Q,%Q)", 1882 1883 p->rDate, rid, p->zUser, zComment 1883 1884 ); 1884 1885 free(zComment); 1885 1886 }else{ 1886 1887 char *zComment; 1887 1888 if( p->zAttachSrc && p->zAttachSrc[0] ){ 1888 - zComment = mprintf("Add attachment \"%h\" to ticket [%.10s]", 1889 - p->zAttachName, p->zAttachTarget); 1889 + zComment = mprintf( 1890 + "Add attachment [%R/artifact/%S|%h] to ticket [%S]", 1891 + p->zAttachSrc, p->zAttachName, p->zAttachTarget); 1890 1892 }else{ 1891 1893 zComment = mprintf("Delete attachment \"%h\" from ticket [%.10s]", 1892 1894 p->zAttachName, p->zAttachTarget); 1893 1895 } 1894 1896 db_multi_exec( 1895 1897 "REPLACE INTO event(type,mtime,objid,user,comment)" 1896 1898 "VALUES('t',%.17g,%d,%Q,%Q)",
Changes to src/report.c.
486 486 @ 487 487 @ <li><p>If a column of the result set is named "#" then that column 488 488 @ is assumed to hold a ticket number. A hyperlink will be created from 489 489 @ that column to a detailed view of the ticket.</p></li> 490 490 @ 491 491 @ <li><p>If a column of the result set is named "bgcolor" then the content 492 492 @ of that column determines the background color of the row.</p></li> 493 + @ 494 + @ <li><p>The text of all columns prior to the first column whose name begins 495 + @ with underscore ("_") is shown character-for-character as it appears in 496 + @ the database. In other words, it is assumed to have a mimetype of 497 + @ text/plain. 493 498 @ 494 499 @ <li><p>The first column whose name begins with underscore ("_") and all 495 - @ subsequent columns are shown on their own rows in the table. This might 496 - @ be useful for displaying the description of tickets. 500 + @ subsequent columns are shown on their own rows in the table and with 501 + @ wiki formatting. In other words, such rows are shown with a mimetype 502 + @ of text/x-fossil-wiki. This is recommended for the "description" field 503 + @ of tickets. 497 504 @ </p></li> 498 505 @ 499 506 @ <li><p>The query can join other tables in the database besides TICKET. 500 507 @ </p></li> 501 508 @ </ul> 502 509 @ 503 510 @ <h3>Examples</h3> ................................................................................ 587 594 @ owner AS 'By', 588 595 @ subsystem AS 'Subsys', 589 596 @ sdate(changetime) AS 'Changed', 590 597 @ assignedto AS 'Assigned', 591 598 @ severity AS 'Svr', 592 599 @ priority AS 'Pri', 593 600 @ title AS 'Title', 594 - @ description AS '_Description', -- When the column name begins with '_' 595 - @ remarks AS '_Remarks' -- the data is shown on a separate row. 601 + @ description AS '_Description', -- When the column name begins with '_' 602 + @ remarks AS '_Remarks' -- content is rendered as wiki 596 603 @ FROM ticket 597 604 @ </pre></blockquote> 598 605 @ 599 606 @ <p>Or, to see part of the description on the same row, use the 600 607 @ <b>wiki()</b> function with some string manipulation. Using the 601 608 @ <b>tkt()</b> function on the ticket number will also generate a linked 602 609 @ field, but without the extra <i>edit</i> column: ................................................................................ 617 624 struct GenerateHTML { 618 625 int rn; /* Report number */ 619 626 int nCount; /* Row number */ 620 627 int nCol; /* Number of columns */ 621 628 int isMultirow; /* True if multiple table rows per query result row */ 622 629 int iNewRow; /* Index of first column that goes on separate row */ 623 630 int iBg; /* Index of column that defines background color */ 631 + int wikiFlags; /* Flags passed into wiki_convert() */ 632 + const char *zWikiStart; /* HTML before display of multi-line wiki */ 633 + const char *zWikiEnd; /* HTML after display of multi-line wiki */ 624 634 }; 625 635 626 636 /* 627 637 ** The callback function for db_query 628 638 */ 629 639 static int generate_html( 630 640 void *pUser, /* Pointer to output state */ ................................................................................ 661 671 if( g.perm.Write && azName[i][0]=='#' ){ 662 672 pState->nCol++; 663 673 } 664 674 if( !pState->isMultirow ){ 665 675 if( azName[i][0]=='_' ){ 666 676 pState->isMultirow = 1; 667 677 pState->iNewRow = i; 678 + pState->wikiFlags = WIKI_NOBADLINKS; 679 + pState->zWikiStart = ""; 680 + pState->zWikiEnd = ""; 681 + if( P("plaintext") ){ 682 + pState->wikiFlags |= WIKI_LINKSONLY; 683 + pState->zWikiStart = "<pre class='verbatim'>"; 684 + pState->zWikiEnd = "</pre>"; 685 + style_submenu_element("Formatted", "Formatted", 686 + "%R/rptview?rn=%d", pState->rn); 687 + }else{ 688 + style_submenu_element("Plaintext", "Plaintext", 689 + "%R/rptview?rn=%d&plaintext", pState->rn); 690 + } 668 691 }else{ 669 692 pState->nCol++; 670 693 } 671 694 } 672 695 } 673 696 674 697 /* The first time this routine is called, output a table header ................................................................................ 726 749 if( pState->iNewRow>=0 && i>=pState->iNewRow ){ 727 750 if( zTid && g.perm.Write ){ 728 751 @ <td valign="top">%z(href("%R/tktedit/%h",zTid))edit</a></td> 729 752 zTid = 0; 730 753 } 731 754 if( zData[0] ){ 732 755 Blob content; 733 - @ </tr><tr style="background-color:%h(zBg)"><td colspan=%d(pState->nCol)> 756 + @ </tr> 757 + @ <tr style="background-color:%h(zBg)"><td colspan=%d(pState->nCol)> 758 + @ %s(pState->zWikiStart) 734 759 blob_init(&content, zData, -1); 735 - wiki_convert(&content, 0, WIKI_NOBADLINKS); 760 + wiki_convert(&content, 0, pState->wikiFlags); 736 761 blob_reset(&content); 762 + @ %s(pState->zWikiEnd) 737 763 } 738 764 }else if( azName[i][0]=='#' ){ 739 765 zTid = zData; 740 766 @ <td valign="top">%z(href("%R/tktview?name=%h",zData))%h(zData)</a></td> 741 767 }else if( zData[0]==0 ){ 742 768 @ <td valign="top"> </td> 743 769 }else{
Changes to src/schema.c.
395 395 @ severity TEXT, 396 396 @ foundin TEXT, 397 397 @ private_contact TEXT, 398 398 @ resolution TEXT, 399 399 @ title TEXT, 400 400 @ comment TEXT 401 401 @ ); 402 +@ CREATE TABLE ticketchng( 403 +@ -- Do not change any column that begins with tkt_ 404 +@ tkt_id INTEGER REFERENCES ticket, 405 +@ tkt_mtime DATE, 406 +@ -- Add as many fields as required below this line 407 +@ login TEXT, 408 +@ username TEXT, 409 +@ mimetype TEXT, 410 +@ icomment TEXT 411 +@ ); 412 +@ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime); 402 413 ; 403 414 404 415 /* 405 416 ** Predefined tagid values 406 417 */ 407 418 #if INTERFACE 408 419 # define TAG_BGCOLOR 1 /* Set the background color for display */
Changes to src/th.c.
1146 1146 if( !pValue->zData ){ 1147 1147 Th_ErrorMessage(interp, "no such variable:", zVar, nVar); 1148 1148 return TH_ERROR; 1149 1149 } 1150 1150 1151 1151 return Th_SetResult(interp, pValue->zData, pValue->nData); 1152 1152 } 1153 + 1154 +/* 1155 +** Return true if variable (zVar, nVar) exists. 1156 +*/ 1157 +int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){ 1158 + return thFindValue(interp, zVar, nVar, 0, 0)!=0; 1159 +} 1153 1160 1154 1161 /* 1155 1162 ** String (zVar, nVar) must contain the name of a scalar variable or 1156 1163 ** array member. If the variable does not exist it is created. The 1157 1164 ** variable is set to the value supplied in string (zValue, nValue). 1158 1165 ** 1159 1166 ** If (zVar, nVar) refers to an existing array, TH_ERROR is returned
Changes to src/th.h.
47 47 */ 48 48 int Th_Expr(Th_Interp *interp, const char *, int); 49 49 50 50 /* 51 51 ** Access TH variables in the current stack frame. If the variable name 52 52 ** begins with "::", the lookup is in the top level (global) frame. 53 53 */ 54 +int Th_ExistsVar(Th_Interp *, const char *, int); 54 55 int Th_GetVar(Th_Interp *, const char *, int); 55 56 int Th_SetVar(Th_Interp *, const char *, int, const char *, int); 56 57 int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int); 57 58 int Th_UnsetVar(Th_Interp *, const char *, int); 58 59 59 60 typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *); 60 61
Changes to src/th_lang.c.
815 815 Th_SetResult(interp, zByte, nByte); 816 816 Th_Free(interp, zByte); 817 817 return TH_OK; 818 818 } 819 819 820 820 /* 821 821 ** TH Syntax: 822 +** 823 +** string trim STRING 824 +** string trimleft STRING 825 +** string trimright STRING 826 +*/ 827 +static int string_trim_command( 828 + Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl 829 +){ 830 + int n; 831 + const char *z; 832 + 833 + if( argc!=3 ){ 834 + return Th_WrongNumArgs(interp, "string trim string"); 835 + } 836 + z = argv[2]; 837 + n = argl[2]; 838 + if( argl[1]<5 || argv[1][4]=='l' ){ 839 + while( n && th_isspace(z[0]) ){ z++; n--; } 840 + } 841 + if( argl[1]<5 || argv[1][4]=='r' ){ 842 + while( n && th_isspace(z[n-1]) ){ n--; } 843 + } 844 + Th_SetResult(interp, z, n); 845 + return TH_OK; 846 +} 847 + 848 +/* 849 +** TH Syntax: 822 850 ** 823 851 ** info exists VAR 824 852 */ 825 853 static int info_exists_command( 826 854 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl 827 855 ){ 828 856 int rc; 829 857 830 858 if( argc!=3 ){ 831 859 return Th_WrongNumArgs(interp, "info exists var"); 832 860 } 833 - rc = Th_GetVar(interp, argv[2], argl[2]); 834 - Th_SetResultInt(interp, rc?0:1); 861 + rc = Th_ExistsVar(interp, argv[2], argl[2]); 862 + Th_SetResultInt(interp, rc); 835 863 return TH_OK; 836 864 } 837 865 838 866 /* 839 867 ** TH Syntax: 840 868 ** 841 869 ** unset VAR ................................................................................ 895 923 { "compare", string_compare_command }, 896 924 { "first", string_first_command }, 897 925 { "is", string_is_command }, 898 926 { "last", string_last_command }, 899 927 { "length", string_length_command }, 900 928 { "range", string_range_command }, 901 929 { "repeat", string_repeat_command }, 930 + { "trim", string_trim_command }, 931 + { "trimleft", string_trim_command }, 932 + { "trimright", string_trim_command }, 902 933 { 0, 0 } 903 934 }; 904 935 return Th_CallSubCommand(interp, ctx, argc, argv, argl, aSub); 905 936 } 906 937 907 938 /* 908 939 ** TH Syntax:
Changes to src/th_main.c.
16 16 ******************************************************************************* 17 17 ** 18 18 ** This file contains an interface between the TH scripting language 19 19 ** (an independent project) and fossil. 20 20 */ 21 21 #include "config.h" 22 22 #include "th_main.h" 23 +#include "sqlite3.h" 23 24 24 25 /* 25 26 ** Global variable counting the number of outstanding calls to malloc() 26 27 ** made by the th1 implementation. This is used to catch memory leaks 27 28 ** in the interpreter. Obviously, it also means th1 is not threadsafe. 28 29 */ 29 30 static int nOutstandingMalloc = 0; ................................................................................ 70 71 static int enableOutputCmd( 71 72 Th_Interp *interp, 72 73 void *p, 73 74 int argc, 74 75 const char **argv, 75 76 int *argl 76 77 ){ 77 - if( argc!=2 ){ 78 - return Th_WrongNumArgs(interp, "enable_output BOOLEAN"); 78 + int rc; 79 + if( argc<2 || argc>3 ){ 80 + return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN"); 79 81 } 80 - return Th_ToInt(interp, argv[1], argl[1], &enableOutput); 82 + rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput); 83 + if( g.thTrace ){ 84 + Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput); 85 + } 86 + return rc; 81 87 } 82 88 83 89 /* 84 90 ** Return a name for a TH1 return code. 85 91 */ 86 92 const char *Th_ReturnCodeName(int rc){ 87 93 static char zRc[32]; ................................................................................ 117 123 fflush(stdout); 118 124 } 119 125 if( encode ) free((char*)z); 120 126 } 121 127 } 122 128 123 129 static void sendError(const char *z, int n, int forceCgi){ 130 + int savedEnable = enableOutput; 131 + enableOutput = 1; 124 132 if( forceCgi || g.cgiOutput ){ 125 133 sendText("<hr><p class=\"thmainError\">", -1, 0); 126 134 } 127 135 sendText("ERROR: ", -1, 0); 128 136 sendText((char*)z, n, 1); 129 137 sendText(forceCgi || g.cgiOutput ? "</p>" : "\n", -1, 0); 138 + enableOutput = savedEnable; 130 139 } 131 140 132 141 /* 133 142 ** TH command: puts STRING 134 143 ** TH command: html STRING 135 144 ** 136 145 ** Output STRING escaped for HTML (html) or unchanged (puts). ................................................................................ 565 574 } 566 575 sqlite3_randomness(n, aRand); 567 576 encode16(aRand, zOut, n); 568 577 Th_SetResult(interp, (const char *)zOut, -1); 569 578 return TH_OK; 570 579 } 571 580 581 +/* 582 +** TH1 command: query SQL CODE 583 +** 584 +** Run the SQL query given by the SQL argument. For each row in the result 585 +** set, run CODE. 586 +** 587 +** In SQL, parameters such as $var are filled in using the value of variable 588 +** "var". Result values are stored in variables with the column name prior 589 +** to each invocation of CODE. 590 +*/ 591 +static int queryCmd( 592 + Th_Interp *interp, 593 + void *p, 594 + int argc, 595 + const char **argv, 596 + int *argl 597 +){ 598 + sqlite3_stmt *pStmt; 599 + int rc; 600 + const char *zSql; 601 + int nSql; 602 + const char *zTail; 603 + int n, i; 604 + int res = TH_OK; 605 + int nVar; 606 + 607 + if( argc!=3 ){ 608 + return Th_WrongNumArgs(interp, "query SQL CODE"); 609 + } 610 + if( g.db==0 ){ 611 + Th_ErrorMessage(interp, "database is not open", 0, 0); 612 + return TH_ERROR; 613 + } 614 + zSql = argv[1]; 615 + nSql = argl[1]; 616 + while( res==TH_OK && nSql>0 ){ 617 + rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail); 618 + if( rc!=0 ){ 619 + Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1); 620 + return TH_ERROR; 621 + } 622 + n = (int)(zTail - zSql); 623 + zSql += n; 624 + nSql -= n; 625 + if( pStmt==0 ) continue; 626 + nVar = sqlite3_bind_parameter_count(pStmt); 627 + for(i=1; i<=nVar; i++){ 628 + const char *zVar = sqlite3_bind_parameter_name(pStmt, i); 629 + int szVar = zVar ? th_strlen(zVar) : 0; 630 + if( szVar>1 && zVar[0]=='$' 631 + && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){ 632 + int nVal; 633 + const char *zVal = Th_GetResult(interp, &nVal); 634 + sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT); 635 + } 636 + } 637 + while( res==TH_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ 638 + int nCol = sqlite3_column_count(pStmt); 639 + for(i=0; i<nCol; i++){ 640 + const char *zCol = sqlite3_column_name(pStmt, i); 641 + int szCol = th_strlen(zCol); 642 + const char *zVal = (const char*)sqlite3_column_text(pStmt, i); 643 + int szVal = sqlite3_column_bytes(pStmt, i); 644 + Th_SetVar(interp, zCol, szCol, zVal, szVal); 645 + } 646 + res = Th_Eval(interp, 0, argv[2], argl[2]); 647 + if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK; 648 + } 649 + rc = sqlite3_finalize(pStmt); 650 + if( rc!=SQLITE_OK ){ 651 + Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1); 652 + return TH_ERROR; 653 + } 654 + } 655 + return res; 656 +} 572 657 573 658 /* 574 659 ** Make sure the interpreter has been initialized. Initialize it if 575 660 ** it has not been already. 576 661 ** 577 662 ** The interpreter is stored in the g.interp global variable. 578 663 */ ................................................................................ 591 676 {"enable_output", enableOutputCmd, 0}, 592 677 {"hascap", hascapCmd, 0}, 593 678 {"hasfeature", hasfeatureCmd, 0}, 594 679 {"html", putsCmd, (void*)&aFlags[0]}, 595 680 {"htmlize", htmlizeCmd, 0}, 596 681 {"linecount", linecntCmd, 0}, 597 682 {"puts", putsCmd, (void*)&aFlags[1]}, 683 + {"query", queryCmd, 0}, 598 684 {"randhex", randhexCmd, 0}, 599 685 {"repository", repositoryCmd, 0}, 600 686 {"stime", stimeCmd, 0}, 601 687 {"utime", utimeCmd, 0}, 602 688 {"wiki", wikiCmd, (void*)&aFlags[0]}, 603 689 {0, 0, 0} 604 690 }; ................................................................................ 799 885 i = 0; 800 886 zResult = (char*)Th_GetResult(g.interp, &n); 801 887 sendText((char*)zResult, n, encode); 802 888 }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ 803 889 sendText(z, i, 0); 804 890 z += i+5; 805 891 for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} 892 + if( g.thTrace ){ 893 + Th_Trace("eval {<pre>%#h</pre>}<br>", i, z); 894 + } 806 895 rc = Th_Eval(g.interp, 0, (const char*)z, i); 807 896 if( rc!=TH_OK ) break; 808 897 z += i; 809 898 if( z[0] ){ z += 6; } 810 899 i = 0; 811 900 }else{ 812 901 i++;
Changes to src/timeline.c.
342 342 } 343 343 }else if( zType[0]=='e' && tagid ){ 344 344 hyperlink_to_event_tagid(tagid<0?-tagid:tagid); 345 345 }else if( (tmFlags & TIMELINE_ARTID)!=0 ){ 346 346 hyperlink_to_uuid(zUuid); 347 347 } 348 348 db_column_blob(pQuery, commentColumn, &comment); 349 - if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){ 349 + if( zType[0]!='c' ){ 350 + /* Comments for anything other than a check-in are generated by 351 + ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */ 352 + wiki_convert(&comment, 0, WIKI_INLINE); 353 + }else if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){ 350 354 Blob truncated; 351 355 blob_zero(&truncated); 352 356 blob_append(&truncated, blob_buffer(&comment), mxWikiLen); 353 357 blob_append(&truncated, "...", 3); 354 358 @ %w(blob_str(&truncated)) 355 359 blob_reset(&truncated); 356 360 }else{
Changes to src/tkt.c.
24 24 25 25 /* 26 26 ** The list of database user-defined fields in the TICKET table. 27 27 ** The real table also contains some addition fields for internal 28 28 ** used. The internal-use fields begin with "tkt_". 29 29 */ 30 30 static int nField = 0; 31 -static char **azField = 0; /* Names of database fields */ 32 -static char **azValue = 0; /* Original values */ 33 -static char **azAppend = 0; /* Value to be appended */ 31 +static struct tktFieldInfo { 32 + char *zName; /* Name of the database field */ 33 + char *zValue; /* Value to store */ 34 + char *zAppend; /* Value to append */ 35 + unsigned mUsed; /* 01: TICKET 02: TICKETCHNG */ 36 +} *aField; 37 +#define USEDBY_TICKET 01 38 +#define USEDBY_TICKETCHNG 02 39 +static int haveTicket = 0; /* True if the TICKET table exists */ 40 +static int haveTicketChng = 0; /* True if the TICKETCHNG table exists */ 34 41 35 42 /* 36 -** Compare two entries in azField for sorting purposes 43 +** Compare two entries in aField[] for sorting purposes 37 44 */ 38 45 static int nameCmpr(const void *a, const void *b){ 39 - return fossil_strcmp(*(char**)a, *(char**)b); 46 + return fossil_strcmp(((const struct tktFieldInfo*)a)->zName, 47 + ((const struct tktFieldInfo*)b)->zName); 40 48 } 41 49 42 50 /* 43 -** Obtain a list of all fields of the TICKET table. Put them 44 -** in sorted order in azField[]. 51 +** Return the index into aField[] of the given field name. 52 +** Return -1 if zFieldName is not in aField[]. 53 +*/ 54 +static int fieldId(const char *zFieldName){ 55 + int i; 56 + for(i=0; i<nField; i++){ 57 + if( fossil_strcmp(aField[i].zName, zFieldName)==0 ) return i; 58 + } 59 + return -1; 60 +} 61 + 62 +/* 63 +** Obtain a list of all fields of the TICKET and TICKETCHNG tables. Put them 64 +** in sorted order in aField[]. 45 65 ** 46 -** Also allocate space for azValue[] and azAppend[] and initialize 47 -** all the values there to zero. 66 +** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and 67 +** TICKETCHANGE tables exist, respectively. 48 68 */ 49 69 static void getAllTicketFields(void){ 50 70 Stmt q; 51 71 int i; 52 - if( nField>0 ) return; 72 + static int once = 0; 73 + if( once ) return; 74 + once = 1; 53 75 db_prepare(&q, "PRAGMA table_info(ticket)"); 54 76 while( db_step(&q)==SQLITE_ROW ){ 55 - const char *zField = db_column_text(&q, 1); 56 - if( strncmp(zField,"tkt_",4)==0 ) continue; 77 + const char *zFieldName = db_column_text(&q, 1); 78 + haveTicket = 1; 79 + if( memcmp(zFieldName,"tkt_",4)==0 ) continue; 80 + if( nField%10==0 ){ 81 + aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) ); 82 + } 83 + aField[nField].zName = mprintf("%s", zFieldName); 84 + aField[nField].mUsed = USEDBY_TICKET; 85 + nField++; 86 + } 87 + db_finalize(&q); 88 + db_prepare(&q, "PRAGMA table_info(ticketchng)"); 89 + while( db_step(&q)==SQLITE_ROW ){ 90 + const char *zFieldName = db_column_text(&q, 1); 91 + haveTicketChng = 1; 92 + if( memcmp(zFieldName,"tkt_",4)==0 ) continue; 93 + if( (i = fieldId(zFieldName))>=0 ){ 94 + aField[i].mUsed |= USEDBY_TICKETCHNG; 95 + continue; 96 + } 57 97 if( nField%10==0 ){ 58 - azField = fossil_realloc(azField, sizeof(azField)*3*(nField+10) ); 98 + aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) ); 59 99 } 60 - azField[nField] = mprintf("%s", zField); 100 + aField[nField].zName = mprintf("%s", zFieldName); 101 + aField[nField].mUsed = USEDBY_TICKETCHNG; 61 102 nField++; 62 103 } 63 104 db_finalize(&q); 64 - qsort(azField, nField, sizeof(azField[0]), nameCmpr); 65 - azAppend = &azField[nField]; 66 - memset(azAppend, 0, sizeof(azAppend[0])*nField); 67 - azValue = &azAppend[nField]; 105 + qsort(aField, nField, sizeof(aField[0]), nameCmpr); 68 106 for(i=0; i<nField; i++){ 69 - azValue[i] = ""; 107 + aField[i].zValue = ""; 108 + aField[i].zAppend = 0; 70 109 } 71 110 } 72 111 73 -/* 74 -** Return the index into azField[] of the given field name. 75 -** Return -1 if zField is not in azField[]. 76 -*/ 77 -static int fieldId(const char *zField){ 78 - int i; 79 - for(i=0; i<nField; i++){ 80 - if( fossil_strcmp(azField[i], zField)==0 ) return i; 81 - } 82 - return -1; 83 -} 84 - 85 112 /* 86 113 ** Query the database for all TICKET fields for the specific 87 114 ** ticket whose name is given by the "name" CGI parameter. 88 115 ** Load the values for all fields into the interpreter. 89 116 ** 90 117 ** Only load those fields which do not already exist as 91 118 ** variables. ................................................................................ 112 139 const char *zName = db_column_name(&q, i); 113 140 char *zRevealed = 0; 114 141 if( zVal==0 ){ 115 142 zVal = ""; 116 143 }else if( strncmp(zName, "private_", 8)==0 ){ 117 144 zVal = zRevealed = db_reveal(zVal); 118 145 } 119 - for(j=0; j<nField; j++){ 120 - if( fossil_strcmp(azField[j],zName)==0 ){ 121 - azValue[j] = mprintf("%s", zVal); 122 - break; 123 - } 124 - } 125 - if( Th_Fetch(zName, &size)==0 ){ 146 + if( (j = fieldId(zName))>=0 ){ 147 + aField[j].zValue = mprintf("%s", zVal); 148 + }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){ 126 149 Th_Store(zName, zVal); 127 150 } 128 151 free(zRevealed); 129 152 } 130 - }else{ 131 - db_finalize(&q); 132 - db_prepare(&q, "PRAGMA table_info(ticket)"); 133 - if( Th_Fetch("tkt_uuid",&size)==0 ){ 134 - Th_Store("tkt_uuid",zName); 135 - } 136 - while( db_step(&q)==SQLITE_ROW ){ 137 - const char *zField = db_column_text(&q, 1); 138 - if( Th_Fetch(zField, &size)==0 ){ 139 - Th_Store(zField, ""); 140 - } 141 - } 142 - if( Th_Fetch("tkt_datetime",&size)==0 ){ 143 - Th_Store("tkt_datetime",""); 144 - } 145 153 } 146 154 db_finalize(&q); 155 + for(i=0; i<nField; i++){ 156 + if( Th_Fetch(aField[i].zName, &size)==0 ){ 157 + Th_Store(aField[i].zName, aField[i].zValue); 158 + } 159 + } 147 160 } 148 161 149 162 /* 150 163 ** Transfer all CGI parameters to variables in the interpreter. 151 164 */ 152 165 static void initializeVariablesFromCGI(void){ 153 166 int i; ................................................................................ 155 168 156 169 for(i=0; (z = cgi_parameter_name(i))!=0; i++){ 157 170 Th_Store(z, P(z)); 158 171 } 159 172 } 160 173 161 174 /* 162 -** Update an entry of the TICKET table according to the information 163 -** in the control file given in p. Attempt to create the appropriate 164 -** TICKET table entry if createFlag is true. If createFlag is false, 165 -** that means we already know the entry exists and so we can save the 166 -** work of trying to create it. 175 +** Update an entry of the TICKET and TICKETCHNG tables according to the 176 +** information in the ticket artifact given in p. Attempt to create 177 +** the appropriate TICKET table entry if tktid is zero. If tktid is nonzero 178 +** then it will be the ROWID of an existing TICKET entry. 167 179 ** 168 -** Return TRUE if a new TICKET entry was created and FALSE if an 169 -** existing entry was revised. 180 +** Parameter rid is the recordID for the ticket artifact in the BLOB table. 181 +** 182 +** Return the new rowid of the TICKET table entry. 170 183 */ 171 -int ticket_insert(const Manifest *p, int createFlag, int rid){ 172 - Blob sql; 184 +static int ticket_insert(const Manifest *p, int rid, int tktid){ 185 + Blob sql1, sql2, sql3; 173 186 Stmt q; 174 - int i; 175 - int rc = 0; 187 + int i, j; 176 188 177 - getAllTicketFields(); 178 - if( createFlag ){ 179 - db_multi_exec("INSERT OR IGNORE INTO ticket(tkt_uuid, tkt_mtime) " 189 + if( tktid==0 ){ 190 + db_multi_exec("INSERT INTO ticket(tkt_uuid, tkt_mtime) " 180 191 "VALUES(%Q, 0)", p->zTicketUuid); 181 - rc = db_changes(); 192 + tktid = db_last_insert_rowid(); 182 193 } 183 - blob_zero(&sql); 184 - blob_appendf(&sql, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime"); 194 + blob_zero(&sql1); 195 + blob_zero(&sql2); 196 + blob_zero(&sql3); 197 + blob_appendf(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime"); 185 198 for(i=0; i<p->nField; i++){ 186 199 const char *zName = p->aField[i].zName; 187 200 if( zName[0]=='+' ){ 188 201 zName++; 189 - if( fieldId(zName)<0 ) continue; 190 - blob_appendf(&sql,", %s=coalesce(%s,'') || %Q", 191 - zName, zName, p->aField[i].zValue); 202 + if( (j = fieldId(zName))<0 ) continue; 203 + if( aField[j].mUsed & USEDBY_TICKET ){ 204 + blob_appendf(&sql1,", %s=coalesce(%s,'') || %Q", 205 + zName, zName, p->aField[i].zValue); 206 + } 192 207 }else{ 193 - if( fieldId(zName)<0 ) continue; 194 - blob_appendf(&sql,", %s=%Q", zName, p->aField[i].zValue); 208 + if( (j = fieldId(zName))<0 ) continue; 209 + if( aField[j].mUsed & USEDBY_TICKET ){ 210 + blob_appendf(&sql1,", %s=%Q", zName, p->aField[i].zValue); 211 + } 212 + } 213 + if( aField[j].mUsed & USEDBY_TICKETCHNG ){ 214 + blob_appendf(&sql2, ",%s", zName); 215 + blob_appendf(&sql3, ",%Q", p->aField[i].zValue); 195 216 } 196 217 if( rid>0 ){ 197 218 wiki_extract_links(p->aField[i].zValue, rid, 1, p->rDate, i==0, 0); 198 219 } 199 220 } 200 - blob_appendf(&sql, " WHERE tkt_uuid='%s' AND tkt_mtime<:mtime", 201 - p->zTicketUuid); 202 - db_prepare(&q, "%s", blob_str(&sql)); 221 + blob_appendf(&sql1, " WHERE tkt_id=%d", tktid); 222 + db_prepare(&q, "%s", blob_str(&sql1)); 203 223 db_bind_double(&q, ":mtime", p->rDate); 204 224 db_step(&q); 205 225 db_finalize(&q); 206 - blob_reset(&sql); 207 - return rc; 226 + blob_reset(&sql1); 227 + if( blob_size(&sql2)>0 ){ 228 + db_prepare(&q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%s)" 229 + "VALUES(%d,:mtime%s)", 230 + blob_str(&sql2), tktid, blob_str(&sql3)); 231 + db_bind_double(&q, ":mtime", p->rDate); 232 + db_step(&q); 233 + db_finalize(&q); 234 + } 235 + blob_reset(&sql2); 236 + blob_reset(&sql3); 237 + return tktid; 208 238 } 209 239 210 240 /* 211 241 ** Rebuild an entire entry in the TICKET table 212 242 */ 213 243 void ticket_rebuild_entry(const char *zTktUuid){ 214 244 char *zTag = mprintf("tkt-%s", zTktUuid); 215 245 int tagid = tag_findid(zTag, 1); 216 246 Stmt q; 217 247 Manifest *pTicket; 248 + int tktid; 218 249 int createFlag = 1; 219 250 220 - fossil_free(zTag); 221 - db_multi_exec( 222 - "DELETE FROM ticket WHERE tkt_uuid=%Q", zTktUuid 223 - ); 251 + fossil_free(zTag); 252 + getAllTicketFields(); 253 + if( haveTicket==0 ) return; 254 + tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid); 255 + if( haveTicketChng ){ 256 + db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid); 257 + } 258 + db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid); 259 + tktid = 0; 224 260 db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid); 225 261 while( db_step(&q)==SQLITE_ROW ){ 226 262 int rid = db_column_int(&q, 0); 227 263 pTicket = manifest_get(rid, CFTYPE_TICKET); 228 264 if( pTicket ){ 229 - ticket_insert(pTicket, createFlag, rid); 265 + tktid = ticket_insert(pTicket, rid, tktid); 230 266 manifest_ticket_event(rid, pTicket, createFlag, tagid); 231 267 manifest_destroy(pTicket); 232 268 } 233 269 createFlag = 0; 234 270 } 235 271 db_finalize(&q); 236 272 } 237 273 238 274 /* 239 -** Create the subscript interpreter and load the "common" code. 275 +** Create the TH1 interpreter and load the "common" code. 240 276 */ 241 277 void ticket_init(void){ 242 278 const char *zConfig; 243 279 Th_FossilInit(0, 0); 244 280 zConfig = ticket_common_code(); 245 281 Th_Eval(g.interp, 0, zConfig, -1); 246 282 } 247 283 248 284 /* 249 -** Create the subscript interpreter and load the "change" code. 285 +** Create the TH1 interpreter and load the "change" code. 250 286 */ 251 287 int ticket_change(void){ 252 288 const char *zConfig; 253 289 Th_FossilInit(0, 0); 254 290 zConfig = ticket_change_code(); 255 291 return Th_Eval(g.interp, 0, zConfig, -1); 256 292 } 257 293 258 294 /* 259 -** Recreate the ticket table. 295 +** Recreate the TICKET and TICKETCHNG tables. 260 296 */ 261 297 void ticket_create_table(int separateConnection){ 262 298 const char *zSql; 263 299 264 - db_multi_exec("DROP TABLE IF EXISTS ticket;"); 300 + db_multi_exec( 301 + "DROP TABLE IF EXISTS ticket;" 302 + "DROP TABLE IF EXISTS ticketchng;" 303 + ); 265 304 zSql = ticket_table_schema(); 266 305 if( separateConnection ){ 306 + db_end_transaction(0); 267 307 db_init_database(g.zRepositoryName, zSql, 0); 268 308 }else{ 269 309 db_multi_exec("%s", zSql); 270 310 } 271 311 } 272 312 273 313 /* 274 -** Repopulate the ticket table 314 +** Repopulate the TICKET and TICKETCHNG tables from scratch using all 315 +** available ticket artifacts. 275 316 */ 276 317 void ticket_rebuild(void){ 277 318 Stmt q; 278 319 ticket_create_table(1); 279 320 db_begin_transaction(); 280 321 db_prepare(&q,"SELECT tagname FROM tag WHERE tagname GLOB 'tkt-*'"); 281 322 while( db_step(&q)==SQLITE_ROW ){ ................................................................................ 285 326 len = strlen(zName); 286 327 if( len<20 || !validate16(zName, len) ) continue; 287 328 ticket_rebuild_entry(zName); 288 329 } 289 330 db_finalize(&q); 290 331 db_end_transaction(0); 291 332 } 333 + 334 +/* 335 +** For trouble-shooting purposes, render a dump of the aField[] table to 336 +** the webpage currently under construction. 337 +*/ 338 +static void showAllFields(void){ 339 + int i; 340 + @ <font color="blue"> 341 + @ <p>Database fields:</p><ul> 342 + for(i=0; i<nField; i++){ 343 + @ <li>aField[%d(i)].zName = "%h(aField[i].zName)"; 344 + @ originally = "%h(aField[i].zValue)"; 345 + @ currently = "%h(PD(aField[i].zName,""))""; 346 + if( aField[i].zAppend ){ 347 + @ zAppend = "%h(aField[i].zAppend)"; 348 + } 349 + @ mUsed = %d(aField[i].mUsed); 350 + } 351 + @ </ul></font> 352 +} 292 353 293 354 /* 294 355 ** WEBPAGE: tktview 295 356 ** URL: tktview?name=UUID 296 357 ** 297 358 ** View a ticket. 298 359 */ ................................................................................ 320 381 "%s/tktnew", g.zTop); 321 382 } 322 383 if( g.perm.ApndTkt && g.perm.Attach ){ 323 384 style_submenu_element("Attach", "Add An Attachment", 324 385 "%s/attachadd?tkt=%T&from=%s/tktview/%t", 325 386 g.zTop, zUuid, g.zTop, zUuid); 326 387 } 388 + if( P("plaintext") ){ 389 + style_submenu_element("Formatted", "Formatted", "%R/tktview/%S", zUuid); 390 + }else{ 391 + style_submenu_element("Plaintext", "Plaintext", 392 + "%R/tktview/%S?plaintext", zUuid); 393 + } 327 394 style_header("View Ticket"); 328 395 if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1); 329 396 ticket_init(); 397 + initializeVariablesFromCGI(); 398 + getAllTicketFields(); 330 399 initializeVariablesFromDb(); 331 400 zScript = ticket_viewpage_code(); 401 + if( P("showfields")!=0 ) showAllFields(); 332 402 if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1); 333 403 Th_Render(zScript); 334 404 if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1); 335 405 336 406 zFullName = db_text(0, 337 407 "SELECT tkt_uuid FROM ticket" 338 408 " WHERE tkt_uuid GLOB '%q*'", zUuid); ................................................................................ 364 434 return Th_WrongNumArgs(interp, "append_field FIELD STRING"); 365 435 } 366 436 if( g.thTrace ){ 367 437 Th_Trace("append_field %#h {%#h}<br />\n", 368 438 argl[1], argv[1], argl[2], argv[2]); 369 439 } 370 440 for(idx=0; idx<nField; idx++){ 371 - if( strncmp(azField[idx], argv[1], argl[1])==0 372 - && azField[idx][argl[1]]==0 ){ 441 + if( memcmp(aField[idx].zName, argv[1], argl[1])==0 442 + && aField[idx].zName[argl[1]]==0 ){ 373 443 break; 374 444 } 375 445 } 376 446 if( idx>=nField ){ 377 447 Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]); 378 448 return TH_ERROR; 379 449 } 380 - azAppend[idx] = mprintf("%.*s", argl[2], argv[2]); 450 + aField[idx].zAppend = mprintf("%.*s", argl[2], argv[2]); 381 451 return TH_OK; 382 452 } 383 453 384 454 /* 385 455 ** Write a ticket into the repository. 386 456 */ 387 457 static void ticket_put( ................................................................................ 438 508 } 439 509 zUuid = (const char *)pUuid; 440 510 blob_zero(&tktchng); 441 511 zDate = date_in_standard_format("now"); 442 512 blob_appendf(&tktchng, "D %s\n", zDate); 443 513 free(zDate); 444 514 for(i=0; i<nField; i++){ 445 - if( azAppend[i] ){ 446 - blob_appendf(&tktchng, "J +%s %z\n", azField[i], 447 - fossilize(azAppend[i], -1)); 515 + if( aField[i].zAppend ){ 516 + blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName, 517 + fossilize(aField[i].zAppend, -1)); 448 518 ++nJ; 449 519 } 450 520 } 451 521 for(i=0; i<nField; i++){ 452 522 const char *zValue; 453 523 int nValue; 454 - if( azAppend[i] ) continue; 455 - zValue = Th_Fetch(azField[i], &nValue); 524 + if( aField[i].zAppend ) continue; 525 + zValue = Th_Fetch(aField[i].zName, &nValue); 456 526 if( zValue ){ 457 527 while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; } 458 - if( strncmp(zValue, azValue[i], nValue) || strlen(azValue[i])!=nValue ){ 459 - if( strncmp(azField[i], "private_", 8)==0 ){ 528 + if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0) 529 + || memcmp(zValue, aField[i].zValue, nValue)!=0 530 + || strlen(aField[i].zValue)!=nValue 531 + ){ 532 + if( memcmp(aField[i].zName, "private_", 8)==0 ){ 460 533 zValue = db_conceal(zValue, nValue); 461 - blob_appendf(&tktchng, "J %s %s\n", azField[i], zValue); 534 + blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue); 462 535 }else{ 463 - blob_appendf(&tktchng, "J %s %#F\n", azField[i], nValue, zValue); 536 + blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue); 464 537 } 465 538 nJ++; 466 539 } 467 540 } 468 541 } 469 542 if( *(char**)pUuid ){ 470 543 zUuid = db_text(0, ................................................................................ 521 594 if( !g.perm.NewTkt ){ login_needed(); return; } 522 595 if( P("cancel") ){ 523 596 cgi_redirect("home"); 524 597 } 525 598 style_header("New Ticket"); 526 599 if( g.thTrace ) Th_Trace("BEGIN_TKTNEW<br />\n", -1); 527 600 ticket_init(); 601 + initializeVariablesFromCGI(); 528 602 getAllTicketFields(); 529 603 initializeVariablesFromDb(); 530 - initializeVariablesFromCGI(); 604 + if( g.zPath[0]=='d' ) showAllFields(); 531 605 form_begin(0, "%R/%s", g.zPath); 532 606 login_insert_csrf_secret(); 533 607 if( P("date_override") && g.perm.Setup ){ 534 608 @ <input type="hidden" name="date_override" value="%h(P("date_override"))"> 535 609 } 536 - @ </p> 537 610 zScript = ticket_newpage_code(); 538 611 Th_Store("login", g.zLogin ? g.zLogin : "nobody"); 539 612 Th_Store("date", db_text(0, "SELECT datetime('now')")); 540 613 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, 541 614 (void*)&zNewUuid, 0); 542 615 if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br />\n", -1); 543 616 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){ ................................................................................ 594 667 return; 595 668 } 596 669 if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br />\n", -1); 597 670 ticket_init(); 598 671 getAllTicketFields(); 599 672 initializeVariablesFromCGI(); 600 673 initializeVariablesFromDb(); 674 + if( g.zPath[0]=='d' ) showAllFields(); 601 675 form_begin(0, "%R/%s", g.zPath); 602 676 @ <input type="hidden" name="name" value="%s(zName)" /> 603 677 login_insert_csrf_secret(); 604 - @ </p> 605 678 zScript = ticket_editpage_code(); 606 679 Th_Store("login", g.zLogin ? g.zLogin : "nobody"); 607 680 Th_Store("date", db_text(0, "SELECT datetime('now')")); 608 681 Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0); 609 682 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0); 610 683 if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br />\n", -1); 611 684 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){ ................................................................................ 633 706 rc = sqlite3_exec(db, zSchema, 0, 0, &zErr); 634 707 if( rc!=SQLITE_OK ){ 635 708 sqlite3_close(db); 636 709 return zErr; 637 710 } 638 711 rc = sqlite3_exec(db, "SELECT tkt_id, tkt_uuid, tkt_mtime FROM ticket", 639 712 0, 0, 0); 640 - sqlite3_close(db); 641 713 if( rc!=SQLITE_OK ){ 642 - zErr = mprintf("schema fails to define a valid ticket table " 643 - "containing all required fields"); 644 - return zErr; 714 + zErr = mprintf("schema fails to define valid a TICKET " 715 + "table containing all required fields"); 716 + }else{ 717 + rc = sqlite3_exec(db, "SELECT tkt_id, tkt_mtime FROM ticketchng", 0,0,0); 718 + if( rc!=SQLITE_OK ){ 719 + zErr = mprintf("schema fails to define valid a TICKETCHNG " 720 + "table containing all required fields"); 721 + } 645 722 } 723 + sqlite3_close(db); 646 724 } 647 - return 0; 725 + return zErr; 648 726 } 649 727 650 728 /* 651 729 ** WEBPAGE: tkttimeline 652 730 ** URL: /tkttimeline?name=TICKETUUID&y=TYPE 653 731 ** 654 732 ** Show the change history for a single ticket in timeline format. ................................................................................ 732 810 ** Show the complete change history for a single ticket 733 811 */ 734 812 void tkthistory_page(void){ 735 813 Stmt q; 736 814 char *zTitle; 737 815 const char *zUuid; 738 816 int tagid; 817 + int nChng = 0; 739 818 740 819 login_check_credentials(); 741 820 if( !g.perm.Hyperlink || !g.perm.RdTkt ){ login_needed(); return; } 742 821 zUuid = PD("name",""); 743 822 zTitle = mprintf("History Of Ticket %h", zUuid); 744 823 style_submenu_element("Status", "Status", 745 824 "%s/info/%s", g.zTop, zUuid); 746 825 style_submenu_element("Check-ins", "Check-ins", 747 826 "%s/tkttimeline?name=%s&y=ci", g.zTop, zUuid); 748 827 style_submenu_element("Timeline", "Timeline", 749 828 "%s/tkttimeline?name=%s", g.zTop, zUuid); 829 + if( P("plaintext")!=0 ){ 830 + style_submenu_element("Formatted", "Formatted", 831 + "%R/tkthistory/%S", zUuid); 832 + }else{ 833 + style_submenu_element("Plaintext", "Plaintext", 834 + "%R/tkthistory/%S?plaintext", zUuid); 835 + } 750 836 style_header(zTitle); 751 837 free(zTitle); 752 838 753 839 tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid); 754 840 if( tagid==0 ){ 755 841 @ No such ticket: %h(zUuid) 756 842 style_footer(); ................................................................................ 762 848 " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)" 763 849 " AND blob.rid=event.objid" 764 850 " UNION " 765 851 "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user" 766 852 " FROM attachment, blob" 767 853 " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)" 768 854 " AND blob.rid=attachid" 769 - " ORDER BY 1 DESC", 855 + " ORDER BY 1", 770 856 tagid, tagid 771 857 ); 772 858 while( db_step(&q)==SQLITE_ROW ){ 773 859 Manifest *pTicket; 774 860 char zShort[12]; 775 861 const char *zDate = db_column_text(&q, 0); 776 862 int rid = db_column_int(&q, 1); 777 863 const char *zChngUuid = db_column_text(&q, 2); 778 864 const char *zFile = db_column_text(&q, 4); 779 865 memcpy(zShort, zChngUuid, 10); 780 866 zShort[10] = 0; 867 + if( nChng==0 ){ 868 + @ <ol> 869 + } 870 + nChng++; 781 871 if( zFile!=0 ){ 782 872 const char *zSrc = db_column_text(&q, 3); 783 873 const char *zUser = db_column_text(&q, 5); 784 874 if( zSrc==0 || zSrc[0]==0 ){ 785 875 @ 786 - @ <p>Delete attachment "%h(zFile)" 876 + @ <li><p>Delete attachment "%h(zFile)" 787 877 }else{ 788 878 @ 789 - @ <p>Add attachment "%h(zFile)" 879 + @ <li><p>Add attachment 880 + @ "%z(href("%R/artifact/%S",zSrc))%h(zFile)</a>" 790 881 } 791 882 @ [%z(href("%R/artifact/%T",zChngUuid))%s(zShort)</a>] 792 883 @ (rid %d(rid)) by 793 884 hyperlink_to_user(zUser,zDate," on"); 794 885 hyperlink_to_date(zDate, ".</p>"); 795 886 }else{ 796 887 pTicket = manifest_get(rid, CFTYPE_TICKET); 797 888 if( pTicket ){ 798 889 @ 799 - @ <p>Ticket change 890 + @ <li><p>Ticket change 800 891 @ [%z(href("%R/artifact/%T",zChngUuid))%s(zShort)</a>] 801 892 @ (rid %d(rid)) by 802 893 hyperlink_to_user(pTicket->zUser,zDate," on"); 803 894 hyperlink_to_date(zDate, ":"); 804 895 @ </p> 805 - ticket_output_change_artifact(pTicket); 896 + ticket_output_change_artifact(pTicket, "a"); 806 897 } 807 898 manifest_destroy(pTicket); 808 899 } 809 900 } 810 901 db_finalize(&q); 902 + if( nChng ){ 903 + @ </ol> 904 + } 811 905 style_footer(); 812 906 } 813 907 814 908 /* 815 909 ** Return TRUE if the given BLOB contains a newline character. 816 910 */ 817 911 static int contains_newline(Blob *p){ ................................................................................ 823 917 return 0; 824 918 } 825 919 826 920 /* 827 921 ** The pTkt object is a ticket change artifact. Output a detailed 828 922 ** description of this object. 829 923 */ 830 -void ticket_output_change_artifact(Manifest *pTkt){ 924 +void ticket_output_change_artifact(Manifest *pTkt, const char *zListType){ 831 925 int i; 832 - @ <ol> 926 + int wikiFlags = WIKI_NOBADLINKS; 927 + const char *zBlock = "<blockquote>"; 928 + const char *zEnd = "</blockquote>"; 929 + if( P("plaintext")!=0 ){ 930 + wikiFlags |= WIKI_LINKSONLY; 931 + zBlock = "<blockquote><pre class='verbatim'>"; 932 + zEnd = "</pre></blockquote>"; 933 + } 934 + if( zListType==0 ) zListType = "1"; 935 + @ <ol type="%s(zListType)"> 833 936 for(i=0; i<pTkt->nField; i++){ 834 937 Blob val; 835 938 const char *z; 836 939 z = pTkt->aField[i].zName; 837 940 blob_set(&val, pTkt->aField[i].zValue); 838 941 if( z[0]=='+' ){ 839 - @ <li>Appended to %h(&z[1]):<blockquote> 840 - wiki_convert(&val, 0, WIKI_NOBADLINKS); 841 - @ </blockquote></li> 842 - }else if( blob_size(&val)<=50 && contains_newline(&val) ){ 843 - @ <li>Change %h(z) to:<blockquote> 844 - wiki_convert(&val, 0, WIKI_NOBADLINKS); 845 - @ </blockquote></li> 942 + @ <li>Appended to %h(&z[1]):%s(zBlock) 943 + wiki_convert(&val, 0, wikiFlags); 944 + @ %s(zEnd)</li> 945 + }else if( blob_size(&val)>50 || contains_newline(&val) ){ 946 + @ <li>Change %h(z) to:%s(zBlock) 947 + wiki_convert(&val, 0, wikiFlags); 948 + @ %s(zEnd)</li> 846 949 }else{ 847 950 @ <li>Change %h(z) to "%h(blob_str(&val))"</li> 848 951 } 849 952 blob_reset(&val); 850 953 } 851 954 @ </ol> 852 955 } ................................................................................ 964 1067 if( !strncmp(g.argv[3],"fields",n) ){ 965 1068 /* simply show all field names */ 966 1069 int i; 967 1070 968 1071 /* read all available ticket fields */ 969 1072 getAllTicketFields(); 970 1073 for(i=0; i<nField; i++){ 971 - printf("%s\n",azField[i]); 1074 + printf("%s\n",aField[i].zName); 972 1075 } 973 1076 }else if( !strncmp(g.argv[3],"reports",n) ){ 974 1077 rpt_list_reports(); 975 1078 }else{ 976 1079 fossil_fatal("unknown ticket list option '%s'!",g.argv[3]); 977 1080 } 978 1081 } ................................................................................ 1039 1142 if( eCmd==history ){ 1040 1143 Stmt q; 1041 1144 int tagid; 1042 1145 1043 1146 if ( i != g.argc ){ 1044 1147 fossil_fatal("no other parameters expected to %s!",g.argv[2]); 1045 1148 } 1046 - tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zTktUuid); 1149 + tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'", 1150 + zTktUuid); 1047 1151 if( tagid==0 ){ 1048 1152 fossil_fatal("no such ticket %h", zTktUuid); 1049 1153 } 1050 1154 db_prepare(&q, 1051 1155 "SELECT datetime(mtime,'localtime'), objid, uuid, NULL, NULL, NULL" 1052 1156 " FROM event, blob" 1053 1157 " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)" 1054 1158 " AND blob.rid=event.objid" 1055 1159 " UNION " 1056 - "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user" 1160 + "SELECT datetime(mtime,'localtime'), attachid, uuid, src, " 1161 + " filename, user" 1057 1162 " FROM attachment, blob" 1058 1163 " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)" 1059 1164 " AND blob.rid=attachid" 1060 1165 " ORDER BY 1 DESC", 1061 1166 tagid, tagid 1062 1167 ); 1063 1168 while( db_step(&q)==SQLITE_ROW ){ ................................................................................ 1069 1174 const char *zFile = db_column_text(&q, 4); 1070 1175 memcpy(zShort, zChngUuid, 10); 1071 1176 zShort[10] = 0; 1072 1177 if( zFile!=0 ){ 1073 1178 const char *zSrc = db_column_text(&q, 3); 1074 1179 const char *zUser = db_column_text(&q, 5); 1075 1180 if( zSrc==0 || zSrc[0]==0 ){ 1076 - fossil_print("Delete attachment %h\n", zFile); 1181 + fossil_print("Delete attachment %s\n", zFile); 1077 1182 }else{ 1078 - fossil_print("Add attachment %h\n", zFile); 1183 + fossil_print("Add attachment %s\n", zFile); 1079 1184 } 1080 - fossil_print(" by %h on %h\n", zUser, zDate); 1185 + fossil_print(" by %s on %s\n", zUser, zDate); 1081 1186 }else{ 1082 1187 pTicket = manifest_get(rid, CFTYPE_TICKET); 1083 1188 if( pTicket ){ 1084 1189 int i; 1085 1190 1086 - fossil_print("Ticket Change by %h on %h:\n", pTicket->zUser, zDate); 1191 + fossil_print("Ticket Change by %s on %s:\n", 1192 + pTicket->zUser, zDate); 1087 1193 for(i=0; i<pTicket->nField; i++){ 1088 1194 Blob val; 1089 1195 const char *z; 1090 1196 z = pTicket->aField[i].zName; 1091 1197 blob_set(&val, pTicket->aField[i].zValue); 1092 1198 if( z[0]=='+' ){ 1093 1199 fossil_print(" Append to "); ................................................................................ 1112 1218 return; 1113 1219 } 1114 1220 /* read all given ticket field/value pairs from command line */ 1115 1221 if( i==g.argc ){ 1116 1222 fossil_fatal("empty %s command aborted!",g.argv[2]); 1117 1223 } 1118 1224 getAllTicketFields(); 1119 - /* read commandline and assign fields in the azValue array */ 1225 + /* read commandline and assign fields in the aField[].zValue array */ 1120 1226 while( i<g.argc ){ 1121 1227 char *zFName; 1122 1228 char *zFValue; 1123 1229 int j; 1124 1230 int append = 0; 1125 1231 1126 1232 zFName = g.argv[i++]; ................................................................................ 1137 1243 zFName++; 1138 1244 } 1139 1245 j = fieldId(zFName); 1140 1246 if( j == -1 ){ 1141 1247 fossil_fatal("unknown field name '%s'!",zFName); 1142 1248 }else{ 1143 1249 if (append) { 1144 - azAppend[j] = zFValue; 1250 + aField[j].zAppend = zFValue; 1145 1251 } else { 1146 - azValue[j] = zFValue; 1252 + aField[j].zValue = zFValue; 1147 1253 } 1148 1254 } 1149 1255 } 1150 1256 1151 1257 /* now add the needed artifacts to the repository */ 1152 1258 blob_zero(&tktchng); 1153 1259 /* add the time to the ticket manifest */ 1154 1260 blob_appendf(&tktchng, "D %s\n", zDate); 1155 1261 /* append defined elements */ 1156 1262 for(i=0; i<nField; i++){ 1157 1263 char *zValue = 0; 1158 1264 char *zPfx; 1159 1265 1160 - if (azAppend[i] && azAppend[i][0] ){ 1266 + if (aField[i].zAppend && aField[i].zAppend[0] ){ 1161 1267 zPfx = " +"; 1162 - zValue = azAppend[i]; 1163 - } else if( azValue[i] && azValue[i][0] ){ 1268 + zValue = aField[i].zAppend; 1269 + } else if( aField[i].zValue && aField[i].zValue[0] ){ 1164 1270 zPfx = " "; 1165 - zValue = azValue[i]; 1271 + zValue = aField[i].zValue; 1166 1272 } else { 1167 1273 continue; 1168 1274 } 1169 - if( strncmp(azField[i], "private_", 8)==0 ){ 1275 + if( memcmp(aField[i].zName, "private_", 8)==0 ){ 1170 1276 zValue = db_conceal(zValue, strlen(zValue)); 1171 - blob_appendf(&tktchng, "J%s%s %s\n", zPfx, azField[i], zValue); 1277 + blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue); 1172 1278 }else{ 1173 1279 blob_appendf(&tktchng, "J%s%s %#F\n", zPfx, 1174 - azField[i], strlen(zValue), zValue); 1280 + aField[i].zName, strlen(zValue), zValue); 1175 1281 } 1176 1282 } 1177 1283 blob_appendf(&tktchng, "K %s\n", zTktUuid); 1178 1284 blob_appendf(&tktchng, "U %F\n", zUser); 1179 1285 md5sum_blob(&tktchng, &cksum); 1180 1286 blob_appendf(&tktchng, "Z %b\n", &cksum); 1181 1287 ticket_put(&tktchng, zTktUuid, 0); 1182 1288 printf("ticket %s succeeded for %s\n", 1183 1289 (eCmd==set?"set":"add"),zTktUuid); 1184 1290 } 1185 1291 } 1186 1292 }
Changes to src/tktsetup.c.
65 65 /* @-comment: ** */ 66 66 static const char zDefaultTicketTable[] = 67 67 @ CREATE TABLE ticket( 68 68 @ -- Do not change any column that begins with tkt_ 69 69 @ tkt_id INTEGER PRIMARY KEY, 70 70 @ tkt_uuid TEXT UNIQUE, 71 71 @ tkt_mtime DATE, 72 -@ -- Add as many field as required below this line 72 +@ -- Add as many fields as required below this line 73 73 @ type TEXT, 74 74 @ status TEXT, 75 75 @ subsystem TEXT, 76 76 @ priority TEXT, 77 77 @ severity TEXT, 78 78 @ foundin TEXT, 79 79 @ private_contact TEXT, 80 80 @ resolution TEXT, 81 81 @ title TEXT, 82 82 @ comment TEXT 83 83 @ ); 84 +@ CREATE TABLE ticketchng( 85 +@ -- Do not change any column that begins with tkt_ 86 +@ tkt_id INTEGER REFERENCES ticket, 87 +@ tkt_mtime DATE, 88 +@ -- Add as many fields as required below this line 89 +@ login TEXT, 90 +@ username TEXT, 91 +@ mimetype TEXT, 92 +@ icomment TEXT 93 +@ ); 94 +@ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime); 84 95 ; 85 96 86 97 /* 87 98 ** Return the ticket table definition 88 99 */ 89 100 const char *ticket_table_schema(void){ 90 101 return db_get("ticket-table", (char*)zDefaultTicketTable); ................................................................................ 118 129 z = db_get(zDbField, (char*)zDfltValue); 119 130 } 120 131 style_header("Edit %s", zTitle); 121 132 if( P("clear")!=0 ){ 122 133 login_verify_csrf_secret(); 123 134 db_unset(zDbField, 0); 124 135 if( xRebuild ) xRebuild(); 125 - z = zDfltValue; 136 + cgi_redirect("tktsetup"); 126 137 }else if( isSubmit ){ 127 138 char *zErr = 0; 128 139 login_verify_csrf_secret(); 129 140 if( xText && (zErr = xText(z))!=0 ){ 130 141 @ <p class="tktsetupError">ERROR: %h(zErr)</p> 131 142 }else{ 132 143 db_set(zDbField, z, 0); ................................................................................ 275 286 0, 276 287 30 277 288 ); 278 289 } 279 290 280 291 static const char zDefaultNew[] = 281 292 @ <th1> 293 +@ if {![info exists mutype]} {set mutype {[links only]}} 282 294 @ if {[info exists submit]} { 283 295 @ set status Open 296 +@ if {$mutype eq "HTML"} { 297 +@ set mimetype "text/html" 298 +@ } elseif {$mutype eq "Wiki"} { 299 +@ set mimetype "text/x-fossil-wiki" 300 +@ } elseif {$mutype eq {[links only]}} { 301 +@ set mimetype "text/x-fossil-plain" 302 +@ } else { 303 +@ set mimetype "text/plain" 304 +@ } 284 305 @ submit_ticket 285 306 @ } 286 307 @ </th1> 287 308 @ <h1 style="text-align: center;">Enter A New Ticket</h1> 288 309 @ <table cellpadding="5"> 289 310 @ <tr> 290 -@ <td colspan="2"> 311 +@ <td colspan="3"> 291 312 @ Enter a one-line summary of the ticket:<br /> 292 313 @ <input type="text" name="title" size="60" value="$<title>" /> 293 314 @ </td> 294 315 @ </tr> 295 316 @ 296 317 @ <tr> 297 -@ <td style="text-align: center;">Type: 298 -@ <th1>combobox type $type_choices 1</th1> 299 -@ </td> 300 -@ <td>What type of ticket is this?</td> 318 +@ <td align="right">Type:</td> 319 +@ <td align="left"><th1>combobox type $type_choices 1</th1></td> 320 +@ <td align="left">What type of ticket is this?</td> 301 321 @ </tr> 302 322 @ 303 323 @ <tr> 304 -@ <td style="text-align: center;">Version: 324 +@ <td align="right">Version:</td> 325 +@ <td align="left"> 305 326 @ <input type="text" name="foundin" size="20" value="$<foundin>" /> 306 327 @ </td> 307 -@ <td>In what version or build number do you observe the problem?</td> 328 +@ <td align="left">In what version or build number do you observe 329 +@ the problem?</td> 308 330 @ </tr> 309 331 @ 310 332 @ <tr> 311 -@ <td style="text-align: center;">Severity: 312 -@ <th1>combobox severity $severity_choices 1</th1> 313 -@ </td> 314 -@ <td>How debilitating is the problem? How badly does the problem 333 +@ <td align="right">Severity:</td> 334 +@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td> 335 +@ <td align="left">How debilitating is the problem? How badly does the problem 315 336 @ affect the operation of the product?</td> 316 337 @ </tr> 317 338 @ 318 339 @ <tr> 319 -@ <td style="text-align: center;">EMail: 320 -@ <input type="text" name="private_contact" value="$<private_contact>" size="30" /> 340 +@ <td align="right">EMail:</td> 341 +@ <td align="left"> 342 +@ <input type="text" name="private_contact" value="$<private_contact>" 343 +@ size="30" /> 321 344 @ </td> 322 -@ <td><span style="text-decoration: underline;">Not publicly visible</span>. 345 +@ <td align="left"><u>Not publicly visible</u> 323 346 @ Used by developers to contact you with questions.</td> 324 347 @ </tr> 325 348 @ 326 349 @ <tr> 327 -@ <td colspan="2"> 350 +@ <td colspan="3"> 328 351 @ Enter a detailed description of the problem. 329 352 @ For code defects, be sure to provide details on exactly how 330 353 @ the problem can be reproduced. Provide as much detail as 331 -@ possible. 354 +@ possible. Format: 355 +@ <th1>combobox mutype {Wiki HTML {Plain Text} {[links only]}} 1</th1> 332 356 @ <br /> 333 357 @ <th1>set nline [linecount $comment 50 10]</th1> 334 -@ <textarea name="comment" cols="80" rows="$nline" 335 -@ wrap="virtual" class="wikiedit">$<comment></textarea><br /> 336 -@ <input type="submit" name="preview" value="Preview" /></td> 358 +@ <textarea name="icomment" cols="80" rows="$nline" 359 +@ wrap="virtual" class="wikiedit">$<icomment></textarea><br /> 337 360 @ </tr> 338 -@ 361 +@ 339 362 @ <th1>enable_output [info exists preview]</th1> 340 -@ <tr><td colspan="2"> 363 +@ <tr><td colspan="3"> 341 364 @ Description Preview:<br /><hr /> 342 -@ <th1>wiki $comment</th1> 343 -@ <hr /> 344 -@ </td></tr> 365 +@ <th1> 366 +@ if {$mutype eq "Wiki"} { 367 +@ wiki $icomment 368 +@ } elseif {$mutype eq "Plain Text"} { 369 +@ set r [randhex] 370 +@ wiki "<verbatim-$r>[string trimright $icomment]\n</verbatim-$r>" 371 +@ } elseif {$mutype eq {[links only]}} { 372 +@ set r [randhex] 373 +@ wiki "<verbatim-$r links>[string trimright $icomment]\n</verbatim-$r>" 374 +@ } else { 375 +@ wiki "<nowiki>$icomment\n</nowiki>" 376 +@ } 377 +@ </th1> 378 +@ <hr /></td></tr> 379 +@ <th1>enable_output 1</th1> 380 +@ 381 +@ <tr> 382 +@ <td><td align="left"> 383 +@ <input type="submit" name="preview" value="Preview" /> 384 +@ </td> 385 +@ <td align="left">See how the description will appear after formatting.</td> 386 +@ </tr> 387 +@ 388 +@ <th1>enable_output [info exists preview]</th1> 389 +@ <tr> 390 +@ <td><td align="left"> 391 +@ <input type="submit" name="submit" value="Submit" /> 392 +@ </td> 393 +@ <td align="left">After filling in the information above, press this 394 +@ button to create the new ticket</td> 395 +@ </tr> 345 396 @ <th1>enable_output 1</th1> 346 397 @ 347 398 @ <tr> 348 -@ <td style="text-align: center;"> 349 -@ <input type="submit" name="submit" value="Submit" /> 350 -@ </td> 351 -@ <td>After filling in the information above, press this button to create 352 -@ the new ticket</td> 353 -@ </tr> 354 -@ <tr> 355 -@ <td style="text-align: center;"> 399 +@ <td><td align="left"> 356 400 @ <input type="submit" name="cancel" value="Cancel" /> 357 401 @ </td> 358 402 @ <td>Abandon and forget this ticket</td> 359 403 @ </tr> 360 404 @ </table> 361 405 ; 362 406 ................................................................................ 385 429 40 386 430 ); 387 431 } 388 432 389 433 static const char zDefaultView[] = 390 434 @ <table cellpadding="5"> 391 435 @ <tr><td class="tktDspLabel">Ticket UUID:</td> 392 -@ <td class="tktDspValue" colspan="3">$<tkt_uuid></td></tr> 436 +@ <th1> 437 +@ if {[hascap s]} { 438 +@ html "<td class='tktDspValue' colspan='3'>$tkt_uuid " 439 +@ html "($tkt_id)</td></tr>\n" 440 +@ } else { 441 +@ html "<td class='tktDspValue' colspan='3'>$tkt_uuid</td></tr>\n" 442 +@ } 443 +@ </th1> 393 444 @ <tr><td class="tktDspLabel">Title:</td> 394 445 @ <td class="tktDspValue" colspan="3"> 395 -@ <th1>wiki $title</th1> 446 +@ $<title> 396 447 @ </td></tr> 397 448 @ <tr><td class="tktDspLabel">Status:</td><td class="tktDspValue"> 398 449 @ $<status> 399 450 @ </td> 400 451 @ <td class="tktDspLabel">Type:</td><td class="tktDspValue"> 401 452 @ $<type> 402 453 @ </td></tr> ................................................................................ 421 472 @ </td> 422 473 @ <th1>enable_output 1</th1> 423 474 @ </tr> 424 475 @ <tr><td class="tktDspLabel">Version Found In:</td> 425 476 @ <td colspan="3" valign="top" class="tktDspValue"> 426 477 @ $<foundin> 427 478 @ </td></tr> 428 -@ <tr><td>Description & Comments:</td></tr> 429 -@ <tr><td colspan="4" class="tktDspValue"> 430 -@ <th1>wiki $comment</th1> 431 -@ </td></tr> 479 +@ 480 +@ <th1> 481 +@ if {[info exists comment] && [string length $comment]>10} { 482 +@ html { 483 +@ <tr><td class="tktDspLabel">Description:</td></tr> 484 +@ <tr><td colspan="5" class="tktDspValue"> 485 +@ } 486 +@ if {[info exists plaintext]} { 487 +@ set r [randhex] 488 +@ wiki "<verbatim-$r links>\n$comment\n</verbatim-$r>" 489 +@ } else { 490 +@ wiki $comment 491 +@ } 492 +@ } 493 +@ set seenRow 0 494 +@ set alwaysPlaintext [info exists plaintext] 495 +@ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin, 496 +@ mimetype as xmimetype, icomment AS xcomment, 497 +@ username AS xusername 498 +@ FROM ticketchng 499 +@ WHERE tkt_id=$tkt_id} { 500 +@ if {$seenRow} { 501 +@ html "<hr>\n" 502 +@ } else { 503 +@ html "<tr><td class='tktDspLabel'>User Comments:</td></tr>\n" 504 +@ html "<tr><td colspan='5' class='tktDspValue'>\n" 505 +@ set seenRow 1 506 +@ } 507 +@ html "[htmlize $xlogin]" 508 +@ if {$xlogin ne $xusername && [string length $xusername]>0} { 509 +@ html " (claiming to be [htmlize $xusername])" 510 +@ } 511 +@ html " added on $xdate:\n" 512 +@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { 513 +@ set r [randhex] 514 +@ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"} 515 +@ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" 516 +@ } elseif {$xmimetype eq "text/x-fossil-wiki"} { 517 +@ wiki "<p>\n[string trimright $xcomment]\n</p>\n" 518 +@ } elseif {$xmimetype eq "text/html"} { 519 +@ wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n" 520 +@ } else { 521 +@ set r [randhex] 522 +@ wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n" 523 +@ } 524 +@ } 525 +@ if {$seenRow} {html "</td></tr>\n"} 526 +@ </th1> 432 527 @ </table> 433 528 ; 434 529 435 530 436 531 /* 437 532 ** Return the code used to generate the view ticket page 438 533 */ ................................................................................ 456 551 0, 457 552 40 458 553 ); 459 554 } 460 555 461 556 static const char zDefaultEdit[] = 462 557 @ <th1> 558 +@ if {![info exists mutype]} {set mutype {[links only]}} 559 +@ if {![info exists icomment]} {set icomment {}} 463 560 @ if {![info exists username]} {set username $login} 464 561 @ if {[info exists submit]} { 465 -@ if {[info exists cmappnd]} { 466 -@ if {[string length $cmappnd]>0} { 467 -@ set ctxt "\n\n<hr /><i>[htmlize $login]" 468 -@ if {$username ne $login} { 469 -@ set ctxt "$ctxt claiming to be [htmlize $username]" 470 -@ } 471 -@ set ctxt "$ctxt added on [date] UTC:</i><br />\n$cmappnd" 472 -@ append_field comment $ctxt 473 -@ } 562 +@ if {$mutype eq "Wiki"} { 563 +@ set mimetype text/x-fossil-wiki 564 +@ } elseif {$mutype eq "HTML"} { 565 +@ set mimetype text/html 566 +@ } elseif {$mutype eq {[links only]}} { 567 +@ set mimetype text/x-fossil-plain 568 +@ } else { 569 +@ set mimetype text/plain 474 570 @ } 475 571 @ submit_ticket 476 572 @ } 477 573 @ </th1> 478 574 @ <table cellpadding="5"> 479 575 @ <tr><td class="tktDspLabel">Title:</td><td> 480 576 @ <input type="text" name="title" value="$<title>" size="60" /> 481 577 @ </td></tr> 578 +@ 482 579 @ <tr><td class="tktDspLabel">Status:</td><td> 483 580 @ <th1>combobox status $status_choices 1</th1> 484 581 @ </td></tr> 582 +@ 485 583 @ <tr><td class="tktDspLabel">Type:</td><td> 486 584 @ <th1>combobox type $type_choices 1</th1> 487 585 @ </td></tr> 586 +@ 488 587 @ <tr><td class="tktDspLabel">Severity:</td><td> 489 588 @ <th1>combobox severity $severity_choices 1</th1> 490 589 @ </td></tr> 590 +@ 491 591 @ <tr><td class="tktDspLabel">Priority:</td><td> 492 592 @ <th1>combobox priority $priority_choices 1</th1> 493 593 @ </td></tr> 594 +@ 494 595 @ <tr><td class="tktDspLabel">Resolution:</td><td> 495 596 @ <th1>combobox resolution $resolution_choices 1</th1> 496 597 @ </td></tr> 598 +@ 497 599 @ <tr><td class="tktDspLabel">Subsystem:</td><td> 498 600 @ <th1>combobox subsystem $subsystem_choices 1</th1> 499 601 @ </td></tr> 602 +@ 500 603 @ <th1>enable_output [hascap e]</th1> 501 604 @ <tr><td class="tktDspLabel">Contact:</td><td> 502 605 @ <input type="text" name="private_contact" size="40" 503 606 @ value="$<private_contact>" /> 504 607 @ </td></tr> 505 608 @ <th1>enable_output 1</th1> 609 +@ 506 610 @ <tr><td class="tktDspLabel">Version Found In:</td><td> 507 611 @ <input type="text" name="foundin" size="50" value="$<foundin>" /> 508 612 @ </td></tr> 613 +@ 509 614 @ <tr><td colspan="2"> 510 -@ <th1> 511 -@ if {![info exists eall]} {set eall 0} 512 -@ if {[info exists aonlybtn]} {set eall 0} 513 -@ if {[info exists eallbtn]} {set eall 1} 514 -@ if {![hascap w]} {set eall 0} 515 -@ if {![info exists cmappnd]} {set cmappnd {}} 516 -@ set nline [linecount $comment 15 10] 517 -@ enable_output $eall 518 -@ </th1> 519 -@ Description And Comments:<br /> 520 -@ <textarea name="comment" cols="80" rows="$nline" 521 -@ wrap="virtual" class="wikiedit">$<comment></textarea><br /> 522 -@ <input type="hidden" name="eall" value="1" /> 523 -@ <input type="submit" name="aonlybtn" value="Append Remark" /> 524 -@ <input type="submit" name="preview1btn" value="Preview" /> 525 -@ <th1>enable_output [expr {!$eall}]</th1> 526 -@ Append Remark from 615 +@ Append Remark with format 616 +@ <th1>combobox mutype {Wiki HTML {Plain Text} {[links only]}} 1</th1> 617 +@ from 527 618 @ <input type="text" name="username" value="$<username>" size="30" />:<br /> 528 -@ <textarea name="cmappnd" cols="80" rows="15" 529 -@ wrap="virtual" class="wikiedit">$<cmappnd></textarea><br /> 530 -@ <th1>enable_output [expr {[hascap w] && !$eall}]</th1> 531 -@ <input type="submit" name="eallbtn" value="Edit All" /> 532 -@ <th1>enable_output [expr {!$eall}]</th1> 533 -@ <input type="submit" name="preview2btn" value="Preview" /> 534 -@ <th1>enable_output 1</th1> 619 +@ <textarea name="icomment" cols="80" rows="15" 620 +@ wrap="virtual" class="wikiedit">$<icomment></textarea> 535 621 @ </td></tr> 536 -@ 537 -@ <th1>enable_output [info exists preview1btn]</th1> 622 +@ 623 +@ <th1>enable_output [info exists preview]</th1> 538 624 @ <tr><td colspan="2"> 539 -@ Description Preview:<br /><hr /> 540 -@ <th1>wiki $comment</th1> 541 -@ <hr /> 542 -@ </td></tr> 543 -@ <th1>enable_output [info exists preview2btn]</th1> 544 -@ <tr><td colspan="2"> 545 -@ Description Preview:<br /><hr /> 546 -@ <th1>wiki $cmappnd</th1> 547 -@ <hr /> 625 +@ Description Preview:<br><hr> 626 +@ <th1> 627 +@ if {$mutype eq "Wiki"} { 628 +@ wiki $icomment 629 +@ } elseif {$mutype eq "Plain Text"} { 630 +@ set r [randhex] 631 +@ wiki "<verbatim-$r>\n[string trimright $icomment]\n</verbatim-$r>" 632 +@ } elseif {$mutype eq {[links only]}} { 633 +@ set r [randhex] 634 +@ wiki "<verbatim-$r links>\n[string trimright $icomment]</verbatim-$r>" 635 +@ } else { 636 +@ wiki "<nowiki>\n[string trimright $icomment]\n</nowiki>" 637 +@ } 638 +@ </th1> 639 +@ <hr> 548 640 @ </td></tr> 549 641 @ <th1>enable_output 1</th1> 550 -@ 551 -@ <tr><td align="right"></td><td> 552 -@ <input type="submit" name="submit" value="Submit Changes" /> 642 +@ 643 +@ <tr> 644 +@ <td align="right"> 645 +@ <input type="submit" name="preview" value="Preview" /> 646 +@ </td> 647 +@ <td align="left">See how the description will appear after formatting.</td> 648 +@ </tr> 649 +@ 650 +@ <th1>enable_output [info exists preview]</th1> 651 +@ <tr> 652 +@ <td align="right"> 653 +@ <input type="submit" name="submit" value="Submit" /> 654 +@ </td> 655 +@ <td align="left">Apply the changes shown above</td> 656 +@ </tr> 657 +@ <th1>enable_output 1</th1> 658 +@ 659 +@ <tr> 660 +@ <td align="right"> 553 661 @ <input type="submit" name="cancel" value="Cancel" /> 554 -@ </td></tr> 662 +@ </td> 663 +@ <td>Abandon this edit</td> 664 +@ </tr> 665 +@ 555 666 @ </table> 556 667 ; 557 668 558 669 /* 559 670 ** Return the code used to generate the edit ticket page 560 671 */ 561 672 const char *ticket_editpage_code(void){
Changes to src/wikiformat.c.
49 49 #define ATTR_COLSPAN 10 50 50 #define ATTR_COMPACT 11 51 51 #define ATTR_FACE 12 52 52 #define ATTR_HEIGHT 13 53 53 #define ATTR_HREF 14 54 54 #define ATTR_HSPACE 15 55 55 #define ATTR_ID 16 56 -#define ATTR_NAME 17 57 -#define ATTR_ROWSPAN 18 58 -#define ATTR_SIZE 19 59 -#define ATTR_SRC 20 60 -#define ATTR_START 21 61 -#define ATTR_STYLE 22 62 -#define ATTR_TARGET 23 63 -#define ATTR_TYPE 24 64 -#define ATTR_VALIGN 25 65 -#define ATTR_VALUE 26 66 -#define ATTR_VSPACE 27 67 -#define ATTR_WIDTH 28 56 +#define ATTR_LINKS 17 57 +#define ATTR_NAME 18 58 +#define ATTR_ROWSPAN 19 59 +#define ATTR_SIZE 20 60 +#define ATTR_SRC 21 61 +#define ATTR_START 22 62 +#define ATTR_STYLE 23 63 +#define ATTR_TARGET 24 64 +#define ATTR_TYPE 25 65 +#define ATTR_VALIGN 26 66 +#define ATTR_VALUE 27 67 +#define ATTR_VSPACE 28 68 +#define ATTR_WIDTH 29 68 69 #define AMSK_ALIGN 0x00000001 69 70 #define AMSK_ALT 0x00000002 70 71 #define AMSK_BGCOLOR 0x00000004 71 72 #define AMSK_BORDER 0x00000008 72 73 #define AMSK_CELLPADDING 0x00000010 73 74 #define AMSK_CELLSPACING 0x00000020 74 75 #define AMSK_CLASS 0x00000040 ................................................................................ 77 78 #define AMSK_COLSPAN 0x00000200 78 79 #define AMSK_COMPACT 0x00000400 79 80 #define AMSK_FACE 0x00000800 80 81 #define AMSK_HEIGHT 0x00001000 81 82 #define AMSK_HREF 0x00002000 82 83 #define AMSK_HSPACE 0x00004000 83 84 #define AMSK_ID 0x00008000 84 -#define AMSK_NAME 0x00010000 85 -#define AMSK_ROWSPAN 0x00020000 86 -#define AMSK_SIZE 0x00040000 87 -#define AMSK_SRC 0x00080000 88 -#define AMSK_START 0x00100000 89 -#define AMSK_STYLE 0x00200000 90 -#define AMSK_TARGET 0x00400000 91 -#define AMSK_TYPE 0x00800000 92 -#define AMSK_VALIGN 0x01000000 93 -#define AMSK_VALUE 0x02000000 94 -#define AMSK_VSPACE 0x04000000 95 -#define AMSK_WIDTH 0x08000000 85 +#define AMSK_LINKS 0x00010000 86 +#define AMSK_NAME 0x00020000 87 +#define AMSK_ROWSPAN 0x00040000 88 +#define AMSK_SIZE 0x00080000 89 +#define AMSK_SRC 0x00100000 90 +#define AMSK_START 0x00200000 91 +#define AMSK_STYLE 0x00400000 92 +#define AMSK_TARGET 0x00800000 93 +#define AMSK_TYPE 0x01000000 94 +#define AMSK_VALIGN 0x02000000 95 +#define AMSK_VALUE 0x04000000 96 +#define AMSK_VSPACE 0x08000000 97 +#define AMSK_WIDTH 0x10000000 96 98 97 99 static const struct AllowedAttribute { 98 100 const char *zName; 99 101 unsigned int iMask; 100 102 } aAttribute[] = { 101 103 { 0, 0 }, 102 104 { "align", AMSK_ALIGN, }, ................................................................................ 111 113 { "colspan", AMSK_COLSPAN, }, 112 114 { "compact", AMSK_COMPACT, }, 113 115 { "face", AMSK_FACE, }, 114 116 { "height", AMSK_HEIGHT, }, 115 117 { "href", AMSK_HREF, }, 116 118 { "hspace", AMSK_HSPACE, }, 117 119 { "id", AMSK_ID, }, 120 + { "links", AMSK_LINKS, }, 118 121 { "name", AMSK_NAME, }, 119 122 { "rowspan", AMSK_ROWSPAN, }, 120 123 { "size", AMSK_SIZE, }, 121 124 { "src", AMSK_SRC, }, 122 125 { "start", AMSK_START, }, 123 126 { "style", AMSK_STYLE, }, 124 127 { "target", AMSK_TARGET, }, ................................................................................ 436 439 */ 437 440 static int markupLength(const char *z){ 438 441 int n = 1; 439 442 int inparen = 0; 440 443 int c; 441 444 if( z[n]=='/' ){ n++; } 442 445 if( !fossil_isalpha(z[n]) ) return 0; 443 - while( fossil_isalnum(z[n]) ){ n++; } 446 + while( fossil_isalnum(z[n]) || z[n]=='-' ){ n++; } 444 447 c = z[n]; 445 448 if( c=='/' && z[n+1]=='>' ){ return n+2; } 446 449 if( c!='>' && !fossil_isspace(c) ) return 0; 447 450 while( (c = z[n])!=0 && (c!='>' || inparen) ){ 448 451 if( c==inparen ){ 449 452 inparen = 0; 450 453 }else if( inparen==0 && (c=='"' || c=='\'') ){ ................................................................................ 748 751 if( j<sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]); 749 752 i++; 750 753 } 751 754 zTag[j] = 0; 752 755 p->iCode = findTag(zTag); 753 756 p->iType = aMarkup[p->iCode].iType; 754 757 p->nAttr = 0; 758 + c = 0; 759 + if( z[i]=='-' ){ 760 + p->aAttr[0].iACode = iACode = ATTR_ID; 761 + i++; 762 + p->aAttr[0].zValue = &z[i]; 763 + while( fossil_isalnum(z[i]) ){ i++; } 764 + p->aAttr[0].cTerm = c = z[i]; 765 + z[i++] = 0; 766 + p->nAttr = 1; 767 + if( c=='>' ) return; 768 + } 755 769 while( fossil_isspace(z[i]) ){ i++; } 756 - while( p->nAttr<8 && fossil_isalpha(z[i]) ){ 770 + while( c!='>' && p->nAttr<8 && fossil_isalpha(z[i]) ){ 757 771 int attrOk; /* True to preserver attribute. False to ignore it */ 758 772 j = 0; 759 773 while( fossil_isalnum(z[i]) ){ 760 774 if( j<sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]); 761 775 i++; 762 776 } 763 777 zTag[j] = 0; ................................................................................ 1159 1173 || strncmp(zTarget, "https:", 6)==0 1160 1174 || strncmp(zTarget, "ftp:", 4)==0 1161 1175 || strncmp(zTarget, "mailto:", 7)==0 1162 1176 ){ 1163 1177 blob_appendf(p->pOut, "<a href=\"%s\">", zTarget); 1164 1178 }else if( zTarget[0]=='/' ){ 1165 1179 blob_appendf(p->pOut, "<a href=\"%s%h\">", g.zTop, zTarget); 1166 - }else if( zTarget[0]=='.' || zTarget[0]=='#' ){ 1180 + }else if( zTarget[0]=='.' 1181 + && (zTarget[1]=='/' || (zTarget[1]=='.' && zTarget[2]=='/')) 1182 + && (p->state & WIKI_LINKSONLY)==0 ){ 1183 + blob_appendf(p->pOut, "<a href=\"%h\">", zTarget); 1184 + }else if( zTarget[0]=='#' ){ 1167 1185 blob_appendf(p->pOut, "<a href=\"%h\">", zTarget); 1168 1186 }else if( is_valid_uuid(zTarget) ){ 1169 1187 int isClosed = 0; 1170 1188 if( is_ticket(zTarget, &isClosed) ){ 1171 1189 /* Special display processing for tickets. Display the hyperlink 1172 1190 ** as crossed out if the ticket is closed. 1173 1191 */ ................................................................................ 1431 1449 } 1432 1450 case TOKEN_MARKUP: { 1433 1451 const char *zId; 1434 1452 int iDiv; 1435 1453 parseMarkup(&markup, z); 1436 1454 1437 1455 /* Markup of the form </div id=ID> where there is a matching 1438 - ** ID somewhere on the stack. Exit the verbatim if were are in 1439 - ** it. Pop the stack up to the matching <div>. Discard the 1440 - ** </div> 1456 + ** ID somewhere on the stack. Exit any contained verbatim. 1457 + ** Pop the stack up to the matching <div>. Discard the </div> 1441 1458 */ 1442 1459 if( markup.iCode==MARKUP_DIV && markup.endTag && 1443 1460 (zId = markupId(&markup))!=0 && 1444 1461 (iDiv = findTagWithId(p, MARKUP_DIV, zId))>=0 1445 1462 ){ 1446 1463 if( p->inVerbatim ){ 1447 1464 p->inVerbatim = 0; ................................................................................ 1525 1542 p->zVerbatimId = 0; 1526 1543 p->inVerbatim = 1; 1527 1544 p->preVerbState = p->state; 1528 1545 p->state &= ~ALLOW_WIKI; 1529 1546 for(ii=0; ii<markup.nAttr; ii++){ 1530 1547 if( markup.aAttr[ii].iACode == ATTR_ID ){ 1531 1548 p->zVerbatimId = markup.aAttr[ii].zValue; 1532 - }else if( markup.aAttr[ii].iACode == ATTR_TYPE ){ 1533 - if( fossil_stricmp(markup.aAttr[ii].zValue, "allow-links")==0 ){ 1534 - p->state |= ALLOW_LINKS; 1535 - }else{ 1536 - blob_appendf(p->pOut, "<pre name='code' class='%s'>", 1537 - markup.aAttr[ii].zValue); 1538 - vAttrDidAppend=1; 1539 - } 1549 + }else if( markup.aAttr[ii].iACode==ATTR_TYPE ){ 1550 + blob_appendf(p->pOut, "<pre name='code' class='%s'>", 1551 + markup.aAttr[ii].zValue); 1552 + vAttrDidAppend=1; 1553 + }else if( markup.aAttr[ii].iACode==ATTR_LINKS 1554 + && !is_false(markup.aAttr[ii].zValue) ){ 1555 + p->state |= ALLOW_LINKS; 1540 1556 } 1541 1557 } 1542 1558 if( !vAttrDidAppend ) { 1543 1559 endAutoParagraph(p); 1544 1560 blob_append(p->pOut, "<pre class='verbatim'>",-1); 1545 1561 } 1546 1562 p->wantAutoParagraph = 0;