# # TWiki WikiClone ($wikiversion has version info) # # Copyright (C) 2002 Peter Thoeny, Peter@Thoeny.com # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details, published at # http://www.gnu.org/copyleft/gpl.html # # ========================= # # This is the EditTablePlugin used to edit tables in place. # # Each plugin is a package that contains the subs: # # initPlugin ( $topic, $web, $user, $installWeb ) # commonTagsHandler ( $text, $topic, $web ) # startRenderingHandler( $text, $web ) # outsidePREHandler ( $text ) # insidePREHandler ( $text ) # endRenderingHandler ( $text ) # # initPlugin is required, all other are optional. # For increased performance, DISABLE handlers you don't need. # # NOTE: To interact with TWiki use the official TWiki functions # in the &TWiki::Func module. Do not reference any functions or # variables elsewhere in TWiki!! # FIXME: The following TWiki calls used will likely break in a # future TWiki release: # TWiki::Store::readTopic(), TWiki::Store::saveTopic() # TWiki::Store::lockTopic(), TWiki::Store::topicIsLockedBy() =pod Cut paste this into EditTable.txt file. Extensions In this version (VinodKulkarni, Aug 26th 2003): Synchronization: * Synchronization from another table source. * DONE: Initialize the empty table with common columns from another table, using Source= parameter. * TODO: Synchronization of selected columns. Managing Table Schema: * Editing Table Schema * DONE: Introduce %EDITTABLE{format="tabledef"}% in a topic (should be first table). This produces a table create and manage table columns. This table is then converted into appropriate "header=" and "format=" when include= syntax is used as part of some other table. * TODO: Require a link to metadata after "Edit Table", i.e. "Edit Table Headers". But this is not a good idea; you are not supposed to change the table schema just like that. Option: Create this link with special "editschema="on"". But as of now, this will work only with include. * textarea is now accepted. But repeat work. Convert eol to BR. Access Control: * Both row and column based access controls can be used. * Row based Access control and row visibility. * acl="| | | rowacl, a,b,c | ...": The column named 'acl' will contain a list that contains user IDs and Group IDs (comma separated). If blank, and user is able to see that row, his ID will be added to this column. Only first such column will be considered. Other 'acl' columns will be treated as text. * Arguments: "a,b,c,...": entries can be: 'match', 'all:ro', userid:rw, groupid:rw and so on. rw=read-write, ro=readonly, rn=read none(i.e. no view). * EARLIER DESIGN: row_access, view_access, edit_access were separately defined. * NOTE: Need to identify special rows such as header rows. Use 'everyone' in that column. * Editing the ACL Column. Do we provide control or not? It could be ro. But for this, we require some other option that selectively makes some columns read-only. Perhaps through column-based access control. * Column based Access Control. * Some columns are visible to all, but some other columns are visible to only selected people. Example, user-id, user-name are visible to all. But 'suggested bonus' (by his boss) is visible to only top management. * We introduce a concept of column match. Can be specific userid (such as 'vinod'), 'all', group ID, 'none' i.e. disable it from view. etc. * column_acl=| a,b,c ...| a,b,c .. | a,b,c ...|..." i.e. these are new set of attributes to each column. * Editing column_acl is currently not possible through edittable dialogues. You can edit metadata using other means (see Editing table schema above). * Examples * %EDITTABLE{ ... acl="| rowacl, match, all:ro, vinod:rw | | | | |"} means first column should contain user IDs/Group IDs. 'match'ed user can see his row as well as edit. Everyone can see all rows. Login id vinod can see and edit all rows. ACL Design: Details of Implementation There are two "States" of operation, as determined by $doEdit, view mode, edit mode. And we also have another hidden state: doSave, where the table is first scanned and saved to topic, and then view mode/edit mode is selected. Standard view mode: Table is drawn as usual, except that 'Edit Table' button is added at end of table. Note that handleTableRow has no work, except to copy the row to output. Here, ACL will come into play to determine if the row is supposed to be visible to the user or not. (Implied meaning: rw,ro means visible. rn=not visible.) * For row ACLs, we upfront decide whether to show or not show the whole row. * For column ACLs: If we are going to show the row, then we are going to 'black out' the specific columns. Options: rn=Don't show at all, rb=show blank, ro,rw=show data. Edit Mode: Table is drawn in "Edit" mode; each row is converted into Form controls, with each cell getting its own name. handleTableRow loops through all columns and determines how to show each cell. * For row ACLs: Upfront decision whether or not to show this row. * For column ACLs: If we are going to allow this row to be edited, then column-by-column decision to decide one of the options: rn=Don't show at all, rb=show blank, ro=show read-only, rw=show edit control. * User may not have access to any row. In which case, he can click on "Add row", and a new row is created for him, with predetermined 'default' ACL. Save Mode: In save mode, all the cell values are available from previous form fill. * Each cell has its own name, derived from row and column. handleTableRow is then called on each row to produce the standard table output (i.e. what is actually stored in file.) * For both row and column ACLs, one of possibilities exist: * We received only specific rows and columns by user. And only as option, all rows and columns were received. * Implementing Security: Only change those rows and columns for which user has access. Ignore other cells. Security info not to be taken from the incoming row/columns. NOTE: It is option to allow change of security column. But let us not implement that in this version. * This output is then saved to topic. However, these contents are not shown to user. Instead, a redirect is made to re-render the view for the page. VinodKulkarni: Version: 2.01; 29/08/2003. =cut # ========================= package TWiki::Plugins::EditTable2Plugin; # ========================= use vars qw( $web $topic $user $installWeb $VERSION $debug $query $renderingWeb $preSp $header $footer @format $changeRows $helpTopic $nrCols $encodeStart $encodeEnd $table $dataSource $textExtractedFromDataSource $acl $rowaclHash $colaclHash $rowaclActive $colaclActive $rowlayout $totalslayout ); $VERSION = '1.001'; $encodeStart = "--EditTableEncodeStart--"; $encodeEnd = "--EditTableEncodeEnd--"; undef $table; $debug=1; # ========================= sub initPlugin { ( $topic, $web, $user, $installWeb ) = @_; # check for Plugins.pm versions if( $TWiki::Plugins::VERSION < 1 ) { &TWiki::Func::writeWarning( "Version mismatch between EditTablePlugin and Plugins.pm" ); return 0; } $query = &TWiki::Func::getCgiQuery(); if( ! $query ) { return 0; } # Get plugin preferences # $doEnable = &TWiki::Func::getPreferencesFlag( "EDITTABLE2PLUGIN_ENABLE" ) || ""; # Get plugin debug flag $debug = &TWiki::Func::getPreferencesFlag( "EDITTABLE2PLUGIN_DEBUG" ); $renderingWeb = $web; # Plugin correctly initialized &TWiki::Func::writeDebug( "- TWiki::Plugins::EditTablePlugin::initPlugin( $web.$topic.) is OK" ) if $debug; # Initialize $table such that the code will correctly detect when to # read in a topic. undef $table; return 1; } # ========================= sub extractParameters { my( $theArgs, $theHeader, $theFooter, $theFormat, $theChangeRows, $theHelpTopic, $theSource, $theAcl, $theRowLayout, $theTotalsLayout ) = @_; #debug("++++the header is : $theHeader"); my $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "header" ); $theHeader = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "footer" ); $theFooter = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "format" ); $tmp =~ s/^\s*\|*\s*//o; $tmp =~ s/\s*\|*\s*$//o; $theFormat = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "changerows" ); $theChangeRows = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "helptopic" ); $theHelpTopic = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "source" ); $theSource = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "acl" ); $theAcl = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "rowlayout" ); $theRowLayout = $tmp if( $tmp ); $tmp = &TWiki::Func::extractNameValuePair( $theArgs, "totalslayout" ); $theTotalsLayout = $tmp if( $tmp ); return ( $theHeader, $theFooter, $theFormat, $theChangeRows, $theHelpTopic, $theSource, $theAcl, $theRowLayout ); } # ========================= sub commonTagsHandler { ### my ( $text, $topic, $web ) = @_; # do not uncomment, use $_[0], $_[1]... instead &TWiki::Func::writeDebug( "- EditTablePlugin::commonTagsHandler( $_[2].$_[1] )" ) if $debug; return unless $_[0] =~ /%EDITTABLE2{(.*)}%/os; my $theWeb = $_[2]; my $theTopic = $_[1]; my $result = ""; my $tableNr = 0; my $rowNr = 0; my $enableForm = 0; my $insideTable = 0; my $doEdit = 0; my $cgiRows = -1; foreach( split( /\n/, $_[0] ) ) { if( s/(\s*)%EDITTABLE2{(.*)}%/&handleEditTableTag( $theWeb, $1, $2 )/geo ) { $enableForm = 1; $tableNr += 1; my $cgiTableNr = $query->param( 'ettablenr' ) || 0; $cgiRows = $query->param( 'etrows' ) || -1; if( $cgiTableNr == $tableNr ) { if( $query->param( 'etsave' ) ) { # [Save table] button pressed doSaveTable( $theWeb, $theTopic, $tableNr ); # never return return; # in case browser does not redirect } elsif( $query->param( 'etcancel' ) ) { # [Cancel] button pressed doCancelEdit( $theWeb, $theTopic ); # never return return; # in case browser does not redirect } elsif( $query->param( 'etaddrow' ) ) { # [Add row] button pressed $cgiRows++ if( $cgiRows >= 0 ); $doEdit = doEnableEdit( $theWeb, $theTopic, 0 ); return unless( $doEdit ); } elsif( $query->param( 'etdelrow' ) ) { # [Delete row] button pressed $cgiRows-- if( $cgiRows > 1 ); $doEdit = doEnableEdit( $theWeb, $theTopic, 0 ); return unless( $doEdit ); } elsif( $query->param( 'etedit' ) ) { # [Edit table] button pressed $doEdit = doEnableEdit( $theWeb, $theTopic, 1 ); # never return if locked or no permission return unless( $doEdit ); $cgiRows = -1; # make sure to get the actual number of rows } } } if( $enableForm ) { if( /^(\s*)\|.*\|\s*$/ ) { # found table row $result .= handleTableStart( $theWeb, $theTopic, $tableNr, $doEdit ) unless $insideTable; $insideTable = 1; $rowNr++; if( ( $doEdit ) && ( $cgiRows >= 0 ) && ( $rowNr > $cgiRows ) ) { # deleted row # ACL code if ( $rowaclActive ) { # Can delete only if the last row is owned by the same user. } $rowNr--; next; } s/^(\s*)\|(.*)/&handleTableRow( $1, $2, $tableNr, $cgiRows, $rowNr, $doEdit, 0 )/eo; # ACL Logic: Don't append this row if this was blank. next if ( ($rowaclActive) && $_ eq "" ) ; } elsif( $insideTable ) { # end of table $insideTable = 0; if( ( $doEdit ) && ( $cgiRows >= 0 ) && ( $rowNr < $cgiRows ) ) { while( $rowNr < $cgiRows ) { $rowNr++; $result .= &handleTableRow( $theSp, "", $tableNr, $cgiRows, $rowNr, $doEdit, 0 ) . "\n"; } } $result .= handleTableEnd( $theWeb, $rowNr, $doEdit ); $enableForm = 0; $doEdit = 0; $rowNr = 0; # debug("Reached end of EDITTABLE"); # We processed the table. ACL Logic at the end of the table. (No?) $dataSource=undef; $rowaclActive=0; $colaclActive=0; } if( /^\s*$/ ) { # empty line if( $enableForm ) { # empty %EDITTABLE%, so create a default table $result .= handleTableStart( $theWeb, $theTopic, $tableNr, $doEdit ); $rowNr = 0; my @otherTable; my $otherTableAsString; if ($dataSource) { @otherTable = getTableFromSource($dataSource, $header); } if( $doEdit ) { # User has clicked on Edit Table on empty table. if( $header ) { $rowNr++; $result .= &handleTableRow( $preSp, "", $tableNr, $cgiRows, $rowNr, $doEdit, 0 ) . "\n"; } # Read data from specified source. Only once to populate empty table. my $size = @otherTable; do { my $rowData; if ( defined(@otherTable)) { # Fewer number of columns is not a problem! $rowData = join( "|", @{$otherTable[$rowNr]})."|"; } $rowNr++; $result .= &handleTableRow( $preSp, $rowData, $tableNr, $cgiRows, $rowNr, $doEdit, 0 ) . "\n"; } while( ($rowNr < $cgiRows) || ($rowNr < $size)); } else { # Normal view, Empty table encountered. foreach ( @otherTable ) { $rowNr++; my $rowData = join( "|", @{$otherTable[$i++]})."|"; # Note: handleTableRow adds the first "|". $result .= &handleTableRow( $preSp, $rowData, $tableNr, $cgiRows, $rowNr, 0, 0 ) . "\n"; } } $result .= handleTableEnd( $theWeb, $rowNr, $doEdit ); $enableForm = 0; } $doEdit = 0; $rowNr = 0; } } $result .= "$_\n"; } $_[0] = $result; } # ========================= sub DISABLE_startRenderingHandler { ### my ( $text, $web ) = @_; # do not uncomment, use $_[0], $_[1] instead &TWiki::Func::writeDebug( "- EditTablePlugin::startRenderingHandler( $_[1] )" ) if $debug; # This handler is called by getRenderedVersion just before the line loop $renderingWeb = $_[1]; } # ========================= sub DISABLE_outsidePREHandler { ### my ( $text ) = @_; # do not uncomment, use $_[0] instead &TWiki::Func::writeDebug( "- EditTablePlugin::outsidePREHandler( $renderingWeb.$topic )" ) if $debug; # This handler is called by getRenderedVersion, in loop outside of
 tag
    # This is the place to define customized rendering rules

    # do custom extension rule, like for example:
    # $_[0] =~ s/old/new/go;
}

