CORE-POS/IS4C

View on GitHub
fannie/cron/management/CronManagementPage.php

Summary

Maintainability
F
5 days
Test Coverage
D
68%
<?php
/*******************************************************************************

    Copyright 2013 Whole Foods Co-op

    This file is part of CORE-POS.

    CORE-POS 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.

    CORE-POS 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
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/
include(dirname(__FILE__) . '/../../config.php');
if (!class_exists('FannieAPI')) {
    include_once(__DIR__ . '/../../classlib2.0/FannieAPI.php');
}

/* --COMMENTS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    * 14Mar2013 Andy Theuninck class overhaul w/ select options
    * 17Oct2012 Eric Lee Add note about Help under Command links Help tooltip in Command.

*/

/* This is a very light wrapper around cron. The
   interface replicates a crontab file, so you do
   have to understand how one works

   This tool helps by:
   1. Listing available scripts
   2. Automatically changing to fannie's "cron" directory
   3. Directing logging to fannie's logs/fannie.log

   Hiding path management makes the list of tasks
   easier to look at, but also insures a script can
   find fannie's configuration without relying on
   hard-coded paths

   This *should* display but otherwise ignore any
   jobs that weren't configured through the web
   interface. The whole tool is meant to be optional
   hand-holding for new users.

*/

class CronManagementPage extends FanniePage 
{

    protected $header = "Manage Scheduled Tasks";
    protected $title = "Fannie : Scheduled Tasks";
    protected $must_authenticate = True;
    protected $auth_classes = array('admin');

    public $description = '[Scheduled Tasks] manages periodic background tasks.';
    public $themed = true;

    function preprocess()
    {
        global $FANNIE_OP_DB, $FANNIE_URL;
        if (FormLib::get_form_value('submit') == 'Save') {

            $indexes = FormLib::get_form_value('enabled');
            if (!is_array($indexes)) {
                // no jobs selected
                $indexes = array();
            }
            $min = FormLib::get_form_value('min',array());
            $hour = FormLib::get_form_value('hour',array());
            $day = FormLib::get_form_value('day',array());
            $month = FormLib::get_form_value('month',array());
            $wkdy = FormLib::get_form_value('wkdy',array());
            $cmd = FormLib::get_form_value('cmd',array());
            $email = FormLib::get_form_value('email','');

            if (is_array($indexes) && count($indexes) > 0) {
                $tmpfn = tempnam(sys_get_temp_dir(),"");
                $fp = fopen($tmpfn,"w");
                if (!empty($email)) {
                    fwrite($fp,"MAILTO=".$email."\n");
                }
                foreach ($indexes as $i) {
                    fprintf($fp,"%s %s %s %s %s %s\n",
                        $min[$i],
                        $hour[$i],
                        $day[$i],
                        $month[$i],
                        $wkdy[$i],
                        $cmd[$i]
                    );
                }
                fclose($fp);

                $output = system("crontab $tmpfn 2>&1");

                /* backup crontab just in case */
                $dbc = FannieDB::get($FANNIE_OP_DB);
                $trun = $dbc->prepare("TRUNCATE TABLE cronBackup");
                $dbc->execute($trun);
                $prep = $dbc->prepare('INSERT INTO cronBackup VALUES ('.$dbc->now().', ?)');
                $dbc->execute($prep,array(file_get_contents($tmpfn)));
                $crontab = file_get_contents($tmpfn);

                unlink($tmpfn);
                $this->add_onload_command("showBootstrapAlert('#alert-area', 'success', 'Enabled " . count($indexes) . " tasks');\n");
            } else {
                $this->add_onload_command("showBootstrapAlert('#alert-area', 'warning', 'Enabled zero tasks');\n");
            }
        }

        return true;
    }

