#!/usr/bin/php
<?PHP
#
#   FILE:  cwis (CWIS command line utility)
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2016-2017 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

$GLOBALS["CCLUVersion"] = "1.0.6";
$GLOBALS["CCLURequiredCwisVersion"] = "3.9.2";

/**
* Print usage information.
*/
function PrintHelp()
{
    global $CCLUVersion;
    print <<<EOT
CWIS Command Line Utility $CCLUVersion
Usage: cwis command [arguments]
Commands:
  clearcache    clear page/object/template cache
  config        get/set system configuration setting value
  defaultui     get/set default user interface
  dumpdb        dump database
  list          list metadata schemas/fields
  loadresources load resource records from file
  mysql         start MySQL command line client
  mysqlargs     print args for MySQL commands
  pdisable      disable plugin
  penable       enable plugin
  tdel          remove task from queue
  tlist         list queued/running/orphaned tasks
  trun          run task
  uadd          add user account
  upriv         add/remove/list user privileges
  userui        set user interface for specified user
Enter a command with no arguments for help on that specific command.

EOT;
}

# check to make sure we are running from the command line
if (php_sapi_name() != "cli")
{
    print "Must be running from the command line.\n";
    exit(1);
}

# check to make sure we are running within a CWIS installation
$BootstrapFile = "include/StartUp.php";
if (!is_readable($BootstrapFile))
{
    if (is_dir("html") && is_readable("html"))
    {
        $BootstrapFile = "html/".$BootstrapFile;
    }

    if (!is_readable($BootstrapFile))
    {
        print "Must be run from the base directory of a working CWIS installation.\n";
        exit(1);
    }
}

# print help message if no arguments supplied
if (count($argv) < 2)
{
    PrintHelp();
    exit(0);
}

# set up operating environment
require_once($BootstrapFile);

# check to make sure we have at least the minimum required CWIS version
if (!version_compare(CWIS_VERSION, $GLOBALS["CCLURequiredCwisVersion"], ">="))
{
    print "CWIS version ".$GLOBALS["CCLURequiredCwisVersion"]
            ." or later is required.\n";
    exit(1);
}

# grab command and command arguments
$Command = strtolower($argv[1]);
$Args = array_slice($argv, 2);
$ArgLetter = (count($Args) && strlen($Args[0])) ? strtoupper($Args[0][0]) : NULL;

