Index: src/attach.c ================================================================== --- src/attach.c +++ src/attach.c @@ -214,11 +214,11 @@ }else{ rid = content_put(pAttach); db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid); db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); } - manifest_crosslink(rid, pAttach); + manifest_crosslink(rid, pAttach, MC_NONE); } /* ** WEBPAGE: attachadd @@ -431,11 +431,11 @@ blob_appendf(&manifest, "D %s\n", zDate); blob_appendf(&manifest, "U %F\n", g.zLogin ? g.zLogin : "nobody"); md5sum_blob(&manifest, &cksum); blob_appendf(&manifest, "Z %b\n", &cksum); rid = content_put(&manifest); - manifest_crosslink(rid, &manifest); + manifest_crosslink(rid, &manifest, MC_NONE); db_end_transaction(0); @

The attachment below has been deleted.

} if( P("del") Index: src/branch.c ================================================================== --- src/branch.c +++ src/branch.c @@ -153,12 +153,12 @@ brid = content_put_ex(&branch, 0, 0, 0, isPrivate); if( brid==0 ){ fossil_fatal("trouble committing manifest: %s", g.zErrMsg); } db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid); - if( manifest_crosslink(brid, &branch)==0 ){ - fossil_fatal("unable to install new manifest"); + if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){ + fossil_fatal("%s\n", g.zErrMsg); } assert( blob_is_reset(&branch) ); content_deltify(rootid, brid, 0); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", brid); fossil_print("New branch: %s\n", zUuid); Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -1816,11 +1816,14 @@ nvid = content_put(&manifest); if( nvid==0 ){ fossil_fatal("trouble committing manifest: %s", g.zErrMsg); } db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nvid); - manifest_crosslink(nvid, &manifest); + if( manifest_crosslink(nvid, &manifest, + dryRunFlag ? MC_NONE : MC_PERMIT_HOOKS)==0 ){ + fossil_fatal("%s\n", g.zErrMsg); + } assert( blob_is_reset(&manifest) ); content_deltify(vid, nvid, 0); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nvid); db_prepare(&q, "SELECT uuid,merge FROM vmerge JOIN blob ON merge=rid" Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -98,10 +98,11 @@ { "timeline-plaintext", CONFIGSET_SKIN }, { "adunit", CONFIGSET_SKIN }, { "adunit-omit-if-admin", CONFIGSET_SKIN }, { "adunit-omit-if-user", CONFIGSET_SKIN }, { "th1-setup", CONFIGSET_TH1 }, + { "th1-uri-regexp", CONFIGSET_TH1 }, #ifdef FOSSIL_ENABLE_TCL { "tcl", CONFIGSET_TH1 }, { "tcl-setup", CONFIGSET_TH1 }, #endif @@ -138,10 +139,12 @@ { "@shun", CONFIGSET_SHUN }, { "xfer-common-script", CONFIGSET_XFER }, { "xfer-push-script", CONFIGSET_XFER }, + { "xfer-commit-script", CONFIGSET_XFER }, + { "xfer-ticket-script", CONFIGSET_XFER }, }; static int iConfig = 0; /* Index: src/content.c ================================================================== --- src/content.c +++ src/content.c @@ -388,11 +388,11 @@ int i; /* Parse the object rid itself */ if( linkFlag ){ content_get(rid, &content); - manifest_crosslink(rid, &content); + manifest_crosslink(rid, &content, MC_NONE); assert( blob_is_reset(&content) ); } /* Parse all delta-manifests that depend on baseline-manifest rid */ db_prepare(&q, "SELECT rid FROM orphan WHERE baseline=%d", rid); @@ -405,11 +405,11 @@ aChild[nChildUsed++] = child; } db_finalize(&q); for(i=0; iport); + *(int*)&addr.sin_addr = inet_addr(pUrlData->name); if( -1 == *(int*)&addr.sin_addr ){ #ifndef FOSSIL_STATIC_LINK struct hostent *pHost; - pHost = gethostbyname(g.urlName); + pHost = gethostbyname(pUrlData->name); if( pHost!=0 ){ memcpy(&addr.sin_addr,pHost->h_addr_list[0],pHost->h_length); }else #endif { - socket_set_errmsg("can't resolve host name: %s", g.urlName); + socket_set_errmsg("can't resolve host name: %s", pUrlData->name); return 1; } } addrIsInit = 1; @@ -167,11 +167,12 @@ if( iSocket<0 ){ socket_set_errmsg("cannot create a socket"); return 1; } if( connect(iSocket,(struct sockaddr*)&addr,sizeof(addr))<0 ){ - socket_set_errmsg("cannot connect to host %s:%d", g.urlName, g.urlPort); + socket_set_errmsg("cannot connect to host %s:%d", pUrlData->name, + pUrlData->port); socket_close(); return 1; } #if !defined(_WIN32) signal(SIGPIPE, SIG_IGN); @@ -215,16 +216,16 @@ /* ** Attempt to resolve g.urlName to IP and setup g.zIpAddr so rcvfrom gets ** populated. For hostnames with more than one IP (or if overridden in ** ~/.ssh/config) the rcvfrom may not match the host to which we connect. */ -void socket_ssh_resolve_addr(void){ +void socket_ssh_resolve_addr(UrlData *pUrlData){ struct hostent *pHost; /* Used to make best effort for rcvfrom */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); - pHost = gethostbyname(g.urlName); + pHost = gethostbyname(pUrlData->name); if( pHost!=0 ){ memcpy(&addr.sin_addr,pHost->h_addr_list[0],pHost->h_length); g.zIpAddr = mprintf("%s", inet_ntoa(addr.sin_addr)); } } Index: src/http_ssl.c ================================================================== --- src/http_ssl.c +++ src/http_ssl.c @@ -183,11 +183,11 @@ ** g.urlName Name of the server. Ex: www.fossil-scm.org ** g.urlPort TCP/IP port to use. Ex: 80 ** ** Return the number of errors. */ -int ssl_open(void){ +int ssl_open(UrlData *pUrlData){ X509 *cert; int hasSavedCertificate = 0; int trusted = 0; unsigned long e; @@ -194,11 +194,11 @@ ssl_global_init(); /* Get certificate for current server from global config and * (if we have it in config) add it to certificate store. */ - cert = ssl_get_certificate(&trusted); + cert = ssl_get_certificate(pUrlData, &trusted); if ( cert!=NULL ){ X509_STORE_add_cert(SSL_CTX_get_cert_store(sslCtx), cert); X509_free(cert); hasSavedCertificate = 1; } @@ -205,11 +205,11 @@ iBio = BIO_new_ssl_connect(sslCtx); BIO_get_ssl(iBio, &ssl); #if (SSLEAY_VERSION_NUMBER >= 0x00908070) && !defined(OPENSSL_NO_TLSEXT) - if( !SSL_set_tlsext_host_name(ssl, g.urlName) ){ + if( !SSL_set_tlsext_host_name(ssl, pUrlData->name) ){ fossil_warning("WARNING: failed to set server name indication (SNI), " "continuing without it.\n"); } #endif @@ -218,23 +218,25 @@ ssl_set_errmsg("SSL: cannot open SSL (%s)", ERR_reason_error_string(ERR_get_error())); return 1; } - BIO_set_conn_hostname(iBio, g.urlName); - BIO_set_conn_int_port(iBio, &g.urlPort); + BIO_set_conn_hostname(iBio, pUrlData->name); + BIO_set_conn_int_port(iBio, &pUrlData->port); if( BIO_do_connect(iBio)<=0 ){ ssl_set_errmsg("SSL: cannot connect to host %s:%d (%s)", - g.urlName, g.urlPort, ERR_reason_error_string(ERR_get_error())); + pUrlData->name, pUrlData->port, + ERR_reason_error_string(ERR_get_error())); ssl_close(); return 1; } if( BIO_do_handshake(iBio)<=0 ) { ssl_set_errmsg("Error establishing SSL connection %s:%d (%s)", - g.urlName, g.urlPort, ERR_reason_error_string(ERR_get_error())); + pUrlData->name, pUrlData->port, + ERR_reason_error_string(ERR_get_error())); ssl_close(); return 1; } /* Check if certificate is valid */ cert = SSL_get_peer_certificate(ssl); @@ -281,11 +283,11 @@ " certificates list\n\n" "If you are not expecting this message, answer no and " "contact your server\nadministrator.\n\n" "Accept certificate for host %s (a=always/y/N)? ", X509_verify_cert_error_string(e), desc, warning, - g.urlName); + pUrlData->name); BIO_free(mem); prompt_user(prompt, &ans); free(prompt); cReply = blob_str(&ans)[0]; @@ -302,11 +304,11 @@ &ans); cReply = blob_str(&ans)[0]; trusted = ( cReply=='a' || cReply=='A' ); blob_reset(&ans); } - ssl_save_certificate(cert, trusted); + ssl_save_certificate(pUrlData, cert, trusted); } } /* Set the Global.zIpAddr variable to the server we are talking to. ** This is used to populate the ipaddr column of the rcvfrom table, @@ -323,44 +325,44 @@ } /* ** Save certificate to global config. */ -void ssl_save_certificate(X509 *cert, int trusted){ +void ssl_save_certificate(UrlData *pUrlData, X509 *cert, int trusted){ BIO *mem; char *zCert, *zHost; mem = BIO_new(BIO_s_mem()); PEM_write_bio_X509(mem, cert); BIO_write(mem, "", 1); /* nul-terminate mem buffer */ BIO_get_mem_data(mem, &zCert); - zHost = mprintf("cert:%s", g.urlName); + zHost = mprintf("cert:%s", pUrlData->name); db_set(zHost, zCert, 1); free(zHost); - zHost = mprintf("trusted:%s", g.urlName); + zHost = mprintf("trusted:%s", pUrlData->name); db_set_int(zHost, trusted, 1); free(zHost); BIO_free(mem); } /* ** Get certificate for g.urlName from global config. ** Return NULL if no certificate found. */ -X509 *ssl_get_certificate(int *pTrusted){ +X509 *ssl_get_certificate(UrlData *pUrlData, int *pTrusted){ char *zHost, *zCert; BIO *mem; X509 *cert; - zHost = mprintf("cert:%s", g.urlName); + zHost = mprintf("cert:%s", pUrlData->name); zCert = db_get(zHost, NULL); free(zHost); if ( zCert==NULL ) return NULL; if ( pTrusted!=0 ){ - zHost = mprintf("trusted:%s", g.urlName); + zHost = mprintf("trusted:%s", pUrlData->name); *pTrusted = db_get_int(zHost, 0); free(zHost); } mem = BIO_new(BIO_s_mem()); Index: src/http_transport.c ================================================================== --- src/http_transport.c +++ src/http_transport.c @@ -52,13 +52,13 @@ /* ** Return the current transport error message. */ -const char *transport_errmsg(void){ +const char *transport_errmsg(UrlData *pUrlData){ #ifdef FOSSIL_ENABLE_SSL - if( g.urlIsHttps ){ + if( pUrlData->isHttps ){ return ssl_errmsg(); } #endif return socket_errmsg(); } @@ -86,47 +86,47 @@ #endif /* ** SSH initialization of the transport layer */ -int transport_ssh_open(void){ +int transport_ssh_open(UrlData *pUrlData){ /* For SSH we need to create and run SSH fossil http ** to talk to the remote machine. */ const char *zSsh; /* The base SSH command */ Blob zCmd; /* The SSH command */ char *zHost; /* The host name to contact */ int n; /* Size of prefix string */ - socket_ssh_resolve_addr(); + socket_ssh_resolve_addr(pUrlData); zSsh = db_get("ssh-command", zDefaultSshCmd); blob_init(&zCmd, zSsh, -1); - if( g.urlPort!=g.urlDfltPort && g.urlPort ){ + if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ #ifdef __MINGW32__ - blob_appendf(&zCmd, " -P %d", g.urlPort); + blob_appendf(&zCmd, " -P %d", pUrlData->port); #else - blob_appendf(&zCmd, " -p %d", g.urlPort); + blob_appendf(&zCmd, " -p %d", pUrlData->port); #endif } if( g.fSshTrace ){ fossil_force_newline(); fossil_print("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ } - if( g.urlUser && g.urlUser[0] ){ - zHost = mprintf("%s@%s", g.urlUser, g.urlName); + if( pUrlData->user && pUrlData->user[0] ){ + zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); }else{ - zHost = mprintf("%s", g.urlName); + zHost = mprintf("%s", pUrlData->name); } n = blob_size(&zCmd); blob_append(&zCmd, " ", 1); shell_escape(&zCmd, zHost); blob_append(&zCmd, " ", 1); - shell_escape(&zCmd, mprintf("%s", g.urlFossil)); + shell_escape(&zCmd, mprintf("%s", pUrlData->fossil)); blob_append(&zCmd, " test-http", 10); - if( g.urlPath && g.urlPath[0] ){ + if( pUrlData->path && pUrlData->path[0] ){ blob_append(&zCmd, " ", 1); - shell_escape(&zCmd, mprintf("%s", g.urlPath)); + shell_escape(&zCmd, mprintf("%s", pUrlData->path)); } if( g.fSshTrace ){ fossil_print("%s\n", blob_str(&zCmd)+n); /* Show tail of SSH command */ } free(zHost); @@ -146,25 +146,25 @@ ** g.urlPort TCP/IP port. Ex: 80 ** g.urlIsHttps Use TLS for the connection ** ** Return the number of errors. */ -int transport_open(void){ +int transport_open(UrlData *pUrlData){ int rc = 0; if( transport.isOpen==0 ){ - if( g.urlIsSsh ){ - rc = transport_ssh_open(); + if( pUrlData->isSsh ){ + rc = transport_ssh_open(pUrlData); if( rc==0 ) transport.isOpen = 1; - }else if( g.urlIsHttps ){ + }else if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL - rc = ssl_open(); + rc = ssl_open(pUrlData); if( rc==0 ) transport.isOpen = 1; #else socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); rc = 1; #endif - }else if( g.urlIsFile ){ + }else if( pUrlData->isFile ){ sqlite3_uint64 iRandId; sqlite3_randomness(sizeof(iRandId), &iRandId); transport.zOutFile = mprintf("%s-%llu-out.http", g.zRepositoryName, iRandId); transport.zInFile = mprintf("%s-%llu-in.http", @@ -173,21 +173,21 @@ if( transport.pFile==0 ){ fossil_fatal("cannot output temporary file: %s", transport.zOutFile); } transport.isOpen = 1; }else{ - rc = socket_open(); + rc = socket_open(pUrlData); if( rc==0 ) transport.isOpen = 1; } } return rc; } /* ** Close the current connection */ -void transport_close(void){ +void transport_close(UrlData *pUrlData){ if( transport.isOpen ){ free(transport.pBuf); transport.pBuf = 0; transport.nAlloc = 0; transport.nUsed = 0; @@ -194,17 +194,17 @@ transport.iCursor = 0; if( transport.pLog ){ fclose(transport.pLog); transport.pLog = 0; } - if( g.urlIsSsh ){ + if( pUrlData->isSsh ){ transport_ssh_close(); - }else if( g.urlIsHttps ){ + }else if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL ssl_close(); #endif - }else if( g.urlIsFile ){ + }else if( pUrlData->isFile ){ if( transport.pFile ){ fclose(transport.pFile); transport.pFile = 0; } file_delete(transport.zInFile); @@ -219,28 +219,28 @@ } /* ** Send content over the wire. */ -void transport_send(Blob *toSend){ +void transport_send(UrlData *pUrlData, Blob *toSend){ char *z = blob_buffer(toSend); int n = blob_size(toSend); transport.nSent += n; - if( g.urlIsSsh ){ + if( pUrlData->isSsh ){ fwrite(z, 1, n, sshOut); fflush(sshOut); - }else if( g.urlIsHttps ){ + }else if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL int sent; while( n>0 ){ sent = ssl_send(0, z, n); /* printf("Sent %d of %d bytes\n", sent, n); fflush(stdout); */ if( sent<=0 ) break; n -= sent; } #endif - }else if( g.urlIsFile ){ + }else if( pUrlData->isFile ){ fwrite(z, 1, n, transport.pFile); }else{ int sent; while( n>0 ){ sent = socket_send(0, z, n); @@ -253,16 +253,16 @@ /* ** This routine is called when the outbound message is complete and ** it is time to being receiving a reply. */ -void transport_flip(void){ - if( g.urlIsFile ){ +void transport_flip(UrlData *pUrlData){ + if( pUrlData->isFile ){ char *zCmd; fclose(transport.pFile); zCmd = mprintf("\"%s\" http \"%s\" \"%s\" \"%s\" 127.0.0.1 --localauth", - g.nameOfExe, g.urlName, transport.zOutFile, transport.zInFile + g.nameOfExe, pUrlData->name, transport.zOutFile, transport.zInFile ); fossil_system(zCmd); free(zCmd); transport.pFile = fossil_fopen(transport.zInFile, "rb"); } @@ -282,21 +282,21 @@ /* ** This routine is called when the inbound message has been received ** and it is time to start sending again. */ -void transport_rewind(void){ - if( g.urlIsFile ){ - transport_close(); +void transport_rewind(UrlData *pUrlData){ + if( pUrlData->isFile ){ + transport_close(pUrlData); } } /* ** Read N bytes of content directly from the wire and write into ** the buffer. */ -static int transport_fetch(char *zBuf, int N){ +static int transport_fetch(UrlData *pUrlData, char *zBuf, int N){ int got; if( sshIn ){ int x; int wanted = N; got = 0; @@ -304,17 +304,17 @@ x = read(sshIn, &zBuf[got], wanted); if( x<=0 ) break; got += x; wanted -= x; } - }else if( g.urlIsHttps ){ + }else if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL got = ssl_receive(0, zBuf, N); #else got = 0; #endif - }else if( g.urlIsFile ){ + }else if( pUrlData->isFile ){ got = fread(zBuf, 1, N, transport.pFile); }else{ got = socket_receive(0, zBuf, N); } /* printf("received %d of %d bytes\n", got, N); fflush(stdout); */ @@ -327,11 +327,11 @@ /* ** Read N bytes of content from the wire and store in the supplied buffer. ** Return the number of bytes actually received. */ -int transport_receive(char *zBuf, int N){ +int transport_receive(UrlData *pUrlData, char *zBuf, int N){ int onHand; /* Bytes current held in the transport buffer */ int nByte = 0; /* Bytes of content received */ onHand = transport.nUsed - transport.iCursor; if( g.fSshTrace){ @@ -351,11 +351,11 @@ N -= toMove; zBuf += toMove; nByte += toMove; } if( N>0 ){ - int got = transport_fetch(zBuf, N); + int got = transport_fetch(pUrlData, zBuf, N); if( got>0 ){ nByte += got; transport.nRcvd += got; } } @@ -366,11 +366,11 @@ /* ** Load up to N new bytes of content into the transport.pBuf buffer. ** The buffer itself might be moved. And the transport.iCursor value ** might be reset to 0. */ -static void transport_load_buffer(int N){ +static void transport_load_buffer(UrlData *pUrlData, int N){ int i, j; if( transport.nAlloc==0 ){ transport.nAlloc = N; transport.pBuf = fossil_malloc( N ); transport.iCursor = 0; @@ -388,11 +388,11 @@ transport.nAlloc = transport.nUsed + N; pNew = fossil_realloc(transport.pBuf, transport.nAlloc); transport.pBuf = pNew; } if( N>0 ){ - i = transport_fetch(&transport.pBuf[transport.nUsed], N); + i = transport_fetch(pUrlData, &transport.pBuf[transport.nUsed], N); if( i>0 ){ transport.nRcvd += i; transport.nUsed += i; } } @@ -404,18 +404,18 @@ ** from the received line and zero-terminate the result. Return a pointer ** to the line. ** ** Each call to this routine potentially overwrites the returned buffer. */ -char *transport_receive_line(void){ +char *transport_receive_line(UrlData *pUrlData){ int i; int iStart; i = iStart = transport.iCursor; while(1){ if( i >= transport.nUsed ){ - transport_load_buffer(g.urlIsSsh ? 2 : 1000); + transport_load_buffer(pUrlData, pUrlData->isSsh ? 2 : 1000); i -= iStart; iStart = 0; if( i >= transport.nUsed ){ transport.pBuf[i] = 0; transport.iCursor = i; @@ -437,15 +437,15 @@ } /* ** Global transport shutdown */ -void transport_global_shutdown(void){ - if( g.urlIsSsh ){ +void transport_global_shutdown(UrlData *pUrlData){ + if( pUrlData->isSsh ){ transport_ssh_close(); } - if( g.urlIsHttps ){ + if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL ssl_global_shutdown(); #endif }else{ socket_global_shutdown(); Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -2194,11 +2194,11 @@ md5sum_blob(&ctrl, &cksum); blob_appendf(&ctrl, "Z %b\n", &cksum); db_begin_transaction(); g.markPrivate = content_is_private(rid); nrid = content_put(&ctrl); - manifest_crosslink(nrid, &ctrl); + manifest_crosslink(nrid, &ctrl, MC_PERMIT_HOOKS); assert( blob_is_reset(&ctrl) ); db_end_transaction(0); } cgi_redirectf("ci?name=%s", zUuid); } Index: src/json_branch.c ================================================================== --- src/json_branch.c +++ src/json_branch.c @@ -291,12 +291,12 @@ brid = content_put(&branch); if( brid==0 ){ fossil_fatal("Problem committing manifest: %s", g.zErrMsg); } db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid); - if( manifest_crosslink(brid, &branch)==0 ){ - fossil_fatal("unable to install new manifest"); + if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){ + fossil_fatal("%s\n", g.zErrMsg); } assert( blob_is_reset(&branch) ); content_deltify(rootid, brid, 0); if( zNewRid ){ *zNewRid = brid; Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -114,10 +114,12 @@ #endif /* ** All global variables are in this structure. */ +#define GLOBAL_URL() ((UrlData *)(&g.urlIsFile)) + struct Global { int argc; char **argv; /* Command-line arguments to the program */ char *nameOfExe; /* Full path of executable. */ const char *zErrlog; /* Log errors to this file, if not NULL */ int isConst; /* True if the output is unchanging */ @@ -168,10 +170,14 @@ int wikiFlags; /* Wiki conversion flags applied to %w and %W */ char isHTTP; /* True if server/CGI modes, else assume CLI. */ char javascriptHyperlink; /* If true, set href= using script, not HTML */ Blob httpHeader; /* Complete text of the HTTP request header */ + /* + ** NOTE: These members MUST be kept in sync with those in the "UrlData" + ** structure defined in "url.c". + */ int urlIsFile; /* True if a "file:" url */ int urlIsHttps; /* True if a "https:" url */ int urlIsSsh; /* True if an "ssh:" url */ char *urlName; /* Hostname for http: or filename for file: */ char *urlHostname; /* The HOST: parameter on http headers */ @@ -831,10 +837,20 @@ const char *get_version(){ static const char version[] = RELEASE_VERSION " " MANIFEST_VERSION " " MANIFEST_DATE " UTC"; return version; } + +/* +** This function returns the user-agent string for Fossil, for +** use in HTTP(S) requests. +*/ +const char *get_user_agent(){ + static const char version[] = "Fossil/" RELEASE_VERSION " (" MANIFEST_DATE + " " MANIFEST_VERSION ")"; + return version; +} /* ** COMMAND: version ** ** Usage: %fossil version ?-verbose|-v? Index: src/manifest.c ================================================================== --- src/manifest.c +++ src/manifest.c @@ -42,10 +42,16 @@ */ #define PERM_REG 0 /* regular file */ #define PERM_EXE 1 /* executable */ #define PERM_LNK 2 /* symlink */ +/* +** Flags for use with manifest_crosslink(). +*/ +#define MC_NONE 0 /* default handling */ +#define MC_PERMIT_HOOKS 1 /* permit hooks to execute */ + /* ** A single F-card within a manifest */ struct ManifestFile { char *zName; /* Name of a file */ @@ -1650,34 +1656,41 @@ ** Historical note: This routine original processed manifests only. ** Processing for other control artifacts was added later. The name ** of the routine, "manifest_crosslink", and the name of this source ** file, is a legacy of its original use. */ -int manifest_crosslink(int rid, Blob *pContent){ - int i; +int manifest_crosslink(int rid, Blob *pContent, int flags){ + int i, result = TH_OK; Manifest *p; Stmt q; int parentid = 0; + const char *zScript = 0; + const char *zUuid = 0; if( (p = manifest_cache_find(rid))!=0 ){ blob_reset(pContent); }else if( (p = manifest_parse(pContent, rid, 0))==0 ){ assert( blob_is_reset(pContent) || pContent==0 ); + fossil_error(1, "syntax error in manifest"); return 0; } if( g.xlinkClusterOnly && p->type!=CFTYPE_CLUSTER ){ manifest_destroy(p); assert( blob_is_reset(pContent) ); + fossil_error(1, "no manifest"); return 0; } if( p->type==CFTYPE_MANIFEST && fetch_baseline(p, 0) ){ manifest_destroy(p); assert( blob_is_reset(pContent) ); + fossil_error(1, "cannot fetch baseline manifest"); return 0; } db_begin_transaction(); if( p->type==CFTYPE_MANIFEST ){ + zScript = xfer_commit_code(); + zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){ char *zCom; for(i=0; inParent; i++){ int pid = uuid_to_rid(p->azParent[i], 1); db_multi_exec("INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)" @@ -1768,11 +1781,11 @@ switch( p->aTag[i].zName[0] ){ case '-': type = 0; break; /* Cancel prior occurrences */ case '+': type = 1; break; /* Apply to target only */ case '*': type = 2; break; /* Propagate to descendants */ default: - fossil_fatal("unknown tag type in manifest: %s", p->aTag); + fossil_error(1, "unknown tag type in manifest: %s", p->aTag); return 0; } tag_insert(&p->aTag[i].zName[1], type, p->aTag[i].zValue, rid, p->rDate, tid); } @@ -1870,10 +1883,12 @@ } } if( p->type==CFTYPE_TICKET ){ char *zTag; + zScript = xfer_ticket_code(); + zUuid = p->zTicketUuid; assert( manifest_crosslink_busy==1 ); zTag = mprintf("tkt-%s", p->zTicketUuid); tag_insert(zTag, 1, 0, rid, p->rDate, rid); free(zTag); db_multi_exec("INSERT OR IGNORE INTO pending_tkt VALUES(%Q)", @@ -1934,33 +1949,39 @@ if( p->type==CFTYPE_CONTROL ){ Blob comment; int i; const char *zName; const char *zValue; - const char *zUuid; + const char *zTagUuid; int branchMove = 0; blob_zero(&comment); if( p->zComment ){ blob_appendf(&comment, " %s.", p->zComment); } /* Next loop expects tags to be sorted on UUID, so sort it. */ qsort(p->aTag, p->nTag, sizeof(p->aTag[0]), tag_compare); for(i=0; inTag; i++){ - zUuid = p->aTag[i].zUuid; - if( !zUuid ) continue; - if( i==0 || fossil_strcmp(zUuid, p->aTag[i-1].zUuid)!=0 ){ + zTagUuid = p->aTag[i].zUuid; + if( !zTagUuid ) continue; + if( i==0 || fossil_strcmp(zTagUuid, p->aTag[i-1].zUuid)!=0 ){ blob_appendf(&comment, " Edit [%S]:", - zUuid); + zTagUuid); branchMove = 0; + if( db_exists("SELECT 1 FROM event, blob" + " WHERE event.type='ci' AND event.objid=blob.rid" + " AND blob.uuid='%s'", zTagUuid) ){ + zScript = xfer_commit_code(); + zUuid = zTagUuid; + } } zName = p->aTag[i].zName; zValue = p->aTag[i].zValue; if( strcmp(zName, "*branch")==0 ){ blob_appendf(&comment, " Move to branch [/timeline?r=%h&nd&dp=%S | %h].", - zValue, zUuid, zValue); + zValue, zTagUuid, zValue); branchMove = 1; continue; }else if( strcmp(zName, "*bgcolor")==0 ){ blob_appendf(&comment, " Change branch background color to \"%h\".", zValue); @@ -2021,17 +2042,23 @@ p->rDate, rid, p->zUser, blob_str(&comment)+1 ); blob_reset(&comment); } db_end_transaction(0); + if( zScript && (flags & MC_PERMIT_HOOKS) ){ + result = xfer_run_common_script(); + if( result==TH_OK ){ + result = xfer_run_script(zScript, zUuid); + } + } if( p->type==CFTYPE_MANIFEST ){ manifest_cache_insert(p); }else{ manifest_destroy(p); } assert( blob_is_reset(pContent) ); - return 1; + return ( result!=TH_ERROR ); } /* ** COMMAND: test-crosslink ** @@ -2045,7 +2072,7 @@ Blob content; db_find_and_open_repository(0, 0); if( g.argc!=3 ) usage("RECORDID"); rid = name_to_rid(g.argv[2]); content_get(rid, &content); - manifest_crosslink(rid, &content); + manifest_crosslink(rid, &content, MC_NONE); } Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -250,11 +250,11 @@ blob_copy(©, pBase); pUse = © } if( zFNameFormat==0 ){ /* We are doing "fossil rebuild" */ - manifest_crosslink(rid, pUse); + manifest_crosslink(rid, pUse, MC_NONE); }else{ /* We are doing "fossil deconstruct" */ char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); char *zFile = mprintf(zFNameFormat, zUuid, zUuid+prefixLength); blob_write_to_file(pUse,zFile); Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -812,11 +812,11 @@ if( zQ && fossil_strcmp(zQ,zVal)!=0 ){ login_verify_csrf_secret(); db_set(zVar, zQ, 0); zVal = zQ; } - @ %s(zLabel) } Index: src/tag.c ================================================================== --- src/tag.c +++ src/tag.c @@ -326,11 +326,11 @@ } blob_appendf(&ctrl, "U %F\n", zUserOvrd ? zUserOvrd : g.zLogin); md5sum_blob(&ctrl, &cksum); blob_appendf(&ctrl, "Z %b\n", &cksum); nrid = content_put(&ctrl); - manifest_crosslink(nrid, &ctrl); + manifest_crosslink(nrid, &ctrl, MC_PERMIT_HOOKS); assert( blob_is_reset(&ctrl) ); } /* ** COMMAND: tag Index: src/th_main.c ================================================================== --- src/th_main.c +++ src/th_main.c @@ -827,10 +827,124 @@ rc = TH_ERROR; } re_free(pRe); return rc; } + +/* +** TH command: http ?-asynchronous? ?--? url ?payload? +** +** Perform an HTTP or HTTPS request for the specified URL. If a +** payload is present, it will be interpreted as text/plain and +** the POST method will be used; otherwise, the GET method will +** be used. Upon success, if the -asynchronous option is used, an +** empty string is returned as the result; otherwise, the response +** from the server is returned as the result. Synchronous requests +** are not currently implemented. +*/ +#define HTTP_WRONGNUMARGS "http ?-asynchronous? ?--? url ?payload?" +static int httpCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int nArg = 1; + int fAsynchronous = 0; + const char *zType, *zRegexp; + Blob payload; + ReCompiled *pRe = 0; + UrlData urlData; + + if( argc<2 || argc>5 ){ + return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS); + } + if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){ + fAsynchronous = 1; nArg++; + } + if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; + if( nArg+1!=argc && nArg+2!=argc ){ + return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); + } + memset(&urlData, '\0', sizeof(urlData)); + url_parse_local(argv[nArg], 0, &urlData); + if( urlData.isSsh || urlData.isFile ){ + Th_ErrorMessage(interp, "url must be http:// or https://", 0, 0); + return TH_ERROR; + } + zRegexp = db_get("th1-uri-regexp", 0); + if( zRegexp && zRegexp[0] ){ + const char *zErr = re_compile(&pRe, zRegexp, 0); + if( zErr ){ + Th_SetResult(interp, zErr, -1); + return TH_ERROR; + } + } + if( !pRe || !re_match(pRe, (const unsigned char *)urlData.canonical, -1) ){ + Th_SetResult(interp, "url not allowed", -1); + re_free(pRe); + return TH_ERROR; + } + re_free(pRe); + blob_zero(&payload); + if( nArg+2==argc ){ + blob_append(&payload, argv[nArg+1], argl[nArg+1]); + zType = "POST"; + }else{ + zType = "GET"; + } + if( fAsynchronous ){ + const char *zSep, *zParams; + Blob hdr; + zParams = strrchr(argv[nArg], '?'); + if( strlen(urlData.path)>0 && zParams!=argv[nArg] ){ + zSep = ""; + }else{ + zSep = "/"; + } + blob_zero(&hdr); + blob_appendf(&hdr, "%s %s%s%s HTTP/1.0\r\n", + zType, zSep, urlData.path, zParams ? zParams : ""); + if( urlData.proxyAuth ){ + blob_appendf(&hdr, "Proxy-Authorization: %s\r\n", urlData.proxyAuth); + } + if( urlData.passwd && urlData.user && urlData.passwd[0]=='#' ){ + char *zCredentials = mprintf("%s:%s", urlData.user, &urlData.passwd[1]); + char *zEncoded = encode64(zCredentials, -1); + blob_appendf(&hdr, "Authorization: Basic %s\r\n", zEncoded); + fossil_free(zEncoded); + fossil_free(zCredentials); + } + blob_appendf(&hdr, "Host: %s\r\n" + "User-Agent: %s\r\n", urlData.hostname, get_user_agent()); + if( zType[0]=='P' ){ + blob_appendf(&hdr, "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %d\r\n\r\n", blob_size(&payload)); + }else{ + blob_appendf(&hdr, "\r\n"); + } + if( transport_open(&urlData) ){ + Th_ErrorMessage(interp, transport_errmsg(&urlData), 0, 0); + blob_reset(&hdr); + blob_reset(&payload); + return TH_ERROR; + } + transport_send(&urlData, &hdr); + transport_send(&urlData, &payload); + blob_reset(&hdr); + blob_reset(&payload); + transport_close(&urlData); + Th_SetResult(interp, 0, 0); /* NOTE: Asynchronous, no results. */ + return TH_OK; + }else{ + Th_ErrorMessage(interp, + "synchronous requests are not yet implemented", 0, 0); + blob_reset(&payload); + return TH_ERROR; + } +} /* ** Make sure the interpreter has been initialized. Initialize it if ** it has not been already. ** @@ -855,10 +969,11 @@ {"enable_output", enableOutputCmd, 0}, {"hascap", hascapCmd, 0}, {"hasfeature", hasfeatureCmd, 0}, {"html", putsCmd, (void*)&aFlags[0]}, {"htmlize", htmlizeCmd, 0}, + {"http", httpCmd, 0}, {"linecount", linecntCmd, 0}, {"puts", putsCmd, (void*)&aFlags[1]}, {"query", queryCmd, 0}, {"randhex", randhexCmd, 0}, {"regexp", regexpCmd, 0}, Index: src/tkt.c ================================================================== --- src/tkt.c +++ src/tkt.c @@ -513,15 +513,16 @@ } /* ** Write a ticket into the repository. */ -static void ticket_put( +static int ticket_put( Blob *pTicket, /* The text of the ticket change record */ const char *zTktId, /* The ticket to which this change is applied */ int needMod /* True if moderation is needed */ ){ + int result; int rid = content_put_ex(pTicket, 0, 0, 0, needMod); if( rid==0 ){ fossil_fatal("trouble committing ticket: %s", g.zErrMsg); } if( needMod ){ @@ -533,13 +534,14 @@ }else{ db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid); db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); } manifest_crosslink_begin(); - manifest_crosslink(rid, pTicket); + result = (manifest_crosslink(rid, pTicket, MC_PERMIT_HOOKS)==0); assert( blob_is_reset(pTicket) ); manifest_crosslink_end(); + return result; } /* ** Subscript command: submit_ticket ** @@ -1344,11 +1346,14 @@ } blob_appendf(&tktchng, "K %s\n", zTktUuid); blob_appendf(&tktchng, "U %F\n", zUser); md5sum_blob(&tktchng, &cksum); blob_appendf(&tktchng, "Z %b\n", &cksum); - ticket_put(&tktchng, zTktUuid, 0); - printf("ticket %s succeeded for %s\n", + if( ticket_put(&tktchng, zTktUuid, 0) ){ + fossil_fatal("%s\n", g.zErrMsg); + }else{ + fossil_print("ticket %s succeeded for %s\n", (eCmd==set?"set":"add"),zTktUuid); + } } } } Index: src/url.c ================================================================== --- src/url.c +++ src/url.c @@ -39,10 +39,34 @@ #define URL_REMEMBER 0x002 /* Remember the url for later reuse */ #define URL_ASK_REMEMBER_PW 0x004 /* Ask whether to remember prompted pw */ #define URL_REMEMBER_PW 0x008 /* Should remember pw */ #define URL_PROMPTED 0x010 /* Prompted for PW already */ +/* +** The URL related data used with this subsystem. +*/ +struct UrlData { + /* + ** NOTE: These members MUST be kept in sync with the related ones in the + ** "Global" structure defined in "main.c". + */ + int isFile; /* True if a "file:" url */ + int isHttps; /* True if a "https:" url */ + int isSsh; /* True if an "ssh:" url */ + char *name; /* Hostname for http: or filename for file: */ + char *hostname; /* The HOST: parameter on http headers */ + char *protocol; /* "http" or "https" */ + int port; /* TCP port number for http: or https: */ + int dfltPort; /* The default port for the given protocol */ + char *path; /* Pathname for http: */ + char *user; /* User id for http: */ + char *passwd; /* Password for http: */ + char *canonical; /* Canonical representation of the URL */ + char *proxyAuth; /* Proxy-Authorizer: string */ + char *fossil; /* The fossil query parameter on ssh: */ + unsigned flags; /* Boolean flags controlling URL processing */ +}; #endif /* INTERFACE */ /* ** Convert a string to lower-case. @@ -51,10 +75,201 @@ while( *z ){ *z = fossil_tolower(*z); z++; } } + +/* +** Parse the given URL. Populate members of the provided UrlData structure +** as follows: +** +** isFile True if FILE: +** isHttps True if HTTPS: +** isSsh True if SSH: +** protocol "http" or "https" or "file" +** name Hostname for HTTP:, HTTPS:, SSH:. Filename for FILE: +** port TCP port number for HTTP or HTTPS. +** dfltPort Default TCP port number (80 or 443). +** path Path name for HTTP or HTTPS. +** user Userid. +** passwd Password. +** hostname HOST:PORT or just HOST if port is the default. +** canonical The URL in canonical form, omitting the password +** +*/ +void url_parse_local( + const char *zUrl, + unsigned int urlFlags, + UrlData *pUrlData +){ + int i, j, c; + char *zFile = 0; + + if( zUrl==0 ){ + zUrl = db_get("last-sync-url", 0); + if( zUrl==0 ) return; + if( pUrlData->passwd==0 ){ + pUrlData->passwd = unobscure(db_get("last-sync-pw", 0)); + } + } + + if( strncmp(zUrl, "http://", 7)==0 + || strncmp(zUrl, "https://", 8)==0 + || strncmp(zUrl, "ssh://", 6)==0 + ){ + int iStart; + char *zLogin; + char *zExe; + char cQuerySep = '?'; + + pUrlData->isFile = 0; + if( zUrl[4]=='s' ){ + pUrlData->isHttps = 1; + pUrlData->protocol = "https"; + pUrlData->dfltPort = 443; + iStart = 8; + }else if( zUrl[0]=='s' ){ + pUrlData->isSsh = 1; + pUrlData->protocol = "ssh"; + pUrlData->dfltPort = 22; + pUrlData->fossil = "fossil"; + iStart = 6; + }else{ + pUrlData->isHttps = 0; + pUrlData->protocol = "http"; + pUrlData->dfltPort = 80; + iStart = 7; + } + for(i=iStart; (c=zUrl[i])!=0 && c!='/' && c!='@'; i++){} + if( c=='@' ){ + /* Parse up the user-id and password */ + for(j=iStart; juser = mprintf("%.*s", j-iStart, &zUrl[iStart]); + dehttpize(pUrlData->user); + if( jisSsh==0 ){ + urlFlags |= URL_ASK_REMEMBER_PW; + } + pUrlData->passwd = mprintf("%.*s", i-j-1, &zUrl[j+1]); + dehttpize(pUrlData->passwd); + } + if( pUrlData->isSsh ){ + urlFlags &= ~URL_ASK_REMEMBER_PW; + } + zLogin = mprintf("%t@", pUrlData->user); + for(j=i+1; (c=zUrl[j])!=0 && c!='/' && c!=':'; j++){} + pUrlData->name = mprintf("%.*s", j-i-1, &zUrl[i+1]); + i = j; + }else{ + for(i=iStart; (c=zUrl[i])!=0 && c!='/' && c!=':'; i++){} + pUrlData->name = mprintf("%.*s", i-iStart, &zUrl[iStart]); + zLogin = mprintf(""); + } + url_tolower(pUrlData->name); + if( c==':' ){ + pUrlData->port = 0; + i++; + while( (c = zUrl[i])!=0 && fossil_isdigit(c) ){ + pUrlData->port = pUrlData->port*10 + c - '0'; + i++; + } + pUrlData->hostname = mprintf("%s:%d", pUrlData->name, pUrlData->port); + }else{ + pUrlData->port = pUrlData->dfltPort; + pUrlData->hostname = pUrlData->name; + } + dehttpize(pUrlData->name); + pUrlData->path = mprintf("%s", &zUrl[i]); + for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} + if( pUrlData->path[i] ){ + pUrlData->path[i] = 0; + i++; + } + zExe = mprintf(""); + while( pUrlData->path[i]!=0 ){ + char *zName, *zValue; + zName = &pUrlData->path[i]; + zValue = zName; + while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } + if( pUrlData->path[i]=='=' ){ + pUrlData->path[i] = 0; + i++; + zValue = &pUrlData->path[i]; + while( pUrlData->path[i] && pUrlData->path[i]!='&' ){ i++; } + } + if( pUrlData->path[i] ){ + pUrlData->path[i] = 0; + i++; + } + if( fossil_strcmp(zName,"fossil")==0 ){ + pUrlData->fossil = zValue; + dehttpize(pUrlData->fossil); + zExe = mprintf("%cfossil=%T", cQuerySep, pUrlData->fossil); + cQuerySep = '&'; + } + } + + dehttpize(pUrlData->path); + if( pUrlData->dfltPort==pUrlData->port ){ + pUrlData->canonical = mprintf( + "%s://%s%T%T%s", + pUrlData->protocol, zLogin, pUrlData->name, pUrlData->path, zExe + ); + }else{ + pUrlData->canonical = mprintf( + "%s://%s%T:%d%T%s", + pUrlData->protocol, zLogin, pUrlData->name, pUrlData->port, + pUrlData->path, zExe + ); + } + if( pUrlData->isSsh && pUrlData->path[1] ) pUrlData->path++; + free(zLogin); + }else if( strncmp(zUrl, "file:", 5)==0 ){ + pUrlData->isFile = 1; + if( zUrl[5]=='/' && zUrl[6]=='/' ){ + i = 7; + }else{ + i = 5; + } + zFile = mprintf("%s", &zUrl[i]); + }else if( file_isfile(zUrl) ){ + pUrlData->isFile = 1; + zFile = mprintf("%s", zUrl); + }else if( file_isdir(zUrl)==1 ){ + zFile = mprintf("%s/FOSSIL", zUrl); + if( file_isfile(zFile) ){ + pUrlData->isFile = 1; + }else{ + free(zFile); + fossil_fatal("unknown repository: %s", zUrl); + } + }else{ + fossil_fatal("unknown repository: %s", zUrl); + } + if( urlFlags ) pUrlData->flags = urlFlags; + if( pUrlData->isFile ){ + Blob cfile; + dehttpize(zFile); + file_canonical_name(zFile, &cfile, 0); + free(zFile); + pUrlData->protocol = "file"; + pUrlData->path = ""; + pUrlData->name = mprintf("%b", &cfile); + pUrlData->canonical = mprintf("file://%T", pUrlData->name); + blob_reset(&cfile); + }else if( pUrlData->user!=0 && pUrlData->passwd==0 && (urlFlags & URL_PROMPT_PW) ){ + url_prompt_for_password_local(pUrlData); + }else if( pUrlData->user!=0 && ( urlFlags & URL_ASK_REMEMBER_PW ) ){ + if( isatty(fileno(stdin)) ){ + if( save_password_prompt(pUrlData->passwd) ){ + pUrlData->flags = urlFlags |= URL_REMEMBER_PW; + }else{ + pUrlData->flags = urlFlags &= ~URL_REMEMBER_PW; + } + } + } +} /* ** Parse the given URL, which describes a sync server. Populate variables ** in the global "g" structure as follows: ** @@ -79,175 +294,11 @@ ** ** ssh://userid@host:port/path?fossil=path/to/fossil.exe ** */ void url_parse(const char *zUrl, unsigned int urlFlags){ - int i, j, c; - char *zFile = 0; - - if( zUrl==0 ){ - zUrl = db_get("last-sync-url", 0); - if( zUrl==0 ) return; - if( g.urlPasswd==0 ){ - g.urlPasswd = unobscure(db_get("last-sync-pw", 0)); - } - } - - if( strncmp(zUrl, "http://", 7)==0 - || strncmp(zUrl, "https://", 8)==0 - || strncmp(zUrl, "ssh://", 6)==0 - ){ - int iStart; - char *zLogin; - char *zExe; - char cQuerySep = '?'; - - g.urlIsFile = 0; - if( zUrl[4]=='s' ){ - g.urlIsHttps = 1; - g.urlProtocol = "https"; - g.urlDfltPort = 443; - iStart = 8; - }else if( zUrl[0]=='s' ){ - g.urlIsSsh = 1; - g.urlProtocol = "ssh"; - g.urlDfltPort = 22; - g.urlFossil = "fossil"; - iStart = 6; - }else{ - g.urlIsHttps = 0; - g.urlProtocol = "http"; - g.urlDfltPort = 80; - iStart = 7; - } - for(i=iStart; (c=zUrl[i])!=0 && c!='/' && c!='@'; i++){} - if( c=='@' ){ - /* Parse up the user-id and password */ - for(j=iStart; jurl); } /* -** Prompt the user for the password for g.urlUser. Store the result -** in g.urlPasswd. -*/ -void url_prompt_for_password(void){ - if( g.urlIsSsh || g.urlIsFile ) return; - if( isatty(fileno(stdin)) - && (g.urlFlags & URL_PROMPT_PW)!=0 - && (g.urlFlags & URL_PROMPTED)==0 - ){ - g.urlFlags |= URL_PROMPTED; - g.urlPasswd = prompt_for_user_password(g.urlUser); - if( g.urlPasswd[0] - && (g.urlFlags & (URL_REMEMBER|URL_ASK_REMEMBER_PW))!=0 - ){ - if( save_password_prompt() ){ - g.urlFlags |= URL_REMEMBER_PW; - }else{ - g.urlFlags &= ~URL_REMEMBER_PW; +** Prompt the user for the password that corresponds to the "user" member of +** the provided UrlData structure. Store the result into the "passwd" member +** of the provided UrlData structure. +*/ +void url_prompt_for_password_local(UrlData *pUrlData){ + if( pUrlData->isSsh || pUrlData->isFile ) return; + if( isatty(fileno(stdin)) + && (pUrlData->flags & URL_PROMPT_PW)!=0 + && (pUrlData->flags & URL_PROMPTED)==0 + ){ + pUrlData->flags |= URL_PROMPTED; + pUrlData->passwd = prompt_for_user_password(pUrlData->user); + if( pUrlData->passwd[0] + && (pUrlData->flags & (URL_REMEMBER|URL_ASK_REMEMBER_PW))!=0 + ){ + if( save_password_prompt(pUrlData->passwd) ){ + pUrlData->flags |= URL_REMEMBER_PW; + }else{ + pUrlData->flags &= ~URL_REMEMBER_PW; } } }else{ fossil_fatal("missing or incorrect password for user \"%s\"", - g.urlUser); + pUrlData->user); } } + +/* +** Prompt the user for the password for g.urlUser. Store the result +** in g.urlPasswd. +*/ +void url_prompt_for_password(void){ + url_prompt_for_password_local(GLOBAL_URL()); +} /* ** Remember the URL and password if requested. */ void url_remember(void){ Index: src/user.c ================================================================== --- src/user.c +++ src/user.c @@ -132,15 +132,15 @@ } /* ** Prompt to save Fossil user password */ -int save_password_prompt(){ +int save_password_prompt(const char *passwd){ Blob x; char c; const char *old = db_get("last-sync-pw", 0); - if( (old!=0) && fossil_strcmp(unobscure(old), g.urlPasswd)==0 ){ + if( (old!=0) && fossil_strcmp(unobscure(old), passwd)==0 ){ return 0; } prompt_user("remember password (Y/n)? ", &x); c = blob_str(&x)[0]; blob_reset(&x); Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -294,11 +294,11 @@ moderation_table_create(); db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); } db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); - manifest_crosslink(nrid, pWiki); + manifest_crosslink(nrid, pWiki, MC_NONE); } /* ** Formal names and common names for the various wiki styles. */ Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -191,11 +191,11 @@ if( rid==0 ){ blob_appendf(&pXfer->err, "%s", g.zErrMsg); blob_reset(&content); }else{ if( !isPriv ) content_make_public(rid); - manifest_crosslink(rid, &content); + manifest_crosslink(rid, &content, MC_NONE); } assert( blob_is_reset(&content) ); remote_has(rid); } @@ -820,33 +820,73 @@ static void server_private_xfer_not_authorized(void){ @ error not\sauthorized\sto\ssync\sprivate\scontent } /* -** Run the specified TH1 script, if any, and returns the return code or TH_OK -** when there is no script. -*/ -static int run_script(const char *zScript){ - if( !zScript ){ - return TH_OK; /* No script, return success. */ - } - Th_FossilInit(TH_INIT_DEFAULT); /* Make sure TH1 is ready. */ - return Th_Eval(g.interp, 0, zScript, -1); -} - -/* -** Run the pre-transfer TH1 script, if any, and returns the return code. -*/ -static int run_common_script(void){ - return run_script(db_get("xfer-common-script", 0)); -} - -/* -** Run the post-push TH1 script, if any, and returns the return code. -*/ -static int run_push_script(void){ - return run_script(db_get("xfer-push-script", 0)); +** Return the common TH1 code to evaluate prior to evaluating any other +** TH1 transfer notification scripts. +*/ +const char *xfer_common_code(void){ + return db_get("xfer-common-script", 0); +} + +/* +** Return the TH1 code to evaluate when a push is processed. +*/ +const char *xfer_push_code(void){ + return db_get("xfer-push-script", 0); +} + +/* +** Return the TH1 code to evaluate when a commit is processed. +*/ +const char *xfer_commit_code(void){ + return db_get("xfer-commit-script", 0); +} + +/* +** Return the TH1 code to evaluate when a ticket change is processed. +*/ +const char *xfer_ticket_code(void){ + return db_get("xfer-ticket-script", 0); +} + +/* +** Run the specified TH1 script, if any, and returns 1 on error. +*/ +int xfer_run_script(const char *zScript, const char *zUuid){ + int result; + if( !zScript ) return TH_OK; + Th_FossilInit(TH_INIT_DEFAULT); + if( zUuid ){ + result = Th_SetVar(g.interp, "uuid", -1, zUuid, -1); + if( result!=TH_OK ){ + fossil_error(1, "%s", Th_GetResult(g.interp, 0)); + return result; + } + } + result = Th_Eval(g.interp, 0, zScript, -1); + if( result!=TH_OK ){ + fossil_error(1, "%s", Th_GetResult(g.interp, 0)); + } + return result; +} + +/* +** Runs the pre-transfer TH1 script, if any, and returns its return code. +** This script may be run multiple times. If the script performs actions +** that cannot be redone, it should use an internal [if] guard similar to +** the following: +** +** if {![info exists common_done]} { +** # ... code here +** set common_done 1 +** } +*/ +int xfer_run_common_script(void){ + Th_FossilInit(TH_INIT_DEFAULT); + return xfer_run_script(xfer_common_code(), 0); } /* ** If this variable is set, disable login checks. Used for debugging ** only. @@ -875,10 +915,11 @@ int isClone = 0; int nGimme = 0; int size; int recvConfig = 0; char *zNow; + int result; if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ fossil_redirect_home(); } g.zLogin = "anonymous"; @@ -904,13 +945,14 @@ db_begin_transaction(); db_multi_exec( "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" ); manifest_crosslink_begin(); - if( run_common_script()==TH_ERROR ){ + result = xfer_run_common_script(); + if( result==TH_ERROR ){ cgi_reset_content(); - @ error common\sscript\sfailed:\s%F(Th_GetResult(g.interp, 0)) + @ error common\sscript\sfailed:\s%F(g.zErrMsg) nErr++; } while( blob_line(xfer.pIn, &xfer.line) ){ if( blob_buffer(&xfer.line)[0]=='#' ) continue; if( blob_size(&xfer.line)==0 ) continue; @@ -1231,14 +1273,17 @@ } blobarray_reset(xfer.aToken, xfer.nToken); blob_reset(&xfer.line); } if( isPush ){ - if( run_push_script()==TH_ERROR ){ - cgi_reset_content(); - @ error push\sscript\sfailed:\s%F(Th_GetResult(g.interp, 0)) - nErr++; + if( result==TH_OK ){ + result = xfer_run_script(xfer_push_code(), 0); + if( result==TH_ERROR ){ + cgi_reset_content(); + @ error push\sscript\sfailed:\s%F(g.zErrMsg) + nErr++; + } } request_phantoms(&xfer, 500); } if( isClone && nGimme==0 ){ /* The initial "clone" message from client to server contains no @@ -1881,13 +1926,13 @@ fossil_force_newline(); fossil_print( "%s finished with %lld bytes sent, %lld bytes received\n", zOpType, nSent, nRcvd); - transport_close(); - transport_global_shutdown(); + transport_close(GLOBAL_URL()); + transport_global_shutdown(GLOBAL_URL()); db_multi_exec("DROP TABLE onremote"); manifest_crosslink_end(); content_enable_dephantomize(1); db_end_transaction(0); return nErr; } Index: src/xfersetup.c ================================================================== --- src/xfersetup.c +++ src/xfersetup.c @@ -31,16 +31,63 @@ if( !g.perm.Setup ){ login_needed(); } style_header("Transfer Setup"); + @ setup_menu_entry("Common", "xfersetup_com", "Common TH1 code run before all transfer request processing."); setup_menu_entry("Push", "xfersetup_push", "Specific TH1 code to run after \"push\" transfer requests."); + setup_menu_entry("Commit", "xfersetup_commit", + "Specific TH1 code to run after processing a commit."); + setup_menu_entry("Ticket", "xfersetup_ticket", + "Specific TH1 code to run after processing a ticket change."); @
+ + url_parse(0, 0); + if( g.urlProtocol ){ + unsigned syncFlags; + const char *zButton; + char *zWarning; + + if( db_get_boolean("dont-push", 0) ){ + syncFlags = SYNC_PULL; + zButton = "Pull"; + zWarning = 0; + }else{ + syncFlags = SYNC_PUSH | SYNC_PULL; + zButton = "Synchronize"; + zWarning = mprintf("WARNING: Pushing to \"%s\" is enabled.", + g.urlCanonical); + } + if( P("sync") ){ + user_select(); + url_enable_proxy(0); + client_sync(syncFlags, 0, 0); + } + @

Press the %h(zButton) button below to synchronize with the + @ "%h(g.urlCanonical)" repository now. This may be useful when + @ testing the various transfer scripts.

+ @

You can use the "http -async" command in your scripts, but + @ make sure the "th1-uri-regexp" setting is set first.

+ if( zWarning ){ + @ + @ %h(zWarning) + free(zWarning); + } + @ + @
+ @
+ login_insert_csrf_secret(); + @ + @
+ @
+ @ + } + style_footer(); } /* ** Common implementation for the transfer setup editor pages. @@ -140,9 +187,49 @@ "Transfer Push Script", "xfer-push-script", zDefaultXferPush, zDesc, 0, + 0, + 30 + ); +} + +static const char *zDefaultXferCommit = 0; + +/* +** WEBPAGE: xfersetup_commit +*/ +void xfersetup_commit_page(void){ + static const char zDesc[] = + @ Enter TH1 script that runs when a commit is processed. + ; + xfersetup_generic( + "Transfer Commit Script", + "xfer-commit-script", + zDefaultXferCommit, + zDesc, + 0, + 0, + 30 + ); +} + +static const char *zDefaultXferTicket = 0; + +/* +** WEBPAGE: xfersetup_ticket +*/ +void xfersetup_ticket_page(void){ + static const char zDesc[] = + @ Enter TH1 script that runs when a ticket change is processed. + ; + xfersetup_generic( + "Transfer Ticket Script", + "xfer-ticket-script", + zDefaultXferTicket, + zDesc, + 0, 0, 30 ); }