# =========================
sub DISABLE_insidePREHandler
{
### my ( $text ) = @_;   # do not uncomment, use $_[0] instead

    &TWiki::Func::writeDebug( "- EditTablePlugin::insidePREHandler( $web.$topic )" ) if $debug;

    # This handler is called by getRenderedVersion, in loop inside of 
 tag
    # This is the place to define customized rendering rules

    # do custom extension rule, like for example:
    # $_[0] =~ s/old/new/go;
}

# =========================
sub endRenderingHandler
{
### my ( $text ) = @_;   # do not uncomment, use $_[0] instead

    &TWiki::Func::writeDebug( "- EditTablePlugin::endRenderingHandler( $web.$topic )" ) if $debug;

    # This handler is called by getRenderedVersion just after the line loop

    return unless $_[0] =~ /$encodeStart/os;

    $_[0] =~ s/$encodeStart(.*?)$encodeEnd/&decodeValue($1)/geos;
}


# =========================
sub encodeValue
{
    my( $theText ) = @_;

    # WindRiver specific hack to remove SprPlugin rendering
    $theText =~ s/ what.
    @colaclHash=(); # Format: one hash for each column. $colaclHash[3]->{entity} gives acl for this entity.  Entity can be: user, group, 'owner', 'all' etc.
    @newaclHash=();


    # Layout Logic. 
    $rowlayout="";
    $totalslayout="";
    $sumsHash=undef; # And not {}.
    $occuranceHash=undef;
}