    function body_content()
    {
        global $FANNIE_ROOT, $FANNIE_URL;
        $ret = '';

        $ret .= '<div id="alert-area"></div>';
        $ret .= '<p>';
        if (function_exists('posix_getpwuid')) {
            $chk = posix_getpwuid(posix_getuid());
            $ret .= "PHP is running as: ".$chk['name']."<br />";
            $ret .= "Fannie will attempt to use their crontab<br /><br />";
        } else {
            $ret .= "PHP is (probably) running as: ".get_current_user()."<br />";
            $ret .= "This is probably Windows; this tool won't work<br /><br />";
        }
        $ret .= '</p>';

        $ret .= '<p>';
        $fannie_path = realpath(__DIR__ . '/../../');
        if (!is_writable($fannie_path . '/logs/fannie.log')) {
            $ret .= "<i>Warning: fannie.log ({$fannie_path}/logs/fannie.log)
                is not writable. Logging task results may
                not work</i>";
         } else {
            $ret .= "Default logging will be to {$fannie_path}/logs/fannie.log";
         }
        $ret .= '</p>';

        $ret .= "<p>Click the 'Command' link for popup Help.</p>";

        $jobs = $this->scanScripts($FANNIE_ROOT.'cron',array());
        $tasks = FannieAPI::listModules('FannieTask');
        $tab = $this->readCrontab();

        $mode = FormLib::get_form_value('mode','simple');

        $ret .= "<form action=\"{$_SERVER['PHP_SELF']}\" method=\"post\" class=\"form form-inline\">";
        $ret .= sprintf ('<input type="hidden" name="mode" value="%s" />',$mode);
        if ($mode == 'simple') {
            $ret .= '<a href="CronManagementPage.php?mode=advanced">Switch to Advanced View</a><br />';
        } else {
            $ret .= '<a href="CronManagementPage.php?mode=simple">Switch to Simple View</a><br />';
        }
        $ret .= "<label>E-mail address</label><input name=\"email\" value=\"{$tab['email']}\" class=\"form-control\" />";

        $ret .= "<table class=\"table\">";
        $ret .= "<tr><th>Enabled</th><th>Min</th><th>Hour</th><th>Day</th><th>Month</th><th>Wkdy</th><th>Command/Help</th></tr>";
        $i = 0;
        foreach ($tasks as $task) {
            $obj = new $task();
            if (!$obj->schedulable) {
                continue;
            }
            $cmd = 'php '.realpath(dirname(__FILE__).'/../../classlib2.0/FannieTask.php').' '.$task.' >> '.$fannie_path.'/logs/fannie.log';;
            $simple = $this->simpleRow($task, $task, $cmd, $tab, $i);
            if ($simple !== false && $mode == 'simple') {
                $ret .= $simple;
            } else {
                $ret .= $this->advancedRow($task, $task, $cmd, $tab, $i);
            }
            $i++;
        }
        foreach ($jobs as $job) {
            $filename = basename($job);
            $classname = substr($filename, 0, strlen($filename)-4);
            if (in_array($classname, $tasks)) {
                // tasks must be listed separately
                continue; 
            }
            $shortname = substr($job,strlen($FANNIE_ROOT."cron/"));
            $nicename = rtrim($shortname,'php');
            $nicename = rtrim($nicename,'.');
            $nicename = str_replace('.',' ',$nicename);

            $cmd = "cd {$fannie_path}/cron && php ./{$shortname} >> {$fannie_path}/logs/fannie.log";

            $simple = $this->simpleRow($shortname,$nicename,$cmd,$tab,$i);
            if ($simple !== false && $mode == 'simple') {
                $simple = str_replace('taskOn', 'deprecatedJobOn', $simple);
                $simple = str_replace('taskOff', 'deprecatedJobOff', $simple);
                $ret .= $simple;
            } else {
                $row = str_replace('taskOn', 'deprecatedJobOn', $this->advancedRow($shortname,$nicename,$cmd,$tab,$i));
                $row = str_replace('taskOff', 'deprecatedJobOff', $row);
                $ret .= $row;
            }
            // defaults are set as once a year so someone doesn't accidentallly
            // start firing a job off every minute
            if (isset($tab['jobs'][$shortname])) {
                unset($tab['jobs'][$shortname]);
            }
            $i++;
        }

        /* list out any jobs that WERE NOT covered
           in the above loop. Those jobs weren't
           set up by this tool and should not be edited
        */
        foreach ($tab['jobs'] as $job) {
            $ret .= sprintf('<tr>
                <td><input type="checkbox" name="enabled[]" %s value="%d" /></td>
                <td><input type="text" size="2" name="min[]" value="%s" /></td>
                <td><input type="text" size="2" name="hour[]" value="%s" /></td>
                <td><input type="text" size="2" name="day[]" value="%s" /></td>
                <td><input type="text" size="2" name="month[]" value="%s" /></td>
                <td><input type="text" size="2" name="wkdy[]" value="%s" /></td>
                <td><input type="hidden" name="cmd[]" value="%s" />%s</td>
                </tr>',
                'checked',$i,
                $job['min'],
                $job['hour'],
                $job['day'],
                $job['month'],
                $job['wkdy'],
                $job['cmd'],
                $job['cmd']
            );
            $i++;
        }

        $ret .= "</table>";
        $ret .= '<p><button type="submit" name="submit" value="Save"
                    class="btn btn-default">Save</button></p>';
        $ret .= '</form>';

        $this->addScript($FANNIE_URL . 'src/javascript/fancybox/jquery.fancybox-1.3.4.js?v=1');
        $this->addCssFile($FANNIE_URL . 'src/javascript/fancybox/jquery.fancybox-1.3.4.css');
        $this->addOnloadCommand('$(\'.fancybox-link\').fancybox();');

        return $ret;
    }