switch ($Command)
{
    # CLEAR ALL CACHES
    case "clearcache":
    case "clearcaches":
        if (strpos("ATOP", $ArgLetter) !== FALSE)
        {
            switch ($ArgLetter)
            {
                case "A":
                    $GLOBALS["AF"]->ClearTemplateLocationCache();
                    print "Template location cache cleared.\n";
                    $GLOBALS["AF"]->ClearObjectLocationCache();
                    print "Object location cache cleared.\n";
                    $GLOBALS["AF"]->ClearPageCache();
                    print "Page cache cleared.";
                    break;

                case "T":
                    $GLOBALS["AF"]->ClearTemplateLocationCache();
                    print "Template location cache cleared.";
                    break;

                case "O":
                    $GLOBALS["AF"]->ClearObjectLocationCache();
                    print "Object location cache cleared.";
                    break;

                case "P":
                    $GLOBALS["AF"]->ClearPageCache();
                    print "Page cache cleared.";
                    break;
            }
        }
        else
        {
            print "Usage: cwis clearcache (all|template|object|page)";
        }
        break;

    # GET/SET SYSTEM CONFIGURATION VALUE
    case "cfg":
    case "config":
        $Cols = $DB->GetColumns("SystemConfiguration");
        sort($Cols);
        if (count($Args))
        {
            $DB = new Database();
            $MatchingCols = array();
            array_walk($Cols, function ($Value, $Index) use ($Args, &$MatchingCols)
            {
                if (strpos(strtolower($Value), strtolower($Args[0])) === 0)
                {
                    $MatchingCols[] = $Value;
                }
            });
            if (count($MatchingCols) == 0)
            {
                print "Usage: cwis config (SettingName) [Value]";
            }
            elseif (count($MatchingCols) > 1)
            {
                print "Multiple matching configuration settings found:";
                foreach ($MatchingCols as $Col)
                {
                    print "\n    ".$Col;
                }
            }
            else
            {
                $CfgCol = $MatchingCols[0];
                if (isset($Args[1]))
                {
                    $NewValue = $Args[1];
                    print "Setting ".$CfgCol." to:";
                    print "\n    ".$NewValue;
                    $GLOBALS["G_SysConfig"]->Value($CfgCol, $NewValue);
                }
                else
                {
                    $CurrentValue = $GLOBALS["G_SysConfig"]->Value($CfgCol);
                    print "Current value of ".$CfgCol.":";
                    print "\n    ".$CurrentValue;
                }
            }
        }
        else
        {
            print "Usage: cwis config (SettingName) [Value]";
            print "\nAvailable system configuration settings:";
            foreach ($Cols as $Col)
            {
                print "\n    ".$Col;
            }
        }
        break;

    # SET DEFAULT INTERFACE
    case "defaultui":
        $NewInterface = isset($Args[0]) ? $Args[0] : NULL;
        $InterfaceList = $GLOBALS["AF"]->GetUserInterfaces();
        if ($NewInterface === NULL)
        {
            $CurrentInterface = $GLOBALS["G_SysConfig"]->DefaultActiveUI();
            $AvailableInterfaces = array_keys($GLOBALS["AF"]->GetUserInterfacePaths());
            natcasesort($AvailableInterfaces);
            print "Available interfaces:\n";
            foreach ($AvailableInterfaces as $Interface)
            {
                print "    ".$Interface."\n";
            }
            print "Current default interface is \"".$CurrentInterface."\".";
        }
        elseif (isset($InterfaceList[$NewInterface]))
        {
            $GLOBALS["G_SysConfig"]->DefaultActiveUI($NewInterface);
            print "Default interface set to \"".$NewInterface."\".";
        }
        else
        {
            if (strlen($NewInterface))
            {
                print "Unknown interface \"".$NewInterface."\".";
            }
            print "Usage: cwis defaultui CanonicalInterfaceName";
        }
        break;

    # DUMP DATABASE CONTENTS IN SQL
    case "dumpdb":
        if (count($Args))
        {
            $FileName = $Args[0];
            if (is_writable($FileName) && is_dir($FileName))
            {
                $PortalName = $GLOBALS["G_SysConfig"]->PortalName();
                $PortalName = preg_replace('/[^A-Za-z0-9 ]/', '', $PortalName);
                $PortalName = preg_replace('/\s+/', '_', $PortalName);
                if (strpos(getcwd(), "/home/") === 0)
                {
                    $UserInfo = posix_getpwuid(posix_geteuid());
                    $UserName = $UserInfo["name"];
                    $PortalName .= "_".$UserName;
                }
                $FileName .= "/".$PortalName."--DB_Backup--".date("ymd_Hi").".sql";
            }
        }
        if (isset($FileName) && is_writable(dirname($FileName)))
        {
            print "Dumping database to ".$FileName."\n";
            $DBInfo = $GLOBALS["G_Config"]["Database"];
            $Cmd = "mysqldump '--user=".$DBInfo["UserName"]."'"
                    ." '--password=".$DBInfo["Password"]."'"
                    ." ".$DBInfo["DatabaseName"]
                    ." > ".$FileName;
            exec($Cmd);
        }
        else
        {
            print "Usage: cwis dumpdb (Directory|File.sql)";
        }
        break;

    # LIST SCHEMAS/FIELDS
    case "list":
        if (strpos("SF", $ArgLetter) !== FALSE)
        {
            switch ($ArgLetter)
            {
                case "S":
                    $Schemas = MetadataSchema::GetAllSchemas();
                    usort($Schemas, function($A, $B)
                    {
                        return ($A->Id() < $B->Id()) ? -1 : 1;
                    });
                    printf("%2s %-30s\n", "ID", "SCHEMA NAME");
                    foreach ($Schemas as $Schema)
                    {
                        printf("%2d %-30s\n", $Schema->Id(), $Schema->Name());
                    }
                    break;

                case "F":
                    $Schemas = MetadataSchema::GetAllSchemas();
                    usort($Schemas, function($A, $B)
                    {
                        return ($A->Id() < $B->Id()) ? -1 : 1;
                    });
                    foreach ($Schemas as $Schema)
                    {
                        print "SCHEMA: ".$Schema->Name()."\n";
                        $Fields = $Schema->GetFields();
                        usort($Fields, function($A, $B)
                        {
                            return ($A->TypeAsName() == $B->TypeAsName())
                                    ? (($A->Name() < $B->Name()) ? -1 : 1)
                                    : (($A->TypeAsName() < $B->TypeAsName()) ? -1 : 1);
                        });
                        $RowFormat = "%-24.24s %-8.8s %3s %3s\n";
                        printf($RowFormat, "NAME", "TYPE", "ID", "ENA");
                        foreach ($Fields as $Field)
                        {
                            printf($RowFormat, $Field->Name(),
                                    $Field->TypeAsName(), $Field->Id(),
                                    ($Field->Enabled() ? "Yes" : "No"));
                        }
                        print "\n";
                    }
                    break;
            }
        }
        else
        {
            print "Usage: cwis list (schemas/fields)";
        }
        break;

    # LOAD RESOURCE RECORDS FROM FILE
    case "loadresource":
    case "loadresources":
        if (count($Args))
        {
            $SchemaId = isset($Args[1]) ? GetSchemaIdForName($Args[1])
                    : MetadataSchema::SCHEMAID_DEFAULT;

            if ($SchemaId === FALSE)
            {
                print "No metadata schema was found with a name of \"".$Args[1]."\".\n";
            }
            else
            {
                $FileName = $Args[0];
                if (!is_readable($FileName))
                {
                    print "No readable file was found with the name \"".$FileName."\".\n";
                }
                else
                {
                    $RFactory = new ResourceFactory($SchemaId);
                    try
                    {
                        $NewResourceIds =
                                $RFactory->ImportResourcesFromXmlFile($FileName);
                        $SiteOwnerId = GetSiteOwner();
                        if ($SiteOwnerId !== NULL)
                        {
                            $Schema = new MetadataSchema($SchemaId);
                            $OwnerFields = [
                                    "Added By Id",
                                    "Last Modified By Id",
                                    ];
                            foreach ($OwnerFields as $FieldName)
                            {
                                foreach ($NewResourceIds as $ResourceId)
                                {
                                    if ($Schema->FieldExists($FieldName))
                                    {
                                        $Resource = new Resource($ResourceId);
                                        $Resource->Set($FieldName, $SiteOwnerId);
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception $Ex)
                    {
                        $Location = basename($Ex->getFile()).":".$Ex->getLine();
                        print "Error encountered at ".$Location." during import:\n    "
                                .$Ex->getMessage()."\n";
                    }
                }
            }
        }

        if (isset($NewResourceIds))
        {
            print count($NewResourceIds)." resources loaded.";
        }
        else
        {
            print "Usage: cwis loadresources (FileName.xml) [SchemaName]";
        }
        break;

    # START MYSQL COMMAND LINE CLIENT
    case "mysql":
        $DBInfo = $GLOBALS["G_Config"]["Database"];
        $Cmd = "mysql --user='".$DBInfo["UserName"]."'"
                ." --password='".$DBInfo["Password"]."'"
                ." ".$DBInfo["DatabaseName"];
        $Proc = proc_open($Cmd, [STDIN, STDOUT, STDERR], $Pipes);
        proc_close($Proc);
        break;

    # PRINT MYSQL COMMAND ARGS
    case "mysqlargs":
        $DBInfo = $GLOBALS["G_Config"]["Database"];
        $Args = "--user='".$DBInfo["UserName"]."'"
                ." --password='".$DBInfo["Password"]."'"
                ." ".$DBInfo["DatabaseName"];
        print $Args."\n";
        break;

    # PLUGIN ENABLE/DISABLE
    case "penable":
    case "pdisable":
        if ($ArgLetter)
        {
            $PluginName = $Args[0];
            $Plugin = $GLOBALS["G_PluginManager"]->GetPlugin($PluginName, TRUE);
        }
        else
        {
            $Plugin = NULL;
        }
        if ($Plugin === NULL)
        {
            if (isset($PluginName))
            {
                print "No plugin found with the name \"".$PluginName."\".\n";
            }
            print "Usage: cwis ".$Command." PluginName\n";
            exit(1);
        }
        $Plugin->IsEnabled(($Command == "penable") ? TRUE : FALSE);
        print "Plugin \"".$PluginName."\" has been "
                .(($Command == "penable") ? "enabled" : "disabled")
                .".\n";
        break;

    # DELETE TASK
    case "tdel":
        if (count($Args) && is_numeric($Args[0]))
        {
            $TaskId = $Args[0];
            $TasksRemoved = $GLOBALS["AF"]->DeleteTask($TaskId);
            if ($TasksRemoved)
            {
                print "Task with ID ".$TaskId." removed.";
            }
            else
            {
                print "No task found with ID ".$TaskId.".";
            }
        }
        else
        {
            print "Usage: cwis tdel TaskID";
        }
        break;

    # LIST TASKS CURRENTLY IN QUEUE
    case "tlist":
        if (strpos("QRO", $ArgLetter) !== FALSE)
        {
            switch ($ArgLetter)
            {
                case "Q":
                    $Tasks = $GLOBALS["AF"]->GetQueuedTaskList();
                    $QueueSize = $GLOBALS["AF"]->GetTaskQueueSize();
                    print "Tasks in Queue: ".$QueueSize."\n";
                    break;

                case "R":
                    $Tasks = $GLOBALS["AF"]->GetRunningTaskList();
                    $QueueSize = count($Tasks);
                    print "Currently Running Tasks: ".$QueueSize."\n";
                    break;

                case "O":
                    $Tasks = $GLOBALS["AF"]->GetOrphanedTaskList();
                    $QueueSize = count($Tasks);
                    print "Orphaned Tasks: ".$QueueSize."\n";
                    break;
            }

            if ($QueueSize)
            {
                printf("%-9s %-40s\n", "ID", "SYNOPSIS");
                foreach ($Tasks as $TaskId => $TaskInfo)
                {
                    $TaskSynopsis = htmlspecialchars_decode(
                            ApplicationFramework::GetTaskCallbackSynopsis($TaskInfo));
                    printf("%-9d %-40s\n", $TaskId, $TaskSynopsis);
                }
            }
        }
        else
        {
            print "Usage: cwis tlist (queued|running|orphaned)";
        }
        break;

    # RUN TASK
    case "trun":
        if (count($Args) && is_numeric($Args[0]))
        {
            $TaskId = $Args[0];
            $Task = $GLOBALS["AF"]->GetTask($TaskId);
            if ($Task)
            {
                # attempt to load task callback if not already available
                $GLOBALS["AF"]->LoadFunction($Task["Callback"]);

                # if callback appears callable
                if (is_callable($Task["Callback"]))
                {
                    # run task
                    if ($Task["Parameters"])
                    {
                        call_user_func_array($Task["Callback"], $Task["Parameters"]);
                    }
                    else
                    {
                        call_user_func($Task["Callback"]);
                    }

                    # remove task from queue
                    $GLOBALS["AF"]->DeleteTask($TaskId);
                }

                print "Task with ID ".$TaskId." has been run.";
            }
            else
            {
                print "No task found with ID ".$TaskId.".";
            }
        }
        else
        {
            print "Usage: cwis trun TaskID";
        }
        break;

    # ADD USER
    case "uadd":
        if ((count($Args) < 2) || !filter_var($Args[0], FILTER_VALIDATE_EMAIL))
        {
            print "Usage: cwis uadd Email Password [UserName]";
        }
        else
        {
            $Email = $Args[0];
            $Password = $Args[1];

            if (isset($Args[2]))
            {
                $UserName = $Args[2];
            }
            else
            {
                $UserName = CWUserFactory::GenerateUniqueUsernameFromEmail($Email);
            }

            $UFactory = new CWUserFactory();
            $NewUser = $UFactory->CreateNewUser(
                    $UserName, $Password, $Password, $Email, $Email);

            if (is_object($NewUser))
            {
                $NewUser->IsActivated(TRUE);
                print "User account '".$UserName."' added.";
            }
            else
            {
                foreach ($NewUser as $ErrorCode)
                {
                    print User::GetStatusMessageForCode($ErrorCode)."\n";
                }
                print "Usage: cwis uadd Email Password [UserName]";
            }
        }
        break;

    # ADD/REMOVE USER PRIVILEGE
    case "upriv":
        if ((strpos("ARL", $ArgLetter) !== FALSE)
                && ((count($Args) >= 3)
                        || ((count($Args) == 2) && ($ArgLetter == "L"))))
        {
            $UserName = $Args[1];
            $UFactory = new UserFactory();
            if ($UFactory->UserNameExists($UserName))
            {
                $User = new CWUser($UserName);
            }
            else
            {
                print "Unknown user name \"".$UserName."\".\n";
                exit(1);
            }

            if (($ArgLetter == "A") || ($ArgLetter == "R"))
            {
                $PrivilegeName = strtoupper($Args[2]);
                $PFactory = new PrivilegeFactory();
                if ((strpos($PrivilegeName, "PRIV_") === 0) && defined($PrivilegeName))
                {
                    $Privilege = constant($PrivilegeName);
                }
                elseif (defined("PRIV_".$PrivilegeName))
                {
                    $Privilege = constant("PRIV_".$PrivilegeName);
                    $PrivilegeName = "PRIV_".$PrivilegeName;
                }
                elseif (is_numeric($PrivilegeName)
                        && $PFactory->ItemExists($PrivilegeName))
                {
                    $Privilege = $PrivilegeName;
                    $PrivObj = new Privilege($Privilege);
                    $PrivilegeName = $PrivObj->Name();
                }
                else
                {
                    print "Privilege \"".$PrivilegeName."\" is unknown.\n";
                    exit(1);
                }
            }

            switch ($ArgLetter)
            {
                case "A":
                    $User->GrantPriv($Privilege);
                    print "Privilege ".$PrivilegeName
                            ." given to user ".$UserName.".\n";
                    break;

                case "R":
                    $User->RevokePriv($Privilege);
                    print "Privilege ".$PrivilegeName
                            ." removed from user ".$UserName.".\n";
                    break;

                case "L":
                    print "Current privileges for user ".$UserName.":\n";
                    $CurrPrivs = $User->GetPrivList();
                    # (get all constants that begin with "PRIV_")
                    $PrivConstants = array_filter(get_defined_constants(),
                            function ($Key) {
                                    return is_string($Key)
                                            && (strpos($Key, "PRIV_") === 0);
                                    },
                            ARRAY_FILTER_USE_KEY);
                    $PrivNamesDefault = array();
                    $PrivNamesCustom = array();
                    foreach ($CurrPrivs as $Priv)
                    {
                        $PrivName = array_search($Priv, $PrivConstants);
                        if ($PrivName !== FALSE)
                        {
                            $PrivNamesDefault[$Priv] = $PrivName;
                        }
                        else
                        {
                            $PrivObj = new Privilege($Priv);
                            $PrivNamesCustom[$Priv] = $PrivObj->Name();
                        }
                    }
                    asort($PrivNamesDefault);
                    asort($PrivNamesCustom);
                    $CurrPrivNames = $PrivNamesDefault + $PrivNamesCustom;
                    foreach ($CurrPrivNames as $Priv => $PrivName)
                    {
                        if (strpos($PrivName, "PRIV_") === 0)
                        {
                            print "    ".$PrivName."\n";
                        }
                        else
                        {
                            print "    ".$PrivName." (".$Priv.")\n";
                        }
                    }
                    break;
            }
        }
        else
        {
            print "Usage: cwis upriv (add|remove|list) username [privilege]\n";
            print "Examples:\n";
            print "    cwis upriv add someuser classadmin\n";
            print "    cwis upriv r someuser priv_sysadmin\n";
            print "    cwis upriv a someuser 102\n";
        }
        break;

    # SET INTERFACE FOR USER
    case "userui":
        if (count($Args) >= 2)
        {
            $UserName = $Args[0];
            $UFactory = new UserFactory();
            $AllUsers = (strtoupper($UserName) == "ALL") ? TRUE : FALSE;
            $User = $UFactory->UserNameExists($UserName)
                    ? new CWUser($UserName) : NULL;

            $NewInterface = isset($Args[1]) ? $Args[1] : NULL;
            $InterfaceList = $GLOBALS["AF"]->GetUserInterfaces();
        }
        if ((count($Args) < 2)
                || (($User === NULL) && !$AllUsers)
                || !isset($InterfaceList[$NewInterface]))
        {
            if (isset($UserName) && ($User === NULL) && !$AllUsers)
            {
                print "Unknown user name \"".$UserName."\".\n";
            }
            if (isset($NewInterface) && !isset($InterfaceList[$NewInterface]))
            {
                print "Unknown interface \"".$NewInterface."\".\n";
            }
            print "Usage: cwis userui (username|all) CanonicalInterfaceName\n";
        }
        else
        {
            if ($AllUsers)
            {
                $UserIds = $UFactory->GetUserIds();
                foreach ($UserIds as $UserId)
                {
                    $User = new CWUser($UserId);
                    $User->Set("ActiveUI", $NewInterface);
                }
                print "Active user interface for all "
                        .number_format(count($UserIds))
                        ." users set to ".$NewInterface.".";
            }
            else
            {
                $User->Set("ActiveUI", $NewInterface);
                print "Active user interface for ".$UserName
                        ." set to ".$NewInterface.".";
            }
        }
        break;

    default:
        print "Unknown command \"".$Command."\"\n";
        PrintHelp();
        exit(1);
}

/**
* Get best guess at user account for site administrator/owner.  If running
* from the command line, this is assumed to be whoever is running the script,
* if a matching user account can be found for that person.
* @return int ID for user account of likely site owner or NULL if could not
*       determine likely owner.
*/
function GetSiteOwner()
{
    # assume no owner will be found
    $OwnerId = NULL;

    # if running from command line
    $UFactory = new CWUserFactory();
    if (PHP_SAPI === 'cli')
    {
        # get name of system user currently executing script
        $SysUserInfo = posix_getpwuid(posix_geteuid());
        $SysUserName = $SysUserInfo["name"];

        # look for account with same name as system user
        $UserNames = $UFactory->FindUserNames($SysUserName);
        if (count($UserNames) == 1)
        {
            $UserIds = array_keys($UserNames);
            $OwnerId = array_pop($UserIds);
        }
    }

    # if no owner found
    if ($OwnerId === NULL)
    {
        # retrieve administrative email address for site
        $AdminEmail = $GLOBALS["G_SysConfig"]->AdminEmail();

        # look for user with same email address
        $UserNames = $UFactory->GetMatchingUsers($AdminEmail, "EMail");

        # if one matching address is found assume that account is owner
        if (count($UserNames) == 1)
        {
            $UserIds = array_keys($UserNames);
            $OwnerId = array_pop($UserIds);
        }
    }

    # if no owner found
    if ($OwnerId === NULL)
    {
        # look for users with system admin privileges
        $PrivUserNames = $UFactory->GetUsersWithPrivileges(PRIV_SYSADMIN);
        $PrivUserIds = array_keys($PrivUserNames);

        # if privileged user found assume user most recently logged in is owner
        if (count($PrivUserIds) > 0)
        {
            usort($PrivUserIds, function ($A, $B) {
                    $UserA = new User($A);
                    $UserB = new User($B);
                    return ($UserA->Get("LastLoginDate")
                            < $UserB->Get("LastLoginDate")) ? -1 : 1;
            });
            $OwnerId = array_pop($PrivUserIds);
        }
    }

    # report owner (if any) to caller
    return $OwnerId;
}


/**
* Retrieve schema ID for specified name, searching first for an exact match
* and then for a case-insensitive match with all non-alphanumeric characters
* stripped out.
* @param string $Name Schema name.
* @return int Schema ID or FALSE if no (or multiple) matching schema(s) found.
*/
function GetSchemaIdForName($Name)
{
    $SchemaNames = MetadataSchema::GetAllSchemaNames();
    $SchemaId = array_search($Name, $SchemaNames);

    if ($SchemaId === FALSE)
    {
        $NormalizeSchemaName = function (&$Value) {
            $Value = strtolower(preg_replace("%[^a-z]%i", "", $Value));
        };

        array_walk($SchemaNames, $NormalizeSchemaName);
        $NormalizeSchemaName($Name);

        $SchemaIds = array_keys($SchemaNames, $Name);
        if (count($SchemaIds) == 1)
        {
            $SchemaId = array_pop($SchemaIds);
        }
    }

    return $SchemaId;
}