# =========================
sub handleEditTableTag
{
    my( $theWeb, $thePreSpace, $theArgs ) = @_;

    $preSp = $thePreSpace || "";
    $header = "";
    $footer = "";
    my $tFormat = "";
    $changeRows = "";

    # May be we require resetTopicGlobals() for modperl environment
    &resetEditTableGlobals();

    my $iTopic = &TWiki::Func::extractNameValuePair( $theArgs, "include" );
    if( $iTopic ) {
       # include topic to read definitions
       if( $iTopic =~ /^([^\.]+)\.(.*)$/o ) {
           $theWeb = $1;
           $iTopic = $2;
       }
       my( $meta, $text ) = &TWiki::Func::readTopic( $theWeb, $iTopic );
       $text =~ /%EDITTABLE2{(.*)}%/os;
       if( $1 ) {
           my $args = $1;
           my $fmt = &TWiki::Func::extractNameValuePair( $args, "format" );
	   if ( "$theWeb.$iTopic" ne "$web.$topic" ) {
                 # expand common vars, unless oneself to prevent recursion
                 $args = &TWiki::Func::expandCommonVariables( $1, $iTopic, $theWeb );
           }
	   # First load the arguments. But we are going to ignore $tFormat here.
           ( $header, $footer, $tFormat, $changeRows, $helpTopic, $dataSource, $acl, $rowlayout, $totalslayout ) = extractParameters( $args, $header, $footer, $tFormat, $changeRows, $helpTopic, $dataSource, $acl, $rowlayout, $totalslayout );
	   if ( $fmt eq "tabledef" ) {

	       # This is a table definition, in table format, with fixed headers and format.
               my $allTables = TWiki::Plugins::Table->new($text);

	       # TODO: Only first table is processed.
	       # Invert the table: Columns become rows and vice versa.
               my @tmpTable = $allTables->getTable("1");

	       # debug("++++tmpTable is: @tmpTable");
	       # Override header, tformat, footer. Note: Leading "|" is already removed at this stage.
	       $header=""; 
	       $tFormat="";
	       # Skip header row 0.
	       for $i (1 .. ($#tmpTable - 1) ) {
		 # debug("tmpTable is:". $tmpTable[$i][0] ." and " . $tmpTable[$i][3]);
		 $header .= $tmpTable[$i][0] . " |" ;
		 $tFormat .=  "$tmpTable[$i][1], $tmpTable[$i][2], $tmpTable[$i][3] |";
	       }
	   }
      }
    } 


    # If it is table definition, then process it.
    my $ttFormat = &TWiki::Func::extractNameValuePair( $theArgs, "format" );
    if ( $ttFormat eq "tabledef" ) {
        # This is a special table that defines the columns of the table.
	# This is how it should look like. Type columns can be: text, select, rows etc.  
	my $tmp=<<'HERE';
                  | *header* | *type* | *size* | *options/default* |
                  | *Movie Name* | text | 12 |  |
                  | *Category* | select | | comedy, tragedy, war, other |
                  | *Year*  | text | 10 |  |
HERE
        $header=" *Column* | *Type* | *Size* | *Options/Default* |";
        $tFormat=" text,10 | select, 1, text, select, row | text, 4 | text, 25 |";
	$changeRows="on";
    } else {
       ( $header, $footer, $tFormat, $changeRows, $helpTopic, $dataSource, $acl, $rowlayout, $totalslayout ) = extractParameters( $theArgs,
      $header, $footer, $tFormat, $changeRows, $helpTopic, $dataSource, $acl, $rowlayout, $totalslayout );
    }
    $header = "" if( $header =~ /^off$/oi );
    $header =~ s/^\s*\|//o;
    $header =~ s/\|\s*$//o;
    $footer = "" if( $footer =~ /^off$/oi );
    $footer =~ s/^\s*\|//o;
    $footer =~ s/\|\s*$//o;
    $changeRows = "" if( $changeRows =~ /^off$/oi );

    $tFormat =~ s/\$nop(\(\))?//gos;      # remove filler
    $tFormat =~ s/\$quot(\(\))?/\"/gos;   # expand double quote
    $tFormat =~ s/\$percnt(\(\))?/\%/gos; # expand percent
    $tFormat =~ s/\$dollar(\(\))?/\$/gos; # expand dollar

    @format = split( /\s*\|\s*/, $tFormat );
    $format[0] = "text,16" unless @format;
    $nrCols = @format;

    # === ACL Logic ===
    # 
    # Check if row ACLs are specified. Only first acl is processed. For formatting purposes, 
    # it is considered as 'text'.
    # Initialized. @rowaclHash=(); # Format:[col]->{who} => what.
    # Initialized. $rowaclActive=-1;
    # Processing syntax: acl="| row_acl:user=rw,everyone=ro | |  |"
    # LATER TOD: Include col_acl syntac as well. One column will have both row_acl and col_acl set, separated by semicolon.


    # $rowaclActive is our global indicator of whether Row ACLs are enabled. 
    $rowaclActive=0; # Active if 0 or more; Contains column in which rowacls are stored.
    $colaclActive=0; # Active if 0 or more; Contains column in which rowacls are stored.

    @rowaclHash=();
    @colaclHash=();
    @newaclHash=();

    if (defined($acl)) {
          &handleEditTableACLDefinition($acl);   
    }
    # Verify the syntax of data source:
    # "id": Table with given ID. (Number of table in topic, or "id=" parameter in Edittable.)
    # "Web.Topic:id":  Table with specified ID in given web and topic.
    # "http://.../": Extract HTML table and convert to twiki table. (Later)
    # "csvattach:Web.Topic:Attachment_name": CSV file in specified web and topic.
    $dataSource =~ s/^\s*//o;
    $dataSource =~ s/\s$//o;

    # FIXME: No handling yet of footer

    return "$preSp";
}



# Parse ACL strings and populate ACL datastructures.
# Input: "| rowacl=owner:rw,all:ro; colacl=user1:rw,mygroup:rw | colacl=... | rowacl=... |"
sub handleEditTableACLDefinition()
{ 
    my $acl=shift;
    
    $acl =~ s/^\s*\|\s*//; # Remove initial "|"
    my $col=-1;
    my @tmp= split(/\s*\|\s*/, $acl); # Get each column information
    foreach (@tmp) 
    {
       $col++;

       # We have string 'row_acl, user:rw,all:ro'
       @tmp2=split(/\s*;\s*/);
       foreach ( @tmp2 ) 
       {
          if( m/^rowacl=/ ) 
     	  {
	      # As of now, only first column in which row acls are defined is kept track of.
              if( ! $rowaclActive ) {
	            $rowaclActive=$col + 1; 
		    # Columns are counted from 1, to keep semantics of 'Active' clean.         
	      }
    	      s/^rowacl=//;
              my @args=split(/\s*,\s*/);

              if( ! defined($rowaclHash[$col])) {
		 $rowaclHash[$col]={};
	      }
	      initAclHash($rowaclHash[$col], $_);

	  } elsif ( m/^colacl=/ ) {

    	    $colaclActive=1;
    	    s/^colacl=//;

	    if (! defined($colaclHash[$col]) ) {
    	       $colaclHash[$col]={} ;
	    }
	    initAclHash($colaclHash[$col], $_);
         

	    # In case of colacls, we need to determine ACLs for currently logged in user now itself.
	    # And for each column.
            my $me= ((defined($user)) ? $user :'guest' );
    	    if ( ! defined($colaclHash[$col]->{$me}) ) 
	    {
                $colaclHash[$col]->{$me}=$colaclHash[$col]->{'all'};
            }
	 } elsif ( m/^newacl=/ ) {
            # Note: Active only if there is no default specified for the column in 'format=' string.
    	    s/^newacl=//;
            $newaclHash[$col]=$_;
	 }
     }
   }
}

# Initialize ACL data structure from the input string such as "user=rw,all=ro".
sub initAclHash()
{
    my ($hash, $str)=@_;    

    # format of each argument: who:whatperms. 
    # who can be 'everyone','match', userid, groupid, 
    # what can be: ro:read only, rw:read write, rn:don't show columns, rb:show blank and access control info when necessary.
    my $tmpaclDefined=0;
    my @args=split(/\s*,\s*/, $str);
    foreach (@args) 
    {
       s/^Main.//;  # TODO: All users are in Main web. User ID/Group ID can have "." in them. E.g. Main.UserA
       s/^\s*//;
       s/\s*$//;
       s/owner:/match:/; # Not sure how we can define owners for columns as of now.
       s/everyone:/all:/;
       /^(\w+)(:(\w+))/;
    
       if( defined($3) ) {
    	   $hash->{$1} = $3; 
    	   $tmpaclDefined=1;
       } elsif (defined($1)) {
    	   $hash->{$1} = 'rw'; 
    	   $tmpaclDefined=1;
       }
    }
    # If there are no ACLs for 'everyone', we should define
    if ( ! $tmpaclDefined ) {
       $hash->{'all'}='ro';
       $hash->{'owner'}='rw';
    }
}



# =========================
sub handleTableStart
{
    my( $theWeb, $theTopic, $theTableNr, $doEdit ) = @_;

    my $viewUrl = &TWiki::Func::getScriptUrl( $theWeb, $theTopic, "viewauth" ) . "\#edittable$theTableNr";
    my $text = "";
    $text .= "$preSp\n" if $doEdit;
    $text .= "$preSp\n";
    $text .= "$preSp
\n"; $text .= "$preSp\n"; $text .= "$preSp\n" unless $doEdit; return $text; } # ========================= sub handleTableEnd { my( $theWeb, $theRowNr, $doEdit ) = @_; my $text = "$preSp\n"; if( $doEdit ) { # Edit mode $text .= "$preSp\n"; if( $changeRows ) { $text .= "$preSp\n"; $text .= "$preSp\n" unless( $changeRows =~ /^add$/oi ); } $text .= "$preSp\n"; if( $helpTopic ) { # read help topic and show below the table if( $helpTopic =~ /^([^\.]+)\.(.*)$/o ) { $theWeb = $1; $helpTopic = $2; } my( $meta, $helpText ) = &TWiki::Func::readTopic( $theWeb, $helpTopic ); if( $helpText ) { $helpText =~ s/.*?%STARTINCLUDE%//os; $helpText =~ s/%STOPINCLUDE%.*//os; $text .= $helpText; } } } else { if ( $totalslayout ne "" ) { &handleTotalsRow(); } # View mode $text .= "$preSp\n"; } $text .= "$preSp
\n"; $text .= "$preSp\n" if $doEdit; return $text; } # ========================= sub inputElement { my ( $theTableNr, $theRowNr, $theCol, $theName, $theValue ) = @_; $theValue = "" if( $theValue eq " " ); my $text = ""; my $i = @format - 1; $i = $theCol if( $theCol < $i ); my @bits = split( /,\s*/, $format[$i] ); my $type = "text"; $type = $bits[0] if @bits > 0; my $size = 0; $size = $bits[1] if @bits > 1; my $val = ""; my $sel = ""; if( $type eq "select" ) { $size = 1 if $size < 1; $text = ""; } elsif( $type eq "row" ) { $size = $size + $theRowNr; $text = "$size"; } elsif( $type eq "label" ) { # show label text as is, and add a hidden field with value $text = $theValue; # To optimize things, only in the case where a read-only column is # being processed (inside of this unless() statement) do we actually # go out and read the original topic. Thus the reason for the # following unless() so we only read the topic the first time through. unless( defined $table ) { # To deal with the situation where TWiki variables, like # %CALC%, have already been processed and end up getting saved # in the table that way (processed), we need to read in the # topic page in raw format my( $meta, $topicContents ) = TWiki::Func::readTopic( $web, $topic ); $table = TWiki::Plugins::Table->new( $topicContents ); } my $cell = $table->getCell( $theTableNr, $theRowNr - 1, $theCol ); $theValue = $cell if( defined $cell ); # original value from file $theValue = $encodeStart . encodeValue( $theValue ) . $encodeEnd if $theValue; $text .= ""; } else { # if( $type eq "text" ) $size = 16 if $size < 1; $theValue = $encodeStart . encodeValue( $theValue ) . $encodeEnd if $theValue; $text = ""; } return $text; } #===========strToArray # takes a string and generates array from it. # Returns if array is already defined. *without check* # NOTE: This routine uses Global variables as well. sub initCells() { # Note: $str doesn't have leading "|". my ($str, $cells)=@_; if( $#{$cells} == 0 ) { @{$cells} = split( /\|/, $str ); my $tmp = $#{$cells}; $nrCols = $tmp if( $tmp > $nrCols ); # expand number of cols # debug("initCells: nrCols=$nrCols"); } } # Initialize ACLs for new row. 'newacl=' parameter is used to initialize new rows. # 'owner' in the ACL gets replaced by the current user. # Mode can be 'edit', 'view' or 'save'. sub initACLsForNewRow { my ($cells, $mode)=@_; # For all modes, we will use same logic for now. # TODO: Logic to be finalized later. for $col (0..$nrCols-1) { if ( ! defined($cells->[$col]) ) { my $acl=$newaclHash[$col]; if ( defined($acl) ) { $acl =~ s/\bowner\b/$user/g; $cells->[$col] = $acl; } elsif ($col == ($rowaclActive-1)) { $cells->[$col] = $user; } } } } sub handleTableRow { my ( $thePre, $theRow, $theTableNr, $theRowMax, $theRowNr, $doEdit, $doSave ) = @_; my $newRow=($theRow eq ""); $theRow =~ s/^\s*\|//o; my @cells=undef; my $rowaclAction='rw'; # Full access in case the acl logic is not used; but we use some acl logic later. # $nrCols is global. # Can be empty for new row. &initCells($theRow, \@cells); @outCells=undef; # TODO: outCells should be null initially. Check this. # Process Header Row in Special way. No Row ACLs. Col ACLs used only to remove the column altogether. if( ( $theRowNr <= 1 ) && ( $header ) ) { # Note: Sometimes, Header is made editable. If so, wrong interpretations can happen w.r.t. ACLs. if ($doSave) { handleTableRowSaveMode(\@cells, \@outCells, $nrCols, $theRowNr, my $isADataRow=0); my $rowFinal=generateFinalRow(\@outCells, $thePre, my $applyRowACLs=0, undef); return $rowFinal; } else { handleTableHeaderRow(\@cells, \@outCells, $thePre, $header, $format); # Headers to be printed always. my $rowFinal=generateFinalRow(\@outCells, $thePre, my $applyRowACLs=1, $rowlayout); return $rowFinal; } } else { # Data Rows; No Total rows at present. @outCells=undef; ############# Save Processing # First let us process doSave, and then return to view. if ($doSave) { # We have received GET variables of all the rows and columns. # And we build the table that would be saved to disk as is. # IMPORTANT NOTE: This table is not used for viewing. # If it is new row, then initialize it with appropriate ACLs. # TODO: This will require a string to be specified as default in acl specification. # And this is mainly required for edit. handleTableRowSaveMode(\@cells, \@outCells, $nrCols, $theRowNr, my $isADataRow=1); my $rowFinal=""; for $i (0..$nrCols-1) { $rowFinal.= $outCells[$i] . " |"; } return "$thePre|$rowFinal"; } ############# View processing: Edit Mode or View Mode. if( ($rowaclActive) ) { if( $newRow ) { initACLsForNewRow(\@cells, 'edit'); } $rowaclAction=getAclActionForRow(\@rowaclHash, \@cells, $user, $rowaclActive-1); } else { $rowaclAction='rw'; } # Action related filters. The cell contents get changed according to the mode and logic. if ($rowaclAction eq 'rn' ) { # Nothing to do. Retain outCells as undefined. } elsif ($rowaclAction eq 'rw') { if ( $doEdit ) { handleTableRowEditMode(\@cells, \@outCells, $nrCols, $theRowNr, $newRow); # Others: rb = view + blanked out, ro=view, rn=remove this row from view. } else { @outCells=@cells; } } elsif ($rowaclAction eq 'rb') { @outCells= (); $outCells[$rowaclCol-1] =$cells[$rowaclCol-1]; } else { # 'ro'. @outCells=@cells; } # By default: Show view. } # LATER: else if Total Rows, Meta Table Information Table, ... ... # Final Processing if ($rowaclAction eq 'rn') { return (undef); } my $rowFinal=generateFinalRow(\@outCells, $thePre, my $applyColACLs=1, $rowlayout); return $rowFinal; } sub generateFinalRow() { my ( $outCells, $thePre, $doApplyColACLs, $rowlayout ) = @_; if ( $rowlayout ne "") { my $rowFinal=applyRowLayout($outCells, $rowlayout, $doApplyColACLs); return "$thePre$rowFinal"; } my $rowFinal=""; for $i (0..$nrCols-1) { my $action; if ( $doApplyColACLs && $colaclActive ) { $action=getAclActionForCol(\@colaclHash, $i, $user); } else { $action='rw'; } if ( ($action eq 'rw') || ($action eq 'ro')) { $rowFinal.= $outCells->[$i] . " |"; } elsif ( $action eq 'rb' ) { $rowFinal.= '~'. " |"; } else { # $action eq 'rn' and undefined cases. # Skip altogether. } } return "$thePre|$rowFinal"; } # Display header row. If the cells are empty, but header="on", initialize it from format string. sub handleTableHeaderRow() { my ($cells, $outCells, $header, $format)=@_; my @hCells = split( /\|/, $header ); foreach $col (0 .. $nrCols-1) { my $cell= $cells->[$col]; unless( $cell ) { if( $header =~ /^on$/i ) { if( ( @format >= ($col+1) ) && ( $format[$col] =~ /(.*?)\,/ ) ) { $cell = $1; } $cell = "text" unless $cell; $cell = "*$cell*"; } else { $cell = $hCells[$col] if( @hCells >= ($col+1) ); $cell = "*text* " unless $cell; } } $outCells[$col]=$cell; } return $outCells; } # Handle table row in edit mode. NOTE: ACLs are already applied. sub handleTableRowEditMode() { my ($cells, $outcells, $nrCols, $theRowNr, $newRow) = @_; my $val = ""; my $cell = ""; my $cellDefined = 0; # If it is new row, the ACL cells should have been already initialized. foreach $col (0..$nrCols-1) { # Establish the value of each cell: Use the ones coming from GET/POST. $cellDefined = 0; $val = $query->param( "etcell${theRowNr}x$col" ); if ( defined($val) ) { $val =~ s/[\n\r]/ /gos; # Netscape on Unix can have new lines in an edit field $cellDefined = 1; $cell = $val; } elsif( $col <= @{$cells} ) { $cell = $cells->[$col]; $cellDefined = 1 if( length( $cell ) > 0 ); $cell =~ s/^\s//o; $cell =~ s/\s$//o; } else { $cell = ""; } my $action=&getAclActionForCol(\@colaclHash, $col, $user); if ( $action eq 'ro' ) { $outcells->[$col]=$cell; next; } elsif ($action eq 'rb') { $outcells->[$col]=$cell; next; # Will be removed at render time } elsif ($action eq 'rn') { $outcells->[$col]=$cell; next; # Will be removed at render time } # action=rw. # Produce edit boxes as applcable if( ( ! $cellDefined ) && ( @format >= ($col+1) ) && ( $format[$col] =~ /^\s*(.*?)\,\s*(.*?)\,\s*(.*?)\s*$/ ) ) { # default value of "| text, 20, a, b,c |" cell is "a, b, c" # default value of "| select, 1, a, b, c |" cell is "a" $val = $1; # type $cell = $3 || ""; $cell =~ s/\,.*$//o if( $val eq "select" ); } $outcells->[$col]=&inputElement( $theTableNr, $theRowNr, $col, "etcell${theRowNr}x$col", $cell ); } return $outcells; } # Note: Row ACLs are already applied. sub handleTableRowSaveMode { my ($cells, $outCells, $nrCols, $theRowNr, $isADataRow) = @_; my $val = ""; my $cell = ""; my $cellDefined = 0; my $populateFromInputs=0; if( ($rowaclActive) ) { # And the ACL column is empty. If we are saving a new row, we should initialize the row. # Otherwise, default ACLs apply. # $rowaclAction=getAclActionForRow(\@rowaclHash, \@cells, $user, $rowaclActive-1); if ( ! $rowaclAction eq 'rw') { if ( $cells[$rowaclActive-1] eq "" ) { $cells[$rowaclActive-1] = $user; # If default is "ro" $rowaclAction='rw'; } } } else { $rowaclAction='rw'; } if( $rowaclAction eq 'rw') { $populateFromInputs=1; } # Processing Special Rows such as headers and footers. For now, let us not allow them to be # saved from this interface. (1) Use topic edit and save, or (2) Use table metadata editor. if (! isADataRow ) { $populateFromInputs=0; } for $col (0..$nrCols-1) { # Establish the value of each cell: Use the ones coming from GET/POST. # This code is redundant since we don't want to process ACLs here. # # # If there are no permissions, retain old value and go to next column. # if ($rowaclDefined && ($rowaclAction neq 'rw') ) { # $outcells->[$col-1]=$cells[$col-1]; # next; # } $cellDefined = 0; $val = $query->param( "etcell${theRowNr}x$col" ); if ($populateFromInputs && (getAclActionForCol(\@colaclHash, $col, $user) eq 'rw') && defined($val) ) { $val =~ s/[\n\r]/ /gos; # Netscape on Unix can have new lines in an edit field $cellDefined = 1; $cell = $val; } elsif( $col <= @{$cells} ) { # nrCols captures no. of columns wanted by user. Not the ones in table. # So, you could delete extra columns by this mechanism $cell = $cells->[$col]; $cellDefined = 1 if( length( $cell ) > 0 ); $cell =~ s/^\s//o; $cell =~ s/\s$//o; } else { $cell = ""; } $outCells->[$col]=$cell; } return $outcells; } # Generate totals row, given the totals layout. sub handleTotalsRow { } # ========================= sub doSaveTable { my ( $theWeb, $theTopic, $theTableNr ) = @_; &TWiki::Func::writeDebug( "- EditTablePlugin::doSaveTable( $theWeb, $theTopic, $theTableNr )" ) if $debug; my( $meta, $text ) = &TWiki::Store::readTopic( $theWeb, $theTopic ); my $cgiRows = $query->param( 'etrows' ) || 1; my $tableNr = 0; my $rowNr = 0; my $insideTable = 0; my $doSave = 0; my $result = ""; foreach( split( /\n/, $text ) ) { if( /%EDITTABLE2{(.*)}%/o ) { $tableNr += 1; if( $tableNr == $theTableNr ) { $doSave = 1; } } if( $doSave ) { if( /^(\s*)\|.*\|\s*$/ ) { $insideTable = 1; $rowNr++; if( $rowNr > $cgiRows ) { # deleted row $rowNr--; next; } s/^(\s*)\|(.*)/&handleTableRow( $1, $2, $tableNr, $cgiRows, $rowNr, 1, 1 )/eo; } elsif( $insideTable ) { $insideTable = 0; if( $rowNr < $cgiRows ) { while( $rowNr < $cgiRows ) { $rowNr++; $result .= &handleTableRow( $preSp, "", $tableNr, $cgiRows, $rowNr, 1, 1 ) . "\n"; } } $doSave = 0; $rowNr = 0; } if( /^\s*$/ ) { # empty line if( $doSave ) { # empty %EDITTABLE%, so create a default table $rowNr = 0; if( $header ) { $rowNr++; $result .= &handleTableRow( $preSp, "", $tableNr, $cgiRows, $rowNr,1 , 1 ) . "\n"; } while( $rowNr < $cgiRows ) { $rowNr++; $result .= &handleTableRow( $preSp, "", $tableNr, $cgiRows, $rowNr, 1, 1 ) . "\n"; } } $doSave = 0; $rowNr = 0; } } $result .= "$_\n"; } my $error = &TWiki::Store::saveTopic( $theWeb, $theTopic, $result, $meta ); &TWiki::Store::lockTopic( $theTopic, "on" ); my $url = &TWiki::Func::getViewUrl( $theWeb, $theTopic ); if( $error ) { $url = &TWiki::Func::getOopsUrl( $theWeb, $theTopic, "oopssaveerr", $error ); } &TWiki::Func::redirectCgiQuery( $query, $url ); } # ========================= sub doCancelEdit { my ( $theWeb, $theTopic ) = @_; &TWiki::Func::writeDebug( "- EditTablePlugin::doCancelEdit( $theWeb, $theTopic )" ) if $debug; &TWiki::Store::lockTopic( $theTopic, "on" ); &TWiki::Func::redirectCgiQuery( $query, &TWiki::Func::getViewUrl( $theWeb, $theTopic ) ); } # ========================= sub doEnableEdit { my ( $theWeb, $theTopic, $doCheckIfLocked ) = @_; &TWiki::Func::writeDebug( "- EditTablePlugin::doEnableEdit( $theWeb, $theTopic )" ) if $debug; my $accessreqd='table'; my $tmp=<<'HERE'; Use cases for EDIT - User access: (a) Allow user to see and modify his row. Only one row for user. (b) Group owns the row. Any user in group can modify these rows. Each group has one row. (c) One user/group owns multiple rows (e.g. all projects of this person). (d) Only valid set of users have control; but they have full control. (e) Free table: any row/col can be modified by any user with effective acls. Use cases for VIEW - Guest access: (a1) - only user, and table admin can see information. (a2) - A specified user group can see the all the information. (a3) - All authenticated users can see the information. (a3) - Even guest users can see the information. | *use case* | *Table Modify with ACLs* | *Topic View access* | *Topic Edit Access* | | | user / Guestview | User / guest | user / guest | | (a) | Works, Works | Works, Works!(*1) | Doesn't work | | (b) | Works, Works | Works | Doesn't work | | (c) | same as (b) | (d) | - | required | required | EDIT Rendering: rowaclactive & guest & acl allows 'all=rw': prompt user id. rowaclactive & guest & acl allows 'guest=rw': Don't prompt user id. rowaclactive & user & acl allows 'user=rw' for atleast one row: VIEW Rendering: rowaclactive & guest & acl allows 'all=rn': prompt user id. rowaclactive & guest & acl allows 'all=ro': prompt user id. HERE my $wikiUserName = &TWiki::Func::getWikiUserName(); my $result=""; # if rowaclDefined then only make sure user is defined. # else standard logic, with tableaccess defined and used. my $userDefined = ($rowaclDefined && ($user ne 'guest')); my $tableaccess=&TWiki::Func::checkAccessPermission( "tableaccess", $wikiUserName, "", $theTopic, $theWeb ); if ( !$userDefined ) { if( (!&TWiki::Func::checkAccessPermission( "tableaccess", $wikiUserName, "", $theTopic, $theWeb)) && (!&TWiki::Func::checkAccessPermission( "change", $wikiUserName, "", $theTopic, $theWeb))) { # user has not permission to change the topic my $url = &TWiki::Func::getOopsUrl( $theWeb, $theTopic, "oopsaccesschange" ); &TWiki::Func::redirectCgiQuery( $query, $url ); return 0; } } my( $lockUser, $lockTime ) = &TWiki::Store::topicIsLockedBy( $theWeb, $theTopic ); if( ( $doCheckIfLocked ) && ( $lockUser ) ) { # warn user that other person is editing this topic $lockUser = &TWiki::Func::userToWikiName( $lockUser ); use integer; $lockTime = ( $lockTime / 60 ) + 1; # convert to minutes my $editLock = $TWiki::editLockTime / 60; my $url = &TWiki::Func::getOopsUrl( $theWeb, $theTopic, "oopslocked", $lockUser, $editLock, $lockTime ); &TWiki::Func::redirectCgiQuery( $query, $url ); return 0; } &TWiki::Store::lockTopic( $theTopic ); return 1; } #== Given data source, pull the specified table and return a table structure. # Input: datasource with format as given below, # Input: headers that we are interested in. If headers don't match, return that column as null elements. # output: table, array of rows. Each row is an array of columns. # # format of datasource: [Webname.]TopicName[:tablenum] # tablenum can be a number (0, 1, 2 etc.), or an Id - a string of alphanumeric characters. # sub getTableFromSource { my ($dataSource, $header) = @_; my $aWeb, $aTopic, $tableNum; $aTopic=undef; $aWeb=undef; $tableNum="1"; # Tables start with 1. Not 0. # Note on Design: We require plug-in mechanism to load data from different sources; in particular # CSV files, web pages. The format of dataSource should be a URL. # This design assume that such loading happens in a different topic, # using different plugins. The only dependency is that the data should be made available attached # to EDITTABLE tag, in standard syntax i.e. the data needs to be cached. A standard Cache Plugin # is necessary for this purpose. # chop off the first word. if ($dataSource =~ s/^(\w+)(.*)$/$2/ ) { $aTopic=$1; } # If something remains, it should be topic. Again chop it off, and readjust. if( $dataSource =~ s/^\.(\w+)(.*)/$2/ ) { $aWeb=$aTopic; $aTopic=$1; } if( $dataSource =~ s/^\:(\w+)$// ) { $tableNum=$1; } if ( ! ($dataSource eq "") ) { # Bad format for data source. # Return nothing; debug("Edittable: Bad datasource specified: web=$aWeb, topic=$aTopic table=$tableNum remaining=$dataSource") if $debug; return undef; } # expand common vars used as part of Datasource definition, unless oneself to prevent recursion if( "$aWeb.$aTopic" ne "$web.$topic" ) { $args = &TWiki::Func::expandCommonVariables( $1, $iTopic, $theWeb ); } debug("Edittable: datasource is OK: web=$aWeb, topic=$aTopic table=$tableNum remaining string=$dataSource"); # Collects all the tables in the topic. my $allTables = TWiki::Plugins::Table->new( undef, $aWeb, $aTopic ); my @selectedTable = $allTables->getTable($tableNum); # Create a hash, mapping the each of the column header to its index, after extracting ID out of it. my $i=0; my %columnsHash = map( { &idFromHeader($_) => $i++} @{$selectedTable[0]} ); if ( (! defined($header)) || ($header eq "all")) { $header = "|". join( "|", @{$selectedTable[0]} ) . "|"; } # Note: Incoming Header is in format of a|b|c. Already valid. # Note: We removed opening | # Note: We go through all the columns that we are interested in, and within each column, go through each row. my @newTable=(); my $numCol=-1; # Note: We go first by columns and then by rows. foreach( split(/\|/, $header) ) { $numCol++; for $numRow (0 .. $#selectedTable) { my $cell= " "; # Required; otherwise columns become "||", which means merged columns. if( $numRow == 0 ) { $cell = $_; } else { my $hdr=idFromHeader($_); # debug ("hdr=".$hdr); my $origCol= $columnsHash{$hdr}; if (defined($origCol) ) { $cell = $selectedTable[$numRow][$origCol]; } } if ( ! defined($newTable[$numRow]) ) { @{$newTable[$numRow]} = (); } $newTable[$numRow][$numCol] = $cell; } } # print array: foreach (@newTable) { foreach (@{$_}) { debug("Element = $_"); } } return @newTable; } # Given a string, remove decorations and return the column ID. # Note: This is definition of column ID. # Each column header in necessarily in special format: An alphanumeric # name, and can have \s, '-' between them.. (Display name can be different.) # First string that matches the reg expression will be treated as ID. sub idFromHeader() { my ($hdr, $rest)=@_; # Find which column matches this header. # First identify the ID of this column. $hdr =~ s/^\s*(.*)$/$1/; # Remove leading space $hdr =~ s/^(.*)\s*$/$1/; # Remove Trailing space # ID for header: Longest string with word chars at either end, and include space and '-' in between. if ( $hdr =~ m/(\w[\w\s\-]*\w)/ ) { # Identify the first longest string that matches our header definition. $hdr=$1; } return $hdr; } # ========================= # Given header, get column index. sub columnFromHeader { my ($h)=@_; if ( ! defined(%headerIndexHash) ) { my @headerCells=split(/\s*\|\s*/, $header); my $i=0; %headerIndexHash = map( { &idFromHeader($_) => $i++} @headerCells); } return $headerIndexHash{&idFromHeader($h)}; } #### Access Control related routines #======== Determine access given the ACL Rules, ACL String and current User. # # aclRules: Hash mapping user to rights. Generic users such as 'owner', 'all' also present. # 'acl': A list of users. 'vinod', or 'all' or 'vinod, ProjectLeadsGroup' and so on. NOTE: 'owner' # field in aclRules defines default access for these users/groups. # Return value: ro, rw, rb, rn; # 'rn' for row means don't show the row/cell. 'rb' - show row/cell as blank. 'rw' - read/write, # 'ro' is read-only. # sub checkACL() { my ($aclRules, $acl, $who) = @_; # Order of processing: 1. Defaults set in rowacl or colacl string in table specification. 2. Match with one of the identities in identified column (in which case, returns the rights specified as 'match'.) 3. Standard defaults. if( defined($aclRules->{$who}) ) { return $aclRules->{$who}; } foreach ( split(/\s*,\s*/, $acl) ) { s/^Main\.//; s/^\s*//; s/\s*$//; # The acl can also be user:ro format. s/(\w+)(:(\w+))*//; # debug("checkACL Inputs: who=$who, perms=$aclSpecified."); my $match=$1; if ( ($1 eq $who) || ( /Group$/ && &TWiki::Access::userIsInGroup($who, $1)) ) { my $rights=( $aclRules->{'owner'} ); # can be undefined as well. return $rights if (! $3); # Process additional rights. 'vinod, guest:ro' will mean that the owner is giving # only read rights to guest. my $extraRights=$3; # owner's rights, set at table level, can be 'rw', 'ro', 'rb', 'rn'. # Extra rights apply if top level rights are at least equal to owner's rights. my %rightsToInt= ('rw'=>4, 'ro' =>3, 'rb'=>2, 'rn'=>1); # no match = 0. my @intToRights= ('rw','rn','rb','ro','rw'); # no match = 0. my $iOwner=$tmphash{$rights}; my $iExtra=$tmphash{$extraRights}; $iExtra=4 if ($iExtra==0); # No match == same as owner's; automatically highest. if ($iExtra < $iOwner) { return $intToRights($iExtra); } else { return $rights; } } } # debug("Returning default checkACL:". $aclRules->{'all'}); return $aclRules->{'all'}; } # Determine access for the current user for the given row, given that @rowaclHash is populated. # For now, only first column where rowacl is defined is considered. # TODO: Extend it so that all columns where rowacl is defined, are considered. sub getAclActionForRow() { my ($aclHash, $cells, $whoAmI) = @_; return 'rw' if ( !$rowaclActive) ; my $aclCol=$rowaclActive-1; my $aclCell=$cells->[$rowaclActive-1]; # Note: Arg is actual column. $aclCell =~ s/\s*(.*)\s*/\1/; # Remove trailing and leading spaces my $action=&checkACL($aclHash->[$col], $aclCell, $whoAmI); # TODO: Check all the columns return "rw" if ( ! defined($action) ); return $action; } #======getAclActionForCol() sub getAclActionForCol() { my ($colAclHash, $col, $whoAmI) = @_; return 'rw' if ( ! $colaclActive) ; $whoAmI='guest' if( ! defined($whoAmI) ); return ($colAclHash->[$col]->{$whoAmI} ); } #============================= sub debug() { &TWiki::Func::writeDebug(@_) # if $debug; } #======================================================================= #Layout Logic # # Given the complete table and totals layout, render it. # Returns the rendered layout. sub totalsrowLayoutFilter { my ($table, $totalsLayout)=@_; if ( !defined($sumsHash) ) { $sumsHash={}; $occuranceHash={}; # Collect the names of fields for which totals are required. $sums->{"id"}=0; while ( $totalsformat =~ /\$sum{\s*\${(\w+)}\s*}/g ) { $sumsHash->{$1}=0; } while ( $totalsformat =~ /\$counts{\s*\${(\w+)}\s*}/g ) { $occuranceHash->{$1}={}; } foreach (@{$table}) { my @row=@{$_}; $sums->{"id"}++; if ( $totalsformat ) { foreach $column (keys %{$sums}) { $sumsHash->{$column} += $row[$column]; } } if ( $occurance ) { foreach $column (keys %{$occurance}) { my $item = $row[$column]; # Increase its frequency by one. $occuranceHash->{$column}->{$item}++; } } } } # Produce the totals string if (1) { # Collect the names of fields for which totals are required. $s=$totalsLayout; while ( $s =~ s/\$sum{\s*\${(\w+)}\s*}/$sums->{$1}/g ) {} # Print occurances; for now simple, fixed format. while ( $s =~ s/\$counts{\s*\${(\w+)}\s*}/&genCounts($1, $occurance)/geo ) {} } return $s; } # Given a row, render it as per the specified layout. # Note: We put upper limit replacements to about 500. Mainly to make sure there are no # inadvertent infinite loops. sub applyRowLayout { my ($cells, $rowlayout)=@_; my $layout=$rowlayout; # Recognize ${id}, ${abc}, ${2}. # Replace them with corresponding elements from the cells. # Note: Each pair of {} is replaced. # TODO: Fix How we identify Header IDs. Ideally this mechanism should be same everywhere # it is used. for $i (0..500) { if ( ! ($layout =~ s/\$\{([^\}]*)\}/&lookupInRow($cells, $1)/e ) ) { break; } } return $layout; } # Replace variable 's' with appropriate meaning. sub lookupInRow { my ($cells, $col)=@_; # col can be either string or digit. If it is string, it is header. Convert it. $col= &columnFromHeader($col) if ( !($col =~ m/^\d+$/)); return "" if ( !defined($col) || ! ($col =~ m/\d+/) ); if ( $colaclActive ) { $action=&getAclActionForCol(\@colaclHash, $col, $user); } else { $action='rw'; } if ( ($action eq 'rw') || ($action eq 'ro')) { return $cells->[$col]; } elsif ( $action eq 'rb' ) { return ""; } else { # $action eq 'rn' and undefined cases. # Skip altogether. return ""; } } sub olderOne { my ( $attributes, $votes ) = @_; my $aexequosep = &TWiki::extractNameValuePair( $attributes, "aexequosep" ) || ' ' ; my $itemformat = &TWiki::extractNameValuePair( $attributes, "itemformat" ) || '$item' ; my $lineformat = &TWiki::extractNameValuePair( $attributes, "lineformat" ) || '| $items | $count |' ; my $limit = scalar &TWiki::extractNameValuePair( $attributes, "limit" ) || "5" ; my $heading = &TWiki::extractNameValuePair( $attributes, "heading" ) || "| *Best $limit results* | *Votes* |" ; my $aWeb = &TWiki::extractNameValuePair( $attributes, "web" ) || $web; my $aTopic = &TWiki::extractNameValuePair( $attributes, "topic" ) || $topic; # VinodKulkarni: Added rows and totals support. my $tableHeading = &TWiki::extractNameValuePair( $attributes, "tableheading" ) || undef; my $rowformat = &TWiki::extractNameValuePair( $attributes, "rowformat" ) || undef; my $totalsformat = &TWiki::extractNameValuePair( $attributes, "totalsformat" ) || undef; # Basic Work: Read and define array of results. my $votes=populateVotesTable($aWeb, $aTopic); # Function 1: VinodKulkarni: Create and show a table in format requested. if ($rowformat || $totalsformat ) { my $str = $tableHeading; if ($str) { $str .= "\n" }; my $user; # Prepare totals line: "... $counts($3) ...# # Supports functions: $counts($column_name), $totals($column_name), ... # For $counts, $show_only(3, $counts($column_name)) ... while ( ($id, $items_arr) = each %{$votes}) { } if($totalsformat) { } #return "\n$str\n"; return $str; } # End of table functionality # Function 2: VinodKulkarni: Given ID, define all field values if( populateFieldsForId ) { # idInput is provided by user. while ( (($id, $items_arr) = each %{$voted}) && !($id eq $idInput)) {} # items_arr is now defined for this ID. Nothing else to do. return ""; } # now count votes my $total = 0; my %counts = (); while ( ($u, $i) = each %{$voted}) { foreach $j ( @{$i} ) { $counts{$j}++; $total++; } } # collects all ex-aequo together my %exequo = (); while ( ($i, $j) = each %counts) { my $f = $itemformat || '$item'; $f =~ s/\$item/$i/g; if ( ! defined( $exequo{$j} ) ) { my @a = (); $exequo{$j} = \@a; } push @{$exequo{$j}}, ($f); } # and finally produce table $i = 0; my $str = "$heading\n"; foreach $j ( reverse sort keys %exequo ) { if ( $i < $limit ) { $i++; #my $items = join($aexequosep, @{$exequo{$j}}); my @items = @{$exequo{$j}}; #my $lf = "$lineformat"; my $perc = $j*100/$total; for my $item (@items) { my $lf = "$lineformat"; #$lf =~ s/\$items/$items/g; $lf =~ s/\$items/$item/g; $lf =~ s/\$count/$j/g; $lf =~ s/\$perc/$perc/g; $str .= "$lf\n"; } } } return $str; } sub genCounts { my $column=shift; my $hash2d=shift; my $str; my $hash=$hash2d->{$column}; foreach $instance (keys %{$hash}) { $str .= "$instance:" . $hash->{$instance} . "
"; } return $str; } #=================================================================================== # The following code is copied from the ChartPlugin Table object. package TWiki::Plugins::Table; sub new { my ($class, $topicContents, $aWeb, $aTopic) = @_; my $this = {}; bless $this, $class; if ( ! defined($topicContents) && defined($aWeb) && defined($aTopic) ) { my( $meta, $text ) = &TWiki::Func::readTopic( $aWeb, $aTopic ); $topicContents = $text; } $this->_parseOutTables($topicContents); return $this; } # The guts of this routine was initially copied from SpreadSheetPlugin.pm # and were used in the ChartPlugin Table object which this was copied from, # but this has been modified to support the functionality needed by the # EditTablePlugin. One major change is to only count and save tables # following an %EDITTABLE{.*}% tag. # # This routine basically returns an array of hashes where each hash # contains the information for a single table. Thus the first hash in the # array represents the first table found on the topic page, the second hash # in the array represents the second table found on the topic page, etc. sub _parseOutTables { my ($this, $topic) = @_; my $tableNum = 1; # Table number (only count tables with EDITTABLE tag) my @tableMatrix; # Currently parsed table. my $inEditTable = 0; # Flag to keep track if in an EDITTABLE table my $result = ""; my $insidePRE = 0; my $insideTABLE = 0; my $line = ""; my @row = (); $topic =~ s/\r//go; $topic =~ s/\\\n//go; # Join lines ending in "\" $topic .= "\n"; # This is to ensure that we execute 'outside table' part when last list is table row. foreach( split( /\n/, $topic ) ) { # change state: m|
|i       && ( $insidePRE = 1 );
        m||i  && ( $insidePRE = 1 );
        m|
|i && ( $insidePRE = 0 ); m||i && ( $insidePRE = 0 ); if( ! $insidePRE ) { $inEditTable = 1 if (/%EDITTABLE2{(.*)}%/); if ($inEditTable) { if( /^\s*\|.*\|\s*$/ ) { # inside | table | $insideTABLE = 1; $line = $_; $line =~ s/^(\s*\|)(.*)\|\s*$/$2/o; # Remove starting '|' @row = split( /\|/o, $line, -1 ); _trim(\@row); # &TWiki::Func::writeDebug("Adding row. @row"); push (@tableMatrix, [ @row ]); } else { # outside | table | # &TWiki::Func::writeDebug("Outside table row, InsideTable= $insideTable "); if( $insideTABLE ) { # We were inside a table and are now outside of it so # save the table info into the Table object. $insideTABLE = 0; $inEditTable = 0; if (@tableMatrix != 0) { # Save the table via its table number $$this{"TABLE_$tableNum"} = [@tableMatrix]; # &TWiki::Func::writeDebug("Adding table no. $tableNum"); $tableNum++; } undef @tableMatrix; # reset table matrix } } } } $result .= "$_\n"; } $$this{NUM_TABLES} = $tableNum; } # Trim any leading and trailing white space and/or '*'. sub _trim { my ($totrim) = @_; for my $element (@$totrim) { $element =~ s/^[\s\*]+//; # Strip of leading white/* $element =~ s/[\s\*]+$//; # Strip of trailing white/* } } # Return the contents of the specified cell sub getCell { my ( $this, $tableNum, $row, $column ) = @_; my @selectedTable = $this->getTable( $tableNum ); my $value = $selectedTable[$row][$column]; return $value; } sub getTable { my ($this, $tableNumber) = @_; my $table = $$this{"TABLE_$tableNumber"}; return @$table if defined( $table ); return (); } sub getTableAsString { my ($this, $tableNumber) = @_; my $table = $$this{"TABLE_$tableNumber"}; my $str=""; foreach (@$table) { $str .= "|"; foreach (@{$_}) { $str .= $_ . "|" ; # $this->getCell("1", $row, $col). "|"; } $str .= "\n"; } return $str; } 1;