/* SOGoToolManageACL.m - this file is part of SOGo
 *
 * Copyright (C) 2017-2020 Inverse inc.
 *
 * This file 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, or (at your option)
 * any later version.
 *
 * This file 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSString.h>

#import <GDLAccess/EOAdaptorChannel.h>

#import <GDLContentStore/EOQualifier+GCS.h>
#import <GDLContentStore/GCSChannelManager.h>
#import <GDLContentStore/GCSFolderManager.h>
#import <GDLContentStore/GCSFolder.h>
#import <GDLContentStore/NSURL+GCS.h>

#import <SOGo/SOGoCache.h>
#import <SOGo/SOGoSource.h>
#import <SOGo/SOGoUserManager.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoGCSFolder.h>
#import <SOGo/SOGoParentFolder.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserFolder.h>
#import <SOGo/SOGoSystemDefaults.h>

#import <NGCards/iCalCalendar.h>
#import <NGCards/NGVCard.h>

#import <NGObjWeb/WOContext+SoObjects.h>

#import "SOGoTool.h"

typedef enum
{
  ManageACLUnknown = -1,
  ManageACLGet = 0,
  ManageACLAdd = 1,
  ManageACLRemove = 2,
  ManageACLSubscribe = 3,
  ManageACLUnsubscribe = 4,
} SOGoManageACLCommand;

@interface SOGoToolManageACL : SOGoTool
{
  SOGoManageACLCommand command;
  NSString *owner;
  NSString *folder;
  NSString *user;
  NSArray *rights;
}

@end

@implementation SOGoToolManageACL

+ (NSString *) command
{
  return @"manage-acl";
}

+ (NSString *) description
{
  return @"manage user's ACL";
}

- (id) init
{
  if ((self = [super init]))
    {
      command = ManageACLUnknown;
      owner = nil;
      folder = nil;
      user = nil;
      rights = nil;
    }

  return self;
}

- (void) dealloc
{
  [owner release];
  [folder release];
  [user release];
  [rights release];
  [super dealloc];
}

- (void) usage
{
  fprintf (stderr, "manage-acl get|add|remove|subscribe|unsubscribe owner folder user|group <rights>\n\n"
	   "           get          get ACL information of folder for user\n"
	   "           add          add ACL information of folder for user\n"
	   "           remove       remove all ACL information of folder for user\n"
	   "           subscribe    subscribe user to owner's folder\n"
	   "           unsubscribe  unsubscribe user to owner's folder\n"
     "           owner        the user owning the folder\n"
	   "           folder       the folder - Calendar/<ID> or Contacts/<ID>\n"
	   "           user         the user (or group without the @ prefix) to get/set rights for - 'ALL', '<default>', 'anonymous' are supported\n"
     "           rights       rights to add\n\n"
     "Example:   sogo-tool manage-acl get jdoe Calendar/personal ALL\n\n"
     "Note:      You can add only one access right at the time. To set them all at once,\n"
     "           invoke 'remove' first to remove them all.\n\n");
}

- (BOOL) parseArguments
{
  NSString *s;
  BOOL rc;
  int max;

  max = [arguments count];
  if (max > 3 && max < 6)
    {
      s = [arguments objectAtIndex: 0];
      if ([s isEqualToString: @"get"])
	command = ManageACLGet;
      else if ([s isEqualToString: @"add"])
	{
	  command = ManageACLAdd;
	  rights = RETAIN([[arguments objectAtIndex: 4] objectFromJSONString]);
	}
      else if ([s isEqualToString: @"remove"])
	command = ManageACLRemove;
      else if ([s isEqualToString: @"subscribe"])
	command = ManageACLSubscribe;
      else if ([s isEqualToString: @"unsubscribe"])
	command = ManageACLUnsubscribe;
      else
	{
	  [self usage];
	  return NO;
	}

      owner = RETAIN([arguments objectAtIndex: 1]);
      folder = RETAIN([arguments objectAtIndex: 2]);
      user = RETAIN([arguments objectAtIndex: 3]);

      rc = YES;
    }
  else
    {
      [self usage];
      rc = NO;
    }

  return rc;
}

- (NSArray *) _fetchUserIDs
{
  NSMutableArray *allUsers, *allSQLUsers;
  NSAutoreleasePool *pool;
  SOGoUserManager *lm;
  NSDictionary *infos;
  NSString *u;

  int count, max;

  lm = [SOGoUserManager sharedUserManager];
  allSQLUsers = [[NSMutableArray alloc] init];
  allUsers = [[NSMutableArray alloc] init];

  if ([user isEqualToString: @"ALL"])
    {
      GCSChannelManager *cm;
      NSURL *folderLocation;
      GCSFolderManager *fm;
      EOAdaptorChannel *fc;
      NSArray *attrs;
      NSString *sql;

      fm = [GCSFolderManager defaultFolderManager];
      cm = [fm channelManager];
      folderLocation = [fm folderInfoLocation];
      fc = [cm acquireOpenChannelForURL: folderLocation];
      if (fc)
        {
          allSQLUsers = [NSMutableArray new];
          sql = [NSString stringWithFormat: @"SELECT DISTINCT c_path2 FROM %@",
                          [folderLocation gcsTableName]];
          [fc evaluateExpressionX: sql];
          attrs = [fc describeResults: NO];
          while ((infos = [fc fetchAttributes: attrs withZone: NULL]))
            {
              u = [infos objectForKey: @"c_path2"];
              if (u)
                [allSQLUsers addObject: u];
            }
          [cm releaseChannel: fc  immediately: YES];
        }

      // We add our system users
      [allSQLUsers addObject: @"<default>"];

      if ([[SOGoSystemDefaults sharedSystemDefaults] enablePublicAccess])
	[allSQLUsers addObject: @"anonymous"];
    }
  else
    [allSQLUsers addObject: user];

  pool = [[NSAutoreleasePool alloc] init];
  max = [allSQLUsers count];

  for (count = 0; count < max; count++)
    {
      if (count > 0 && count%100 == 0)
        {
          DESTROY(pool);
          pool = [[NSAutoreleasePool alloc] init];
        }

      u = [allSQLUsers objectAtIndex: count];

      // We skip lookup for our 'system users' but keep them to set ACLs
      if ([u isEqualToString: @"anonymous"] || [u isEqualToString: @"<default>"])
	{
	  [allUsers addObject: u];
	  continue;
	}

      // Skip the owner
      if ([u isEqualToString: owner])
	continue;

      infos = [lm contactInfosForUserWithUIDorEmail: u];
      if (infos)
        {
          if (infos && [[infos objectForKey: @"isGroup"] boolValue])
            [allUsers addObject: [NSString stringWithFormat: @"@%@", [infos objectForKey: @"c_uid"]]];
          else
            [allUsers addObject: [infos objectForKey: @"c_uid"]];
        }
      else
        {
          // We haven't found the user based on the GCS table name
          // Let's try to strip the domain part and search again.
          // This can happen when using SOGoEnableDomainBasedUID (YES)
          // but login in SOGo using a UID without domain (DomainLessLogin gets set)
          NSRange r;

          r = [u rangeOfString: @"@"];

          if (r.location != NSNotFound)
            {
              u = [u substringToIndex: r.location];
              infos = [lm contactInfosForUserWithUIDorEmail: u];
              if (infos)
                [allUsers addObject: [infos objectForKey: @"c_uid"]];
              else
                NSLog (@"user '%@' unknown", u);
            }
          else
            NSLog (@"user '%@' unknown", u);
        }
    }

  DESTROY(pool);
  RELEASE(allSQLUsers);

  return AUTORELEASE(allUsers);
}


- (void) addACLForUser: (NSString *) theUser
                folder: (GCSFolder *) theFolder
{
  NSString *currentRole, *SQL, *path, *u;
  EOAdaptorChannel *channel;
  NSArray *allUsers;
  int i, j;

  channel = [theFolder acquireAclChannel];
  path = [NSString stringWithFormat: @"%@/%@", owner, folder];
  allUsers = [self _fetchUserIDs];

  for (i = 0; i < [allUsers count]; i++)
    {
      u = [allUsers objectAtIndex: i];
      NSLog(@"Settings rights for user %@", u);
      for (j = 0; j < [rights count]; j++)
	{
	  currentRole = [rights objectAtIndex: j];
	  if ([GCSFolderManager singleStoreMode])
	    SQL = [NSString stringWithFormat: @"INSERT INTO %@"
			    @" (c_object, c_uid, c_role, c_folder_id)"
			    @" VALUES ('/%@', '%@', '%@', %@)",
			    [theFolder aclTableName],
			    path, u, currentRole, [theFolder folderId]];
	  else
	    SQL = [NSString stringWithFormat: @"INSERT INTO %@"
			    @" (c_object, c_uid, c_role)"
			    @" VALUES ('/%@', '%@', '%@')",
			    [theFolder aclTableName],
			    path, u, currentRole];
	  [channel evaluateExpressionX: SQL];
	}
    }
}

- (void) getACLForUser: (NSString *) theUser
		folder: (GCSFolder *) theFolder
{
  NSArray *allRights, *allKeys;
  NSMutableDictionary *d;
  EOQualifier *qualifier;
  NSDictionary *right;
  NSString *qs;
  id o;

  int i;

  if ([theUser isEqualToString: @"ALL"])
    qualifier = nil;
  else
    {
      qs = [NSString stringWithFormat: @"c_uid = '%@'", theUser];
      qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
    }

  allRights = [theFolder fetchAclMatchingQualifier: qualifier];
  d = [NSMutableDictionary dictionary];

  for (i = 0; i < [allRights count]; i++)
    {
      right = [allRights objectAtIndex: i];
      if ((o = [d objectForKey: [right objectForKey: @"c_uid"]]))
	[o addObject: [right objectForKey: @"c_role"]];
      else
	[d setObject: [NSMutableArray arrayWithObject: [right objectForKey: @"c_role"]]
	      forKey: [right objectForKey: @"c_uid"]];	   
    }

  allKeys = [d allKeys];

  for (i = 0; i < [allKeys count]; i++)
    {
      o = [allKeys objectAtIndex: i];
      NSLog(@"Rights for %@ %@", o, [[d objectForKey: o] jsonRepresentation]);
    }
}

- (void) removeACLForUser: (NSString *) theUser
		   folder: (GCSFolder *) theFolder
{
  EOQualifier *qualifier;
  NSString *qs, *path;

  if ([theUser isEqualToString: @"ALL"])
    qs = [NSString stringWithFormat: @"c_uid LIKE '%%'"];
  else
    qs = [NSString stringWithFormat: @"c_uid = '%@'", theUser];

  qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
  
  [theFolder deleteAclMatchingQualifier: qualifier];

  // We clear the cache. We first strip /Users/ from our path
  path = [[[[theFolder path] pathComponents] subarrayWithRange: NSMakeRange(2,3)] componentsJoinedByString: @"/"];
  [[SOGoCache sharedCache] setACLs: nil
			   forPath: path];
}

- (void) subscribeOrUnsubscribeUser: (NSString *) theUser
			   toFolder: (GCSFolder *) theFolder
			   reallyDo: (BOOL) reallyDo
{
  SOGoParentFolder *parentFolder;
  SOGoUserFolder *userFolder;
  SOGoGCSFolder *gcsFolder;
  WOContext *localContext;
  NSArray *components;

  localContext = [WOContext context];
  [localContext setActiveUser: [SOGoUser userWithLogin: owner]];
  userFolder = [SOGoUserFolder objectWithName: owner inContainer: nil];
  components = [folder componentsSeparatedByString: @"/"];
  parentFolder = [userFolder lookupName: [components objectAtIndex: 0]
			      inContext: localContext
				acquire: NO];

  gcsFolder = [parentFolder lookupPersonalFolder: [components objectAtIndex: 1]
				  ignoringRights: YES];

  [gcsFolder subscribeUserOrGroup: user  reallyDo: reallyDo  response: nil];
}

- (BOOL) proceed
{
  NSAutoreleasePool *pool;
  GCSFolderManager *fm;
  GCSFolder *f;

  BOOL rc;

  rc = YES;

  pool = [NSAutoreleasePool new];

  fm = [GCSFolderManager defaultFolderManager];
  f = [fm folderAtPath: [NSString stringWithFormat: @"/Users/%@/%@", owner, folder]];

  // Skip the folder existence check so we can auto-create the personal folder
  // for users that have never logged-in
  if (!f && command != ManageACLSubscribe)
    {
      NSLog(@"No folder %@ found for user %@", folder, owner);
      rc = NO;
    }
  else
    {
      if (command == ManageACLGet)
	[self getACLForUser: user  folder: f];
      else if (command == ManageACLRemove)
	[self removeACLForUser: user  folder: f];
      else if (command == ManageACLAdd)
	[self addACLForUser: user  folder: f];
      else if (command == ManageACLSubscribe)
	[self subscribeOrUnsubscribeUser: user  toFolder: f  reallyDo: YES];
      else if (command == ManageACLUnsubscribe)
	[self subscribeOrUnsubscribeUser: user  toFolder: f  reallyDo: NO];
      else
	[self usage];
    }

  [pool release];

  return rc;
}

- (BOOL) run
{
  return ([self parseArguments] && [self proceed]);
}

@end
