<?PHP
#
#   FILE:  AutoScreenshot.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2011 Edward Almasy and Internet Scout
#   http://scout.wisc.edu
#

class AutoScreenshot extends Plugin{

    function Register()
    {
        $this->Name = "Auto Screenshot";
        $this->Version = "1.0.3";
        $this->Description = "Automatically obtain resources screenshots";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.2.2");

        $this->CfgSetup["MinutesBetweenChecks"] = array(
            "Type" => "Number",
            "Label" => "Screenshot Check Interval",
            "Units" => "minutes",
            "MaxVal" => 999999,
            "Help" => "The number of minutes between automatic screenshot generation.",
            );

        $this->CfgSetup["Method"] = array(
            "Type" => "Option",
            "Label" => "Screenshot Method",
            "Help" =>
            "Select what screenshot generating program you want to use.",
            "Options" =>
            array(
                "wkhtmltoimage" => "wkhtmltoimage",
                "firefox"       => "Firefox with Xvfb"
                )
            );
    }

    function Install()
    {
        $DB = new Database();

        $DB->Query("CREATE TABLE AutoScreenshot_Failures "
                   ."(ResourceId INT, Messages TEXT, FailureTime TIMESTAMP DEFAULT NOW())");
        $DB->Query("CREATE TABLE AutoScreenshot_ApprovalQueue (ResourceId INT, ImageId INT)");
        $DB->Query("CREATE TABLE AutoScreenshot_Blacklist (ResourceId INT);");

        $this->ConfigSetting("MinutesBetweenChecks", 5);
        $this->ConfigSetting("Method", "wkhtmltoimage");
    }

    function Uninstall()
    {
        $DB = new Database();

        # Go through the qpproval queue, cleaning up all the images
        #  which live there
        $DB->Query("SELECT * FROM AutoScreenshot_ApprovalQueue");
        $ApprovalQueue = $DB->FetchRows();

        foreach ($ApprovalQueue as $QueueEntry)
        {
            $ThisImage = new SPTImage($QueueEntry["ImageId"]);
            $ThisImage->Delete();
        }

        # Delete all our tables:
        foreach( array(
                     "AutoScreenshot_Failures" => "failed screenshot",
                     "AutoScreenshot_ApprovalQueue" => "screenshot approval queue",
                     "AutoScreenshot_Blacklist" => "screenshot blacklist",
                     ) as $Table => $Desc)
        {
            if (FALSE === $DB->Query("DROP TABLE ".$Table) )
            { return "Could not remove the ".$Desc." table"; }
        }
    }