    private function simpleRow($shortname,$nicename,$cmd,$tab,$i)
    {
        $t_index = 'jobs';
        // if shortname is a task class name, switch indexes
        // and populate defaults if the task is not enabled
        if (substr($shortname, -4) != '.php') {
            $t_index = 'tasks';
            $obj = new $shortname(); 
            if (!isset($tab['tasks'][$shortname])) {
                // cast-to-string necessary to correctly compare
                // actual strings like '*' and 0
                $tab['tasks'][$shortname] = array(
                    'min' => ''.$obj->default_schedule['min'],
                    'hour' => ''.$obj->default_schedule['hour'],
                    'day' => ''.$obj->default_schedule['day'],
                    'month' => ''.$obj->default_schedule['month'],
                    'wkdy' => ''.$obj->default_schedule['weekday'],
                );
            }
            if ($obj->name != 'Fannie Task') {
                $nicename = $obj->name;
            }
        }
        $enabled = false;
        // tasks will populate all fields except cmd with defaults
        // so that's the indicator whether a task/job is enabled
        if (isset($tab[$t_index][$shortname]) && isset($tab[$t_index][$shortname]['cmd'])) {
            $enabled = true;
        }
        $ret = '<tr class="' . ($enabled ? 'taskOn' : 'taskOff') . '">';
        $ret .= sprintf('<td><input type="checkbox" name="enabled[]" %s value="%d" /></td>',
            ($enabled ? 'checked' :''),$i);
        
        $ret .= sprintf('<td><input type="text" class="form-control" size="2" name="min[]" value="%s" /></td>',
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['min']:'0'));

        /**
          Match crontab's current setting against values
          in the <select> dropdown. If the setting does not
          match any, simpleRow has to return false. The data
          for this row can only be presented with the
          advanced interface. Same for other <select>s.
        */
        $vals = array('*'=>'*',0=>'12AM');
        for ($i=1;$i<24;$i++) {
            $vals[$i] = (($i>12)?($i-12):$i) . (($i>11)?'PM':'AM');
        }
        $ret .= '<td><select class="form-control" name="hour[]">';
        list($matched, $opts) = $this->getOpts($vals, $tab[$t_index], $shortname, 'hour', 0);
        $ret .= $opts;
        $ret .= '</select></td>';
        if (!$matched) {
            return false;
        }

        // same as hours
        $vals = array('*'=>'*');
        for ($i=1;$i<32;$i++) {
            $vals[$i] = $i;
        }
        $ret .= '<td><select class="form-control" name="day[]">';
        $matched = False;
        list($matched, $opts) = $this->getOpts($vals, $tab[$t_index], $shortname, 'day', 1);
        $ret .= $opts;
        $ret .= '</select></td>';
        if (!$matched) {
            return false;
        }

        // same as hours
        $vals = array('*'=>'*');
        for ($i=1;$i<13;$i++) {
            $vals[$i] = date('M',mktime(0,0,0,$i,1,2000));
        }
        $ret .= '<td><select class="form-control" name="month[]">';
        $matched = false;
        list($matched, $opts) = $this->getOpts($vals, $tab[$t_index], $shortname, 'month', 1);
        $ret .= $opts;
        $ret .= '</select></td>';
        if (!$matched) {
            return false;
        }