    function Upgrade($PreviousVersion)
    {
        $DB = new Database();

        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            foreach( array("Failures", "ApprovalQueue", "Blacklist")
                     as $TableName )
            {
                $DB->Query("ALTER TABLE AutoScreenshot".$TableName
                           ." RENAME TO AutoScreenshot_".$TableName);
            }
        }
    }

    function HookEvents()
    {
        return array(
            "EVENT_COLLECTION_ADMINISTRATION_MENU" => "AddCollectionAdminMenuItems",
            "EVENT_PERIODIC" => "TakeScreenshots",
            );
    }

    function AddCollectionAdminMenuItems()
    {
        return array(
            "ApprovalQueue" => "Automatic Screenshots",
            );
    }

    /**
     * Run external command with timeout
     * @param string $Command The command to run
     * @param string $Stdin Data to feed to the command's stdin
     * @param int Timeout Timeout in seconds (OPTIONAL)
     * @return array with keys "Status", "Output", and "Error"
     *
     * Timeout defaults to 75% of the MaxExecutionTime.
     *
     * In the return value,
     *  Status gives the exit code when the process completed successfully,
     *  -1 for processes that were killed, or -2 if the process couldn't be run
     *  Output gives stdout from the process, and Error gives stderr from the process.
     */
    function ExecWithTimeout($Command, $Stdin, $Timeout=NULL)
    {
        if ($Timeout===NULL)
        {
            global $AF;
            $Timeout = 0.75 * $AF->MaxExecutionTime();
        }

        # Set up named indexes for readability:
        $StdinIndex = 0;
        $StdoutIndex = 1;
        $StderrIndex = 2;

        # Run the helper program:
        $DescriptorSpec = array(
            $StdinIndex  => array("pipe","r"),
            $StdoutIndex => array("pipe","w"),
            $StderrIndex => array("pipe","w") );
        $Process = proc_open($Command, $DescriptorSpec, $ProcPipes);

        if (is_resource($Process))
        {
            # If we were able to successfully start the process,
            # feed it data on stdin:
            fwrite( $ProcPipes[$StdinIndex], $Stdin );

            # Then start a timer and spinwait until either
            # the process completes or we've hit our timeout:
            $StartTime = time();
            do
            {
                sleep(1);
                $ProcStatus = proc_get_status( $Process );
            }
            while ( (time()-$StartTime) < $Timeout &&
                    $ProcStatus["running"] );

            if ($ProcStatus["running"])
            {
                # If the process didn't finish within our timeout,
                # Kill the misbehaving process
                proc_terminate($Process);
                # Set the stdout and stderr pipes to nonblocking
                # so that we can grab the output/error so far
                stream_set_blocking( $ProcPipes[$StdoutIndex], 0);
                stream_set_blocking( $ProcPipes[$StderrIndex], 0);

                $CmdStatus = -1;
            }
            else
            {
                # Otherwise, in the case of a happy exit, grab the exit code.
                $CmdStatus = $ProcStatus["exitcode"];
            }

            # Determine how much free memory we have to work with right now:
            $MemLimit = trim(ini_get('memory_limit'));
            switch (strtolower($MemLimit[strlen($MemLimit)-1]))
            {
            case 'g': $MemLimit *= 1024;
            case 'm': $MemLimit *= 1024;
            case 'k': $MemLimit *= 1024;
            }

            $FreeMemory = $MemLimit - memory_get_usage();

            # Only use 50% of it for command output, so that we don't
            # die from badly behaved super-verbose commands
            $CmdOut = stream_get_contents($ProcPipes[$StdoutIndex], 0.25 * $FreeMemory );
            $CmdErr = stream_get_contents($ProcPipes[$StderrIndex], 0.25 * $FreeMemory );

            # And, clean up:
            fclose($ProcPipes[$StdinIndex]);
            fclose($ProcPipes[$StdoutIndex]);
            fclose($ProcPipes[$StderrIndex]);
            proc_close($Process);
        }
        else
        {
            $CmdStatus = -2;
            $CmdOut = "";
            $CmdErr = "";
        }

        return array(
            "Status" => $CmdStatus,
            "Output" => $CmdOut,
            "Error"  => $CmdErr
            );
    }

    /**
     * Check to see if there any eligibile resources missing a screenshot,
     *  take screenshots if so.
     */
    function TakeScreenshots($LastRunAt)
    {
        $DB = new Database();

        $Schema = new MetadataSchema();

        $Field = $Schema->GetFieldByName("Screenshot");

        $DB->Query("SELECT ResourceId FROM Resources ".
                   "WHERE ResourceId > 0 AND ".
                   "(".$Field->DBFieldName()."=0 OR ".
                   "ISNULL(".$Field->DBFieldName()."))");

        if ($DB->NumRowsSelected() > 0 )
        {
            $Resources = array();
            while ($Row = $DB->FetchRow() )
            {
                $Resources []= $Row["ResourceId"];
            }

            $ResourcesToFilter = array();

            $DB->Query("SELECT ResourceId FROM AutoScreenshot_Failures");
            while ($Row = $DB->FetchRow())
            {
                $ResourcesToFilter []= $Row["ResourceId"];
            }

            $DB->Query("SELECT ResourceId FROM AutoScreenshot_ApprovalQueue");
            while ($Row = $DB->FetchRow())
            {
                $ResourcesToFilter []= $Row["ResourceId"];
            }

            $DB->Query("SELECT ResourceId FROM AutoScreenshot_Blacklist");
            while ($Row = $DB->FetchRow())
            {
                $ResourcesToFilter []= $Row["ResourceId"];
            }

            $FilteredResources = array_diff($Resources, $ResourcesToFilter);

            if (count($FilteredResources)>0)
            {
                $ResourceId = array_pop($FilteredResources);

                $TargetResource = new Resource($ResourceId);

                $Url = $TargetResource->Get("Url");

                $ScreenshotBinary = dirname(__FILE__)."/bin/screenshot-".
                    $this->ConfigSetting("Method").".sh";

                $TmpFileName = "tmp/".md5(mt_rand()).".jpg";

                $CmdString = $ScreenshotBinary.
                    " ".escapeshellarg($Url).
                    " ".escapeshellarg($TmpFileName);

                $CmdResult = $this->ExecWithTimeout($CmdString, "");

                if (file_exists($TmpFileName) &&
                    $CmdResult["Status"]==0 &&
                    filesize($TmpFileName) < 409600 )
                {
                    $NewImage = new SPTImage(
                        $TmpFileName,
                        $Field->MaxWidth(), $Field->MaxHeight(),
                        $Field->MaxPreviewWidth(), $Field->MaxPreviewHeight(),
                        $Field->MaxThumbnailWidth(), $Field->MaxThumbnailHeight());

                    $DB->Query("INSERT INTO AutoScreenshot_ApprovalQueue "
                               ."(ResourceId,ImageId) VALUES "
                               ."(".intval($TargetResource->Id()).",".
                               intval($NewImage->Id()).")");
                }
                else
                {
                    ob_start();
                    print("Command was: ".$CmdString."\n");
                    print("\nStdout:\n");
                    print($CmdResult["Output"]);
                    print("--\n\nStderr:\n");
                    print($CmdResult["Error"]);
                    print("--\n");
                    $ErrorMessages = ob_get_contents();
                    ob_end_clean();

                    $DB->Query("INSERT INTO AutoScreenshot_Failures "
                               ."(ResourceId,Messages) VALUES (".$TargetResource->Id().","
                               ."'".addslashes($ErrorMessages)."')");
                }

                if (file_exists($TmpFileName))
                {
                    unlink($TmpFileName);
                }
            }
            else
            {
                $DB->Query("DELETE FROM AutoScreenshot_Failures "
                           ."WHERE FailureTime < NOW() - INTERVAL 1 WEEK");
            }
        }

        return $this->ConfigSetting("MinutesBetweenChecks");
    }

    function CleanDuplicatesOfManual()
    {
        $DB = new Database();

        $Schema = new MetadataSchema();

        $Field = $Schema->GetFieldByName("Screenshot");

        $DB->Query("SELECT * FROM AutoScreenshot_ApprovalQueue");
        $ApprovalQueue = $DB->FetchRows();

        foreach ($ApprovalQueue as $QueueEntry)
        {
            $ResourceId = $QueueEntry["ResourceId"];
            $Resource = new Resource($ResourceId);

            $ImageFiles = $Resource->Get("Screenshot");
            if (count($ImageFiles)>0)
            {
                $ThisImage = new SPTImage($QueueEntry["ImageId"]);
                if (is_object($ThisImage) )
                {
                    $ThisImage->Delete();
                    $DB->Query("DELETE FROM AutoScreenshot_ApprovalQueue "
                               ."WHERE ResourceId=".intval($ResourceId));
                }
            }
        }
    }
}
?>