        // same as hours
        $vals = array('*'=>'*');
        $tstamp = time();
        while (date('w',$tstamp) != 0) {
            $tstamp = mktime(0,0,0,date('n',$tstamp),date('j',$tstamp)+1,date('Y'));
        }
        for ($i=0;$i<7;$i++) {
            $vals[$i] = date('D',$tstamp);
            $tstamp = mktime(0,0,0,date('n',$tstamp),date('j',$tstamp)+1,date('Y'));
        }
        $ret .= '<td><select name="wkdy[]" class="form-control">';
        $matched = false;
        list($matched, $opts) = $this->getOpts($vals, $tab[$t_index], $shortname, 'wkdy', '*');
        $ret .= $opts;
        $ret .= '</select></td>';
        if (!$matched) {
            return false;
        }

        $ret .= sprintf('
            <td><input type="hidden" name="cmd[]" value="%s" />
            <a href="help.php?fn=%s" title="Help" class="fancybox-link">%s</a></td>
            </tr>',
            (isset($tab['jobs'][$shortname])?$tab['jobs'][$shortname]['cmd']:$cmd),
            base64_encode($shortname),$nicename
        );

        return $ret;
    }

    private function advancedRow($shortname,$nicename,$cmd,$tab,$i)
    {
        $t_index = 'jobs';
        // if shortname is a task class name, switch indexes
        // and populate defaults if the task is not enabled
        if (substr($shortname, -4) != '.php') {
            $t_index = 'tasks';
            $obj = new $shortname(); 
            if (!isset($tab['tasks'][$shortname])) {
                $tab['tasks'][$shortname] = array(
                    'min' => ''.$obj->default_schedule['min'],
                    'hour' => ''.$obj->default_schedule['hour'],
                    'day' => ''.$obj->default_schedule['day'],
                    'month' => ''.$obj->default_schedule['month'],
                    'wkdy' => ''.$obj->default_schedule['weekday'],
                );
            }
            if ($obj->name != 'Fannie Task') {
                $nicename = $obj->name;
            }
        }

        // defaults are set as once a year so someone doesn't accidentallly
        // start firing a job off every minute
        return sprintf('<tr class="%s">
            <td><input type="checkbox" name="enabled[]" %s value="%d" /></td>
            <td><input class="form-control" type="text" size="2" name="min[]" value="%s" /></td>
            <td><input class="form-control" type="text" size="2" name="hour[]" value="%s" /></td>
            <td><input class="form-control" type="text" size="2" name="day[]" value="%s" /></td>
            <td><input class="form-control" type="text" size="2" name="month[]" value="%s" /></td>
            <td><input class="form-control" type="text" size="2" name="wkdy[]" value="%s" /></td>
            <td><input type="hidden" name="cmd[]" value="%s" />
            <a href="help.php?fn=%s" class="fancybox-link" title="Help">%s</a></td>
            </tr>',
            (isset($tab[$t_index][$shortname])&&isset($tab[$t_index][$shortname]['cmd'])?'taskOn':'taskOff'),
            (isset($tab[$t_index][$shortname])&&isset($tab[$t_index][$shortname]['cmd'])?'checked':''),$i,
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['min']:'0'),
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['hour']:'0'),
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['day']:'1'),
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['month']:'1'),
            (isset($tab[$t_index][$shortname])?$tab[$t_index][$shortname]['wkdy']:'*'),
            (isset($tab[$t_index][$shortname])&&isset($tab[$t_index][$shortname]['cmd'])?$tab[$t_index][$shortname]['cmd']:$cmd),
            base64_encode($shortname),$nicename
        );
    }

    /**
      Get list of php files from cron directory
    */
    private function scanScripts($dir,$arr)
    {
        if (!is_dir($dir)){ 
            if (substr($dir,-4) == ".php") {
                $arr[] = $dir;
            }
            return $arr;
        } else if (substr($dir,-11) == "/management"){
            return $arr;
        } else {
            $dhd = opendir($dir);
            while(($file = readdir($dhd)) !== false){
                if ($file == "." || $file == "..") {
                    continue;
                }
                $arr = $this->scanScripts($dir."/".$file,$arr);
            }
            return $arr;
        }
    }

    /**
       Read webserver user's current cron tab

       Extract email address as well as both "jobs"
       and "tasks". Jobs are traditional CLI-scripts.
       Tasks are classes implementing FannieTask.
    */
    private function readCrontab()
    {
        global $FANNIE_ROOT;
        $pct = popen('crontab -l 2>&1','r');
        $lines = array();
        while (!feof($pct)) {
            $lines[] = fgets($pct);
        }
        pclose($pct);

        $ret = array(
        'jobs' => array(),
        'tasks' => array(),
        'email' => ''
        );

        foreach ($lines as $line) {
            if ($line === false) {
                continue;
            }
            $line = trim($line);
            if ($line[0] == "#") {
                continue;
            }
            if (substr($line,0,6) == "MAILTO") {
                $ret['email'] = substr($line,7);
                continue;
            }
            $tmp = preg_split("/\s+/",$line,6);
            if (count($tmp) == 6) {
                if (strstr($tmp[5], 'FannieTask')) {
                    $script = str_replace(" >> {$FANNIE_ROOT}logs/fannie.log","",$tmp[5]);
                    $script = str_replace(" >> {$FANNIE_ROOT}logs/dayend.log","",$script);
                    $tmp[5] = str_replace(" >> {$FANNIE_ROOT}logs/dayend.log", " >> {$FANNIE_ROOT}logs/fannie.log", $tmp[5]);
                    $parts = explode(' ', $script);
                    $script = $parts[count($parts)-1];
                    $ret['tasks'][$script] = array(
                        'min' => $tmp[0],
                        'hour' => $tmp[1],
                        'day' => $tmp[2],
                        'month' => $tmp[3],
                        'wkdy' => $tmp[4],
                        'cmd' => $tmp[5]
                    );
                } else {
                    $script = str_replace("cd {$FANNIE_ROOT}cron && php ./","",$tmp[5]);
                    $script = str_replace(" >> {$FANNIE_ROOT}logs/fannie.log","",$script);
                    $script = str_replace(" >> {$FANNIE_ROOT}logs/dayend.log","",$script);
                    $tmp[5] = str_replace(" >> {$FANNIE_ROOT}logs/dayend.log", " >> {$FANNIE_ROOT}logs/fannie.log", $tmp[5]);
                    $ret['jobs'][$script] = array(
                        'min' => $tmp[0],
                        'hour' => $tmp[1],
                        'day' => $tmp[2],
                        'month' => $tmp[3],
                        'wkdy' => $tmp[4],
                        'cmd' => $tmp[5]
                    );
                }
            }
        }

        return $ret;
    }

    function css_content()
    {
        return '
        tr.taskOn td {
            font-weight: bold;
            background: #ccc;
        }
        tr.deprecatedJobOn td {
            font-weight: bold;
            background: #C56060;
        }
        tr.deprecatedJobOff td {
            background: #D99595;
        }
        ';
    }

    public function helpContent()
    {
        return '<p>Scheduled Tasks are background jobs that run periodically
            based on this schedule. Note this interface only works on Linux
            and other systems with cron.</p>
            <p>When reading the schedule fields, asterisk (*) means <em>all</em>.
            For example, setting the last three fields to asterisk means run 
            daily at the time specified by hour & minutes.</p>
            <p>Clicking on the right-hand command name shows a bit more information
            about what that particular task does.</p>
            <p>Entering an e-mail address will send any task-related error
            messages to that address.</p>
            <p>The <em>Advanced View</em> provides five text fields for scheduling
            instead of dropdowns. This is for experienced users who want to use
            more advanced cron settings.</p>'
            ;
    }

    private function getOpts($vals, $tab, $shortname, $period, $default)
    {
        $ret = '';
        $matched = false;
        foreach ($vals as $k=>$v) {
            $ret .= sprintf('<option value="%s"',$k);
            if ("$k" === (isset($tab[$shortname])?$tab[$shortname][$period]:"{$default}")) {
                $ret .= ' selected';
                $matched = true;
            }
            $ret .= '>'.$v.'</option>';
        }

        return array($matched, $ret);
    }

    public function unitTest($phpunit)
    {
        $phpunit->assertNotEquals(0, strlen($this->body_content()));
    }

}

FannieDispatch::conditionalExec();