text:
<?php
/**
 * Critical CSS Cron logic:
 * processes the queue, submitting jobs to criticalcss.com and retrieving generated CSS from criticalcss.com and saving rules.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class autoptimizeCriticalCSSCron {
    public function __construct()
    {
        // fetch all options at once and populate them individually explicitely as globals.
        $all_options = autoptimizeCriticalCSSBase::fetch_options();
        foreach ( $all_options as $_option => $_value ) {
            global ${$_option};
            ${$_option} = $_value;
        }

        // Add queue control to a registered event.
        add_action( 'ao_ccss_queue', array( $this, 'ao_ccss_queue_control' ) );
        // Add cleaning job to a registered event.
        add_action( 'ao_ccss_maintenance', array( $this, 'ao_ccss_cleaning' ) );
    }

    public function ao_ccss_queue_control() {
        // The queue execution backend.
        global $ao_ccss_key;
        if ( ! isset( $ao_ccss_key ) || empty( $ao_ccss_key ) ) {
            // no key set, not processing the queue!
            autoptimizeCriticalCSSCore::ao_ccss_log( 'No key set, so not processing queue.', 3 );
            return;
        }

        /**
         * Provide a debug facility for the queue
         *    This debug facility provides a way to easily force some queue behaviors useful for development and testing.
         *    To enable this feature, create the file AO_CCSS_DIR . 'queue.json' with a JSON object like the one bellow:
         *
         *    {"enable":bool,"htcode":int,"status":0|"str","resultStatus ":0|"str","validationStatus":0|"str"}
         *
         *    Where values are:
         *    - enable                    : 0 or 1, enable or disable this debug facility straight from the file
         *    - htcode                    : 0 or any HTTP reponse code (e.g. 2xx, 4xx, 5xx) to force API responses
         *    - status                    : 0 or a valid value for 'status' (see 'Generating critical css - Job Status Types' in spec docs)
         *    - resultStatus        : 0 or a valid value for 'resultStatus' (see 'Appendix - Result status types' in the spec docs)
         *    - validationStatus: 0 or a valid value for 'validationStatus' (see 'Appendix - Validation status types' in the spec docs)
         *
         *    When properly set, queue will always finish a job with the declared settings above regardless of the real API responses.
         */
        $queue_debug = false;
        if ( file_exists( AO_CCSS_DEBUG ) ) {
            $qdobj_raw = file_get_contents( AO_CCSS_DEBUG );
            $qdobj     = json_decode( $qdobj_raw, true );
            if ( $qdobj ) {
                if ( 1 === $qdobj['enable'] ) {
                    $queue_debug = true;
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue operating in debug mode with the following settings: <' . $qdobj_raw . '>', 3 );
                }
            }
        }

        // Set some default values for $qdobj to avoid function call warnings.
        if ( ! $queue_debug ) {
            $qdobj['htcode'] = false;
        }

        // Check if queue is already running.
        $queue_lock = false;
        if ( file_exists( AO_CCSS_LOCK ) ) {
            $queue_lock = true;
        }

        // Proceed with the queue if it's not already running.
        if ( ! $queue_lock ) {

            // Log queue start and create the lock file.
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue control started', 3 );
            if ( touch( AO_CCSS_LOCK ) ) {
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue control locked', 3 );
            }

            // Attach required variables.
            global $ao_ccss_queue;
            global $ao_ccss_rtimelimit;

            // Initialize counters.
            if ( $ao_ccss_rtimelimit == 0 ) {
                // no time limit set, let's go with 1000 seconds.
                $ao_ccss_rtimelimit = 1000;
            }
            $mt = time() + $ao_ccss_rtimelimit; // maxtime queue processing can run.
            $jc = 1; // job count number.
            $jr = 1; // jobs requests number.
            $jt = count( $ao_ccss_queue ); // number of jobs in queue.

            // Sort queue by ascending job status (e.g. ERROR, JOB_ONGOING, JOB_QUEUED, NEW...).
            array_multisort( array_column( $ao_ccss_queue, 'jqstat' ), $ao_ccss_queue ); // @codingStandardsIgnoreLine

            // Iterates over the entire queue.
            foreach ( $ao_ccss_queue as $path => $jprops ) {
                // Prepare flags and target rule.
                $update      = false;
                $deljob      = false;
                $rule_update = false;
                $oldccssfile = false;
                $trule       = explode( '|', $jprops['rtarget'] );

                // Log job count.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Processing job ' . $jc . ' of ' . $jt . ' with id <' . $jprops['ljid'] . '> and status <' . $jprops['jqstat'] . '>', 3 );

                // Process NEW jobs.
                if ( 'NEW' == $jprops['jqstat'] ) {

                    // Log the new job.
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'Found NEW job with local ID <' . $jprops['ljid'] . '>, starting its queue processing', 3 );

                    // Compare job and rule hashes (if any).
                    $hash = $this->ao_ccss_diff_hashes( $jprops['ljid'], $jprops['hash'], $jprops['hashes'], $jprops['rtarget'] );

                    // If job hash is new or different of a previous one.
                    if ( $hash ) {
                        if ( $jr > 2 ) {
                            // we already posted 2 jobs to criticalcss.com, don't post more this run
                            // but we can keep on processing the queue to keep it tidy.
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Holding off on generating request for job with local ID <' . $jprops['ljid'] . '>, maximum number of POSTS reached.', 3 );
                            continue;
                        }

                        // Set job hash.
                        $jprops['hash'] = $hash;

                        // Dispatch the job generate request and increment request count.
                        $apireq = $this->ao_ccss_api_generate( $path, $queue_debug, $qdobj['htcode'] );
                        $jr++;

                        // NOTE: All the following conditions maps to the ones in admin_settings_queue.js.php.
                        if ( 'JOB_QUEUED' == $apireq['job']['status'] || 'JOB_ONGOING' == $apireq['job']['status'] ) {
                            // SUCCESS: request has a valid result.
                            // Update job properties.
                            $jprops['jid']    = $apireq['job']['id'];
                            $jprops['jqstat'] = $apireq['job']['status'];
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> generate request successful, remote id <' . $jprops['jid'] . '>, status now is <' . $jprops['jqstat'] . '>', 3 );
                        } elseif ( 'STATUS_JOB_BAD' == $apireq['job']['status'] ) {
                            // ERROR: concurrent requests
                            // Update job properties.
                            $jprops['jid']    = $apireq['job']['id'];
                            $jprops['jqstat'] = $apireq['job']['status'];
                            if ( $apireq['job']['error'] ) {
                                $jprops['jrstat'] = $apireq['job']['error'];
                            } else {
                                $jprops['jrstat'] = 'Baby did a bad bad thing';
                            }
                            $jprops['jvstat'] = 'NONE';
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Concurrent requests when processing job id <' . $jprops['ljid'] . '>, job status is now <' . $jprops['jqstat'] . '>', 3 );
                        } elseif ( 'INVALID_JWT_TOKEN' == $apireq['errorCode'] ) {
                            // ERROR: key validation
                            // Update job properties.
                            $jprops['jqstat'] = $apireq['errorCode'];
                            $jprops['jrstat'] = $apireq['error'];
                            $jprops['jvstat'] = 'NONE';
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'API key validation error when processing job id <' . $jprops['ljid'] . '>, job status is now <' . $jprops['jqstat'] . '>', 3 );
                        } elseif ( empty( $apireq ) ) {
                            // ERROR: no response
                            // Update job properties.
                            $jprops['jqstat'] = 'NO_RESPONSE';
                            $jprops['jrstat'] = 'NONE';
                            $jprops['jvstat'] = 'NONE';
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> request has no response, status now is <' . $jprops['jqstat'] . '>', 3 );
                        } else {
                            // UNKNOWN: unhandled generate exception
                            // Update job properties.
                            $jprops['jqstat'] = 'JOB_UNKNOWN';
                            $jprops['jrstat'] = 'NONE';
                            $jprops['jvstat'] = 'NONE';
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> generate request has an UNKNOWN condition, status now is <' . $jprops['jqstat'] . '>, check log messages above for more information', 2 );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job response was: ' . json_encode( $apireq ), 3 );
                        }
                    } else {
                        // SUCCESS: Job hash is equal to a previous one, so it's done
                        // Update job status and finish time.
                        $jprops['jqstat'] = 'JOB_DONE';
                        $jprops['jftime'] = microtime( true );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> requires no further processing, status now is <' . $jprops['jqstat'] . '>', 3 );
                    }

                    // Set queue update flag.
                    $update = true;

                } elseif ( 'JOB_QUEUED' == $jprops['jqstat'] || 'JOB_ONGOING' == $jprops['jqstat'] ) {
                    // Process QUEUED and ONGOING jobs
                    // Log the pending job.
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'Found PENDING job with local ID <' . $jprops['ljid'] . '>, continuing its queue processing', 3 );

                    // Dispatch the job result request and increment request count.
                    $apireq = $this->ao_ccss_api_results( $jprops['jid'], $queue_debug, $qdobj['htcode'] );

                    // NOTE: All the following condigitons maps to the ones in admin_settings_queue.js.php
                    // Replace API response values if queue debugging is enabled and some value is set.
                    if ( $queue_debug ) {
                        if ( $qdobj['status'] ) {
                            $apireq['status'] = $qdobj['status'];
                        }
                        if ( $qdobj['resultStatus'] ) {
                            $apireq['resultStatus'] = $qdobj['resultStatus'];
                        }
                        if ( $qdobj['validationStatus'] ) {
                            $apireq['validationStatus'] = $qdobj['validationStatus'];
                        }
                    }

                    if ( 'JOB_QUEUED' == $apireq['status'] || 'JOB_ONGOING' == $apireq['status'] ) {
                        // SUCCESS: request has a valid result
                        // Process a PENDING job
                        // Update job properties.
                        $jprops['jqstat'] = $apireq['status'];
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . '> unchanged', 3 );
                    } elseif ( 'JOB_DONE' == $apireq['status'] ) {
                        // Process a DONE job
                        // New resultStatus from ccss.com "HTML_404", consider as "GOOD" for now.
                        if ( 'HTML_404' == $apireq['resultStatus'] ) {
                            $apireq['resultStatus'] = 'GOOD';
                        }

                        if ( 'GOOD' == $apireq['resultStatus'] && ( 'GOOD' == $apireq['validationStatus'] || 'WARN' == $apireq['validationStatus'] ) ) {
                            // SUCCESS: GOOD job with GOOD or WARN validation
                            // Update job properties.
                            $jprops['file']   = $this->ao_ccss_save_file( $apireq['css'], $trule, false );
                            $jprops['jqstat'] = $apireq['status'];
                            $jprops['jrstat'] = $apireq['resultStatus'];
                            $jprops['jvstat'] = $apireq['validationStatus'];
                            $jprops['jftime'] = microtime( true );
                            $rule_update      = true;
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . '>, file saved <' . $jprops['file'] . '>', 3 );
                        } elseif ( 'GOOD' == $apireq['resultStatus'] && ( 'BAD' == $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' == $apireq['validationStatus'] ) ) {
                            // SUCCESS: GOOD job with BAD or SCREENSHOT_WARN_BLANK validation
                            // Update job properties.
                            $jprops['jqstat'] = $apireq['status'];
                            $jprops['jrstat'] = $apireq['resultStatus'];
                            $jprops['jvstat'] = $apireq['validationStatus'];
                            $jprops['jftime'] = microtime( true );
                            if ( apply_filters( 'autoptimize_filter_ccss_save_review_rules', true ) ) {
                                $jprops['file']   = $this->ao_ccss_save_file( $apireq['css'], $trule, true );
                                $rule_update      = true;
                                autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . ', file saved <' . $jprops['file'] . '> but requires REVIEW', 3 );
                            } else {
                                autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . ', file not saved because it required REVIEW.', 3 );
                            }
                        } elseif ( 'GOOD' != $apireq['resultStatus'] && ( 'GOOD' != $apireq['validationStatus'] || 'WARN' != $apireq['validationStatus'] || 'BAD' != $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' != $apireq['validationStatus'] ) ) {
                            // ERROR: no GOOD, WARN or BAD results
                            // Update job properties.
                            $jprops['jqstat'] = $apireq['status'];
                            $jprops['jrstat'] = $apireq['resultStatus'];
                            $jprops['jvstat'] = $apireq['validationStatus'];
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
                            $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job response was: ' . json_encode( $apireq ), 3 );
                        } else {
                            // UNKNOWN: unhandled JOB_DONE exception
                            // Update job properties.
                            $jprops['jqstat'] = 'JOB_UNKNOWN';
                            $jprops['jrstat'] = $apireq['resultStatus'];
                            $jprops['jvstat'] = $apireq['validationStatus'];
                            $jprops['jftime'] = microtime( true );
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful but job is UNKNOWN, status now is <' . $jprops['jqstat'] . '>', 2 );
                            $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
                            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job response was: ' . json_encode( $apireq ), 3 );
                        }
                    } elseif ( 'JOB_FAILED' == $apireq['job']['status'] || 'STATUS_JOB_BAD' == $apireq['job']['status'] ) {
                        // ERROR: failed job
                        // Update job properties.
                        $jprops['jqstat'] = $apireq['job']['status'];
                        if ( $apireq['job']['error'] ) {
                            $jprops['jrstat'] = $apireq['job']['error'];
                        } else {
                            $jprops['jrstat'] = 'Baby did a bad bad thing';
                        }
                        $jprops['jvstat'] = 'NONE';
                        $jprops['jftime'] = microtime( true );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
                    } elseif ( 'This css no longer exists. Please re-generate it.' == $apireq['error'] ) {
                        // ERROR: CSS doesn't exist
                        // Update job properties.
                        $jprops['jqstat'] = 'NO_CSS';
                        $jprops['jrstat'] = $apireq['error'];
                        $jprops['jvstat'] = 'NONE';
                        $jprops['jftime'] = microtime( true );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
                    } elseif ( empty( $apireq ) ) {
                        // ERROR: no response
                        // Update job properties.
                        $jprops['jqstat'] = 'NO_RESPONSE';
                        $jprops['jrstat'] = 'NONE';
                        $jprops['jvstat'] = 'NONE';
                        $jprops['jftime'] = microtime( true );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> request has no response, status now is <' . $jprops['jqstat'] . '>', 3 );
                    } else {
                        // UNKNOWN: unhandled results exception
                        // Update job properties.
                        $jprops['jqstat'] = 'JOB_UNKNOWN';
                        $jprops['jrstat'] = 'NONE';
                        $jprops['jvstat'] = 'NONE';
                        $jprops['jftime'] = microtime( true );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> result request has an UNKNOWN condition, status now is <' . $jprops['jqstat'] . '>, check log messages above for more information', 2 );
                    }

                    // Set queue update flag.
                    $update = true;
                }

                // Mark DONE jobs for removal.
                if ( 'JOB_DONE' == $jprops['jqstat'] ) {
                    $update = true;
                    $deljob = true;
                }

                // Persist updated queue object.
                if ( $update ) {
                    if ( ! $deljob ) {
                        // Update properties of a NEW or PENDING job...
                        $ao_ccss_queue[ $path ] = $jprops;
                    } else {
                        // ...or remove the DONE job.
                        unset( $ao_ccss_queue[ $path ] );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> is DONE and was removed from the queue', 3 );
                    }

                    // Update queue object.
                    $ao_ccss_queue_raw = json_encode( $ao_ccss_queue );
                    update_option( 'autoptimize_ccss_queue', $ao_ccss_queue_raw, false );
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue updated by job id <' . $jprops['ljid'] . '>', 3 );

                    // Update target rule.
                    if ( $rule_update ) {
                        $this->ao_ccss_rule_update( $jprops['ljid'], $jprops['rtarget'], $jprops['file'], $jprops['hash'] );
                        autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $jprops['ljid'] . '> updated the target rule <' . $jprops['rtarget'] . '>', 3 );
                    }
                } else {
                    // Or log no queue action.
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'Nothing to do on this job', 3 );
                }

                // Break the loop if request time limit is (almost exceeded).
                if ( time() > $mt ) {
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'The time limit of ' . $ao_ccss_rtimelimit . ' seconds was exceeded, queue control must finish now', 3 );
                    break;
                }

                // Increment job counter.
                $jc++;
            }

            // Remove the lock file and log the queue end.
            if ( file_exists( AO_CCSS_LOCK ) ) {
                unlink( AO_CCSS_LOCK );
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue control unlocked', 3 );
            }
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue control finished', 3 );

            // Log that queue is locked.
        } else {
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue is already running, skipping the attempt to run it again', 3 );
        }
    }

    public function ao_ccss_diff_hashes( $ljid, $hash, $hashes, $rule ) {
        // Compare job hashes
        // STEP 1: update job hashes.
        if ( 1 == count( $hashes ) ) {
            // Job with a single hash
            // Set job hash.
            $hash = $hashes[0];
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> updated with SINGLE hash <' . $hash . '>', 3 );
        } else {
            // Job with multiple hashes
            // Loop through hashes to concatenate them.
            $nhash = '';
            foreach ( $hashes as $shash ) {
                $nhash .= $shash;
            }

            // Set job hash.
            $hash = md5( $nhash );
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> updated with a COMPOSITE hash <' . $hash . '>', 3 );
        }

        // STEP 2: compare job to existing jobs to prevent double submission for same type+hash.
        global $ao_ccss_queue;

        foreach ( $ao_ccss_queue as $queue_item ) {
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Comparing <' . $rule . $hash . '> with <' . $queue_item['rtarget'] . $queue_item['hash'] . '>', 3 );
            if ( $queue_item['hash'] == $hash && $queue_item['rtarget'] == $rule && in_array( $queue_item['jqstat'], array( 'JOB_QUEUED', 'JOB_ONGOING', 'JOB_DONE' ) ) ) {
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> matches the already pending job <' . $queue_item['ljid'] . '>', 3 );
                return false;
            }
        }

        // STEP 3: compare job and existing rule (if any) hashes
        // Attach required arrays.
        global $ao_ccss_rules;

        // Prepare rule variables.
        $trule = explode( '|', $rule );
        $srule = $ao_ccss_rules[ $trule[0] ][ $trule[1] ];

        // Check if a MANUAL rule exist and return false.
        if ( ! empty( $srule ) && ( 0 == $srule['hash'] && 0 != $srule['file'] ) ) {
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> matches the MANUAL rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
            return false;
        } elseif ( ! empty( $srule ) ) {
            // Check if an AUTO rule exist.
            if ( $hash === $srule['hash'] && is_file( AO_CCSS_DIR . $srule['file'] ) && 0 != filesize( AO_CCSS_DIR . $srule['file'] ) ) {
                // Check if job hash matches rule, if the CCSS file exists said file is not empty and return FALSE is so.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> with hash <' . $hash . '> MATCH the one in rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
                return false;
            } else {
                // Or return the new hash if they differ.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> with hash <' . $hash . '> DOES NOT MATCH the one in rule <' . $trule[0] . '|' . $trule[1] . '> or rule\'s CCSS file was invalid.', 3 );
                return $hash;
            }
        } else {
            // Or just return the hash if no rule exist yet.
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Job id <' . $ljid . '> with hash <' . $hash . '> has no rule yet', 3 );
            return $hash;
        }
    }

    public function ao_ccss_api_generate( $path, $debug, $dcode ) {
        // POST jobs to criticalcss.com and return responses
        // Get key and key status.
        global $ao_ccss_key;
        global $ao_ccss_keyst;
        $key        = $ao_ccss_key;
        $key_status = $ao_ccss_keyst;

        // Prepare full URL to request.
        global $ao_ccss_noptimize;

        $site_host = get_site_url();
        $site_path = parse_url( $site_host, PHP_URL_PATH );

        if ( ! empty( $site_path ) ) {
            $site_host = str_replace( $site_path, '', $site_host );
        }

        // Logic to bind to one domain to avoid site clones of sites would
        // automatically begin spawning requests to criticalcss.com which has
        // a per domain cost.
        global $ao_ccss_domain;
        if ( empty( $ao_ccss_domain ) ) {
            // first request being done, update option to allow future requests are only allowed if from same domain.
            update_option( 'autoptimize_ccss_domain', str_rot13( $site_host ) );
        } elseif ( trim( $ao_ccss_domain, '\'"' ) !== 'none' && parse_url( $site_host, PHP_URL_HOST ) !== parse_url( $ao_ccss_domain, PHP_URL_HOST ) && apply_filters( 'autoptimize_filter_ccss_bind_domain', true ) ) {
            // not the same domain, log as error and return without posting to criticalcss.com.
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Request for domain ' . $site_host . ' does not match bound domain ' . $ao_ccss_domain . ' so not proceeding.', 2 );
            return false;
        }

        $src_url = $site_host . $path;

        // Avoid AO optimizations if required by config or avoid lazyload if lazyload is active in AO.
        if ( ! empty( $ao_ccss_noptimize ) ) {
            $src_url .= '?ao_noptirocket=1';
        } elseif ( class_exists( 'autoptimizeImages', false ) && autoptimizeImages::should_lazyload_wrapper() ) {
            $src_url .= '?ao_nolazy=1';
        }

        $src_url = apply_filters( 'autoptimize_filter_ccss_cron_srcurl', $src_url );

        // Initialize request body.
        $body           = array();
        $body['url']    = $src_url;
        $body['aff']    = 1;
        $body['aocssv'] = AO_CCSS_VER;

        // Prepare and add viewport size to the body if available.
        $viewport = autoptimizeCriticalCSSCore::ao_ccss_viewport();
        if ( ! empty( $viewport['w'] ) && ! empty( $viewport['h'] ) ) {
            $body['width']  = $viewport['w'];
            $body['height'] = $viewport['h'];
        }

        // Prepare and add forceInclude to the body if available.
        global $ao_ccss_finclude;
        $finclude = $this->ao_ccss_finclude( $ao_ccss_finclude );
        if ( ! empty( $finclude ) ) {
            $body['forceInclude'] = $finclude;
        }

        // Add filter to allow the body array to be altered (e.g. to add customPageHeaders).
        $body = apply_filters( 'autoptimize_ccss_cron_api_generate_body', $body );

        // Body must be json and log it.
        $body = json_encode( $body );
        autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: POST generate request body is ' . $body, 3 );

        // Prepare the request.
        $url  = esc_url_raw( AO_CCSS_API . 'generate?aover=' . AO_CCSS_VER );
        $args = array(
            'headers' => array(
                'User-Agent'    => 'Autoptimize v' . AO_CCSS_VER,
                'Content-type'  => 'application/json; charset=utf-8',
                'Authorization' => 'JWT ' . $key,
                'Connection'    => 'close',
            ),
            'body'    => $body,
        );

        // Dispatch the request and store its response code.
        $req  = wp_safe_remote_post( $url, $args );
        $code = wp_remote_retrieve_response_code( $req );
        $body = json_decode( wp_remote_retrieve_body( $req ), true );

        if ( $debug && $dcode ) {
            // If queue debug is active, change response code.
            $code = $dcode;
        }

        if ( 200 == $code ) {
            // Response code is OK.
            // Workaround criticalcss.com non-RESTful reponses.
            if ( 'JOB_QUEUED' == $body['job']['status'] || 'JOB_ONGOING' == $body['job']['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) {
                // Log successful and return encoded request body.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied successfully', 3 );

                // This code also means the key is valid, so cache key status for 24h if not already cached.
                if ( ( ! $key_status || 2 != $key_status ) && $key ) {
                    update_option( 'autoptimize_ccss_keyst', 2 );
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: API key is valid, updating key status', 3 );
                }

                // Return the request body.
                return $body;
            } else {
                // Log successful requests with invalid reponses.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
                autoptimizeCriticalCSSCore::ao_ccss_log( print_r( $body, true ), 2 );
                return $body;
            }
        } else {
            // Response code is anything else.
            // Log failed request with a valid response code and return body.
            if ( $code ) {
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with error code <' . $code . '>, body follows...', 2 );
                autoptimizeCriticalCSSCore::ao_ccss_log( print_r( $body, true ), 2 );

                if ( 401 == $code ) {
                    // If request is unauthorized, also clear key status.
                    update_option( 'autoptimize_ccss_keyst', 1 );
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: API key is invalid, updating key status', 3 );
                }

                // Return the request body.
                return $body;
            } else {
                // Log failed request with no response and return false.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: POST generate request for path <' . $src_url . '> has no response, this could be a service timeout', 2 );
                if ( is_wp_error( $req ) ) {
                    autoptimizeCriticalCSSCore::ao_ccss_log( $req->get_error_message(), 2 );
                }

                return false;
            }
        }
    }

    public function ao_ccss_api_results( $jobid, $debug, $dcode ) {
        // GET jobs from criticalcss.com and return responses
        // Get key.
        global $ao_ccss_key;
        $key = $ao_ccss_key;

        // Prepare the request.
        $url  = AO_CCSS_API . 'results?resultId=' . $jobid;
        $args = array(
            'headers' => array(
                'User-Agent'    => 'Autoptimize CriticalCSS Power-Up v' . AO_CCSS_VER,
                'Authorization' => 'JWT ' . $key,
                'Connection'    => 'close',
            ),
        );

        // Dispatch the request and store its response code.
        $req  = wp_safe_remote_get( $url, $args );
        $code = wp_remote_retrieve_response_code( $req );
        $body = json_decode( wp_remote_retrieve_body( $req ), true );

        if ( $debug && $dcode ) {
            // If queue debug is active, change response code.
            $code = $dcode;
        }

        if ( 200 == $code ) {
            // Response code is OK.
            if ( is_array( $body ) && ( array_key_exists( 'status', $body ) || array_key_exists( 'job', $body ) ) && ( 'JOB_QUEUED' == $body['status'] || 'JOB_ONGOING' == $body['status'] || 'JOB_DONE' == $body['status'] || 'JOB_FAILED' == $body['status'] || 'JOB_UNKNOWN' == $body['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) ) {
                // Workaround criticalcss.com non-RESTful reponses
                // Log successful and return encoded request body.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully', 3 );
                return $body;
            } elseif ( is_array( $body ) && ( array_key_exists( 'error', $body ) && 'This css no longer exists. Please re-generate it.' == $body['error'] ) ) {
                // Handle no CSS reply
                // Log no CSS error and return encoded request body.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully but the CSS for it does not exist anymore', 3 );
                return $body;
            } else {
                // Log failed request and return false.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
                autoptimizeCriticalCSSCore::ao_ccss_log( print_r( $body, true ), 2 );
                return false;
            }
        } else {
            // Response code is anything else
            // Log failed request with a valid response code and return body.
            if ( $code ) {
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with error code <' . $code . '>, body follows...', 2 );
                autoptimizeCriticalCSSCore::ao_ccss_log( print_r( $body, true ), 2 );
                if ( 401 == $code ) {
                    // If request is unauthorized, also clear key status.
                    update_option( 'autoptimize_ccss_keyst', 1 );
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: API key is invalid, updating key status', 3 );
                }

                // Return the request body.
                return $body;
            } else {
                // Log failed request with no response and return false.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> has no response, this could be a service timeout', 2 );
                return false;
            }
        }
    }

    public function ao_ccss_save_file( $ccss, $target, $review ) {
        // Save critical CSS into the filesystem and return its filename
        // Prepare review mark.
        if ( $review ) {
            $rmark = '_R';
        } else {
            $rmark = '';
        }

        // Prepare target rule, filename and content.
        $filename = false;
        $content  = $ccss;

        if ( autoptimizeCriticalCSSCore::ao_ccss_check_contents( $content ) ) {
            // Sanitize content, set filename and try to save file.
            $file     = AO_CCSS_DIR . 'ccss_' . md5( $ccss . $target[1] ) . $rmark . '.css';
            $status   = file_put_contents( $file, $content, LOCK_EX );
            $filename = pathinfo( $file, PATHINFO_BASENAME );
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Critical CSS file for the rule <' . $target[0] . '|' . $target[1] . '> was saved as <' . $filename . '>, size in bytes is <' . $status . '>', 3 );

            if ( ! $status ) {
                // If file has not been saved, reset filename.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Critical CSS file <' . $filename . '> could not be not saved', 2 );
                $filename = false;
                return $filename;
            }
        } else {
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Critical CSS received did not pass content check', 2 );
            return $filename;
        }

        // Remove old critical CSS if a previous one existed in the rule and if that file exists in filesystem
        // NOTE: out of scope critical CSS file removal (issue #5)
        // Attach required arrays.
        global $ao_ccss_rules;

        // Prepare rule variables.
        $srule   = $ao_ccss_rules[ $target[0] ][ $target[1] ];
        $oldfile = $srule['file'];

        if ( $oldfile && $oldfile !== $filename ) {
            $delfile = AO_CCSS_DIR . $oldfile;
            if ( file_exists( $delfile ) ) {
                $unlinkst = unlink( $delfile );
                if ( $unlinkst ) {
                    autoptimizeCriticalCSSCore::ao_ccss_log( 'A previous critical CSS file <' . $oldfile . '> was removed for the rule <' . $target[0] . '|' . $target[1] . '>', 3 );
                }
            }
        }

        // Return filename or false.
        return $filename;
    }

    public function ao_ccss_rule_update( $ljid, $srule, $file, $hash ) {
        // Update or create a rule
        // Attach required arrays.
        global $ao_ccss_rules;

        // Prepare rule variables.
        $trule  = explode( '|', $srule );
        $rule   = $ao_ccss_rules[ $trule[0] ][ $trule[1] ];
        $action = false;
        $rtype  = '';

        if ( 0 === $rule['hash'] && 0 !== $rule['file'] ) {
            // manual rule, don't ever overwrite.
            $action = 'NOT UPDATED';
            $rtype  = 'MANUAL';
        } elseif ( 0 === $rule['hash'] && 0 === $rule['file'] ) {
            // If this is an user created AUTO rule with no hash and file yet, update its hash and filename
            // Set rule hash, file and action flag.
            $rule['hash'] = $hash;
            $rule['file'] = $file;
            $action       = 'UPDATED';
            $rtype        = 'AUTO';
        } elseif ( 0 !== $rule['hash'] && ctype_alnum( $rule['hash'] ) ) {
            // If this is an genuine AUTO rule, update its hash and filename
            // Set rule hash, file and action flag.
            $rule['hash'] = $hash;
            $rule['file'] = $file;
            $action       = 'UPDATED';
            $rtype        = 'AUTO';
        } else {
            // If rule doesn't exist, create an AUTO rule
            // AUTO rules were only for types, but will now also work for paths.
            if ( ( 'types' == $trule[0] || 'paths' == $trule[0] ) && ! empty( $trule[1] ) ) {
                // Set rule hash and file and action flag.
                $rule['hash'] = $hash;
                $rule['file'] = $file;
                $action       = 'CREATED';
                $rtype        = 'AUTO';
            } else {
                // Log that no rule was created.
                autoptimizeCriticalCSSCore::ao_ccss_log( 'Exception, no AUTO rule created', 3 );
            }
        }

        if ( $action ) {
            // If a rule creation/update is required, persist updated rules object.
            $ao_ccss_rules[ $trule[0] ][ $trule[1] ] = $rule;
            $ao_ccss_rules_raw                       = json_encode( $ao_ccss_rules );
            update_option( 'autoptimize_ccss_rules', $ao_ccss_rules_raw );
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Target rule <' . $srule . '> of type <' . $rtype . '> was ' . $action . ' for job id <' . $ljid . '>', 3 );
        } else {
            autoptimizeCriticalCSSCore::ao_ccss_log( 'No rule action required', 3 );
        }
    }

    function ao_ccss_finclude( $finclude_raw ) {
        // Prepare forceInclude object.
        if ( ! empty( $finclude_raw ) ) {
            // If there are any content
            // Convert raw string into arra and initialize the returning object.
            $fincludes = explode( ',', $finclude_raw );
            $finclude  = array();

            // Interacts over every rule.
            $i = 0;
            foreach ( $fincludes as $include ) {
                // Trim leading and trailing whitespaces.
                $include = trim( $include );

                if ( substr( $include, 0, 2 ) === '//' ) {
                    // Regex rule
                    // Format value as required.
                    $include = str_replace( '//', '/', $include );
                    $include = $include . '/i';

                    // Store regex object.
                    $finclude[ $i ]['type']  = 'RegExp';
                    $finclude[ $i ]['value'] = $include;
                } else {
                    // Simple value rule.
                    $finclude[ $i ]['value'] = $include;
                }

                $i++;
            }

            // Return forceInclude object.
            return $finclude;
        } else {
            // Or just return false if empty.
            return false;
        }
    }

    public function ao_ccss_cleaning() {
        // Perform plugin maintenance
        // Truncate log file >= 1MB .
        if ( file_exists( AO_CCSS_LOG ) ) {
            if ( filesize( AO_CCSS_LOG ) >= 1048576 ) {
                $logfile = fopen( AO_CCSS_LOG, 'w' );
                fclose( $logfile );
            }
        }

        // Remove lock file.
        if ( file_exists( AO_CCSS_LOCK ) ) {
            unlink( AO_CCSS_LOCK );
        }

        // Make sure queue processing is scheduled, recreate if not.
        if ( ! wp_next_scheduled( 'ao_ccss_queue' ) ) {
            wp_schedule_event( time(), apply_filters( 'ao_ccss_queue_schedule', 'ao_ccss' ), 'ao_ccss_queue' );
        }

        // Queue cleaning.
        global $ao_ccss_queue;
        $queue_purge_threshold = 100;
        $queue_purge_age       = 24 * 60 * 60;
        $queue_length          = count( $ao_ccss_queue );
        $timestamp_yesterday   = microtime( true ) - $queue_purge_age;
        $remove_old_new        = false;
        $queue_altered         = false;

        if ( $queue_length > $queue_purge_threshold ) {
            $remove_old_new = true;
        }

        foreach ( $ao_ccss_queue as $path => $job ) {
            if ( ( $remove_old_new && 'NEW' == $job['jqstat'] && $job['jctime'] < $timestamp_yesterday ) || in_array( $job['jqstat'], array( 'JOB_FAILED', 'STATUS_JOB_BAD', 'NO_CSS', 'NO_RESPONSE' ) ) ) {
                unset( $ao_ccss_queue[ $path ] );
                $queue_altered = true;
            }
        }

        // save queue to options!
        if ( $queue_altered ) {
            $ao_ccss_queue_raw = json_encode( $ao_ccss_queue );
            update_option( 'autoptimize_ccss_queue', $ao_ccss_queue_raw, false );
            autoptimizeCriticalCSSCore::ao_ccss_log( 'Queue cleaning done.', 3 );
        }

        // re-check key if invalid.
        global $ao_ccss_keyst;
        if ( 1 == $ao_ccss_keyst ) {
            $this->ao_ccss_api_generate( '', '', '' );
        }
    }
}


text: Spot deals at eTilbudsavis.dk that interests you

redpajama_set_name: RedPajamaGithub


text:
{{{
  "title": "CenturyLink Cloud Guide to HAProxy",
  "date": "12-17-2015",
  "author": "Gavin Lai",
  "attachments": [],
  "contentIsHTML": false
}}}

### Table of contents

* [Overview](#overview)
* [Prerequisites](#prerequisites)
* [Use Case Scenarios](#use-case-scenarios)
* [Preparation](#preparation)
* [Deployment](#deployment)
* [HAProxy configuration differences](#haproxy-configuration-differences)
* [Testing](#testing)
* [Troubleshooting](#troubleshooting)
* [Support](#support)

### Overview

HAProxy is a free, open source solution offering high availability load
balancing and proxy for TCP and HTTP-based applications. HAProxy can
provide a feature rich alternative to the built-in load balancing
offering from CenturyLink Cloud. HAProxy and Keepalive can be used
together to create a high availability load balancing solution.

### Prerequisites

-   Access to the CenturyLink Cloud platform as an authorized user

-   Identify a Network VLAN you want the HAProxy to reside on

-   Understanding the functions of load balancer (beyond the scope of this article) and CenturyLink Cloud load balancing  offerings, to learn more, please see this [KB](../Network/load-balancing-dedicated-vs-shared.md),
    [general knowledge](//en.wikipedia.org/wiki/Load_balancing_%28computing%29)
     and [HAProxy documentations](//www.haproxy.org/#docs)

-   Existing web servers or application servers to be load balanced

- High Availability HAProxy nodes have to reside on the same VLAN as multicast is used for heartbeats

### Use Case Scenarios

HAProxy can be used as dedicated applications (layer 7) or TCP (layer 4) load balancer to create a high availability environment for any internal and external web or applications environment. In this use case, two web servers are load balanced by a pair of HAProxy to create a high availability environment.
HAProxy can be configured to handle both internal and external network traffic and used to perform the following functions:

-   Dedicated web services load balancing

-   Dedicated Application load balancing

-   Dedicated Work load distribution

-   SSL offloading

![HAProxy Network Diagram](../images/haproxy/HAProxy-blockdiagram.png)

Web traffic can come through a public/private Virtual IP (VIP)  and reach the HAProxy pair. HAProxy would redirect the
traffic based on the algorithm chosen in the configuration.
In this scenario, port 80 and 443 are being load balanced with round-robin algorithm.  A complete list of algorithm can be found [here](//cbonte.github.io/haproxy-dconv/configuration-1.6.html)

### Preparation

In preparation. There are several factors need to be considered:

-   Network ports required for the load balance application

-   Firewall rules required

-   SSL certificate if the application is SSL based

-   Configure the Virtual IP for the HAProxy servers with Public IP address:
 - [Assign a public IP address](../Network/how-to-add-public-ip-to-virtual-machine.md) to one of the HAProxy servers and bind it to a new IP address
 ![Adding Public IP with new Privte IP](../images/haproxy/HAProxy-add-publicip.png)
 - From that server, run `ifconfig ens160:1 down` and `remove /etc/sysconfig/network-scripts/ifcfg-ens160:1` file; verify this step by pinging the new public and private IP addresses, there should be no reply from these addresses (assuming ens160 is the network interface)

 - In this example, 10.100.96.28 is the new private IP assigned along with the public IP addresses

- If only Private IP address is required:
 - Please following this [KB](../Network/how-to-reserve-ip-addresses.md) to reserve the Private IP address required for HAProxy

### Deployment

HAProxy can be deployed in a single or HA configuration. This
walk-through cover both single and HA configuration. HAProxy can be
configured with a single network interface or 2 (or more) network
interfaces, depending on the security and infrastructure requirement,
both can be implemented in CenturyLink Cloud.

-   Deploy a CentOS 7 server using either the [Control
    Portal](//control.ctl.io), [CLI](//github.com/CenturyLinkCloud/clc-go-cli) or [API](//www.ctl.io/developers/)

-   Once deployed, connect to [Client
    VPN](../Network/how-to-configure-client-vpn.md)
    and secure shell to the server and start implementation:

-  As the applications use 'GetHostByName' system calls, `hostname -s` output should match the real hostname and not localhost.  In order to change hostname, run the first two commands below and update the repository with `yum update`

    a.  `grep HOSTNAME network | awk -F= '{print \$2}' | tee /etc/hostname`

    b.  `` hostname \`grep HOSTNAME /etc/sysconfig/network | awk -F= '{print \$2}'\` ``

    c.  `yum update`

-  Install HAproxy (and Keepalive only for HA)

    a.  `yum install haproxy -y`

    b.  `yum install keepalived –y`

-  Depending on the application or network ports required to be
    balanced, both the OS, haproxy configuration and firewall need to be
    configured:

    a.  For the OS:
      -   Enable Packet forwarding
      -   Enable reverse path filtering
      -   Bind to non-local address needs to be enabled

    ```
          systctl –w net.ipv4.ip\_forward=1
          systctl –w net.ipv4.conf.default.rp\_filter=1
          systctl –w net.ipv4.ip\_nonlocal\_bind=1
    ```

  b.  For HAProxy configuration (/etc/haproxy/haproxy.cfg)
      -   Enable the application(s) to be load balanced by editing the /etc/haproxy/haproxy.cfg.  The file content would look similar to below:

  ```
  global
     log         127.0.0.1 local2 info
     chroot      /var/lib/haproxy
     pidfile     /var/run/haproxy.pid
     maxconn     4000
     user        haproxy
     group       haproxy
  daemon
     stats socket /var/lib/haproxy/stats
  defaults
     mode                    http
     log                     global
     option                  httplog
     option                  dontlognull
     option http-server-close
     option forwardfor       except 127.0.0.0/8
     option                  redispatch
     retries                 3
     timeout http-request    10s
     timeout queue           1m
     timeout connect         10s
     timeout client          1m
     timeout server          1m
     timeout http-keep-alive 10s
     timeout check           10s
     maxconn                 3000
  frontend  https-in *:443
     mode tcp
     option tcplog
     default_backend             backend_server
  frontend  http-in *:80
     mode http
     default_backend http_server
  backend backend_server
     mode tcp
     balance     roundrobin
     option ssl-hello-chk
     server      wwws01 10.100.96.21:443 check
     server      wwws02 10.100.96.27:443 check
     http-request set-header X-Forwarded-Port %[dst_port]
     http-request add-header X-Forwarded-Proto https if { ssl_fc }
  backend http_server
     mode http
     balance     roundrobin
     server      www01 10.100.96.21:80 check
     server      www02 10.100.96.27:80 check
 ```

   - options in haproxy.cfg like balance algorithm Round-robin (other options are [available](//cbonte.github.io/haproxy-dconv/configuration-1.5.html#4.2-balance)) and options can be found at [HAProxy site](//www.haproxy.org/#docs)

  -   Setting up proper logging (optional, if not set, HAProxy logs might not be reliability populated in /var/log/messages); To do this, UDP syslog reception needs to be enabled as HAProxy is running with chroot by changing the following in /etc/rsyslog.conf:

 ```
    \$ModLoad imudp
    \$UDPServerRun 514
    \$AllowedSender UDP, 127.0.0.1
    \*.info;mail.none;authpriv.none;cron.none;local2.none   /var/log/messages
    local2.\* /var/log/haproxy.log
 ```

  c. Keepalive parameters (in /etc/keepalived/keepalived.conf)

 ```
   global_defs {
     notification_email {
     your@email
   }
   notification_email_from your@email
   smtp_server  your_smtp_server
   smtp_connect_timeout 30
   router_id CA3CCVAHAPROX02
   }

   vrrp_script chk_haproxy {
        script "killall -0 haproxy"
        interval 2
        weigth 2
   }
   vrrp_instance VI_1 {
    state MASTER
    interface ens160
    virtual_router_id 51
    priority 101
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 57924680
    }
    virtual_ipaddress {
        10.100.96.28 dev ens160
    }
    track_interface {
                ens160
        }
    track_script {
                chk_haproxy
        }
   }
 ```

d.  Firewall rules (this varies as application ports and security policy
    can be different, please use the following as references):

  -   HAProxy and Keepalive

 ```
    firewall-cmd --permanent --zone=internal --add-service=http
    firewall-cmd --permanent --zone=internal --add-service=https
    firewall-cmd --zone=internal --add-port=443/tcp
    firewall-cmd --zone=internal --add-port=80/tcp
 ```
  -   Keepalive

 ```
    firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -i ens160 -d 224.0.0.0/8 -j ACCEPT
    firewall-cmd --direct --perm --add-rule ipv4 filter INPUT 0 -i ens160 -d 224.0.0.0/8 -j ACCEPT
    firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p vrrp -i ens160 -j ACCEPT
    firewall-cmd --direct --perm --add-rule ipv4 filter INPUT 0 -p vrrp -i ens160 -j ACCEPT
    firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p vrrp -o ens160 -j ACCEPT
    firewall-cmd --direct --perm --add-rule ipv4 filter OUTPUT 0 -p vrrp -o ens160 -j ACCEPT
    firewall-cmd --reload
 ```
#### HAProxy configuration differences
The difference in configuration (haproxy.cfg) between the two HAProxy nodes:

| HXPRoxy       | haproxy1        | haproxy2  |
|-------------|---------------|---------|
| state         | MASTER          | BACKUP    |
| priority      | 101             |   100     |
| router_id     | SeverName1      |ServerName2|

### Testing
The environment can be tested by disabling httpd (or the load balanced application) and haproxy.  In this example, `systemctl stop httpd` and `systemctl stop haproxy` are used to perform testing.  From the demo below, the screen on the left shows the output of a https request to the VIP.  On the right, the commands are run to show the effect of the https requests.  There is minimal delay for the fail over to take place.
![Demo failover](../images/haproxy/haproxy-demo-failover.gif)

### Troubleshooting
- The most common issue is firewall ports are not configured properly, firewall can be disabled for testing purpose

- If the HAProxy load balancers are on different VLANs, please make sure the firewall ports are configured between the two VLANs (for details, please see this [KB](../Network/connecting-data-center-networks-through-firewall-policies.md))

- Keepalive uses VRRP for heatbeat, the following command would help identify if VRRP is working:

    ```
    netstat -g
    netstat -ng
    ip maddr show
    ```

- As for the virtual IP on the HAProxy server, the Master HAProxy should own the Virtual IP address, `ip a` command will show if that node has the VIP address, the sample output:

 ```
 2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:0c:29:6e:bd:96 brd ff:ff:ff:ff:ff:ff
    inet 10.100.96.26/24 brd 10.100.96.255 scope global ens160
       valid_lft forever preferred_lft forever
    inet 10.100.96.28/32 scope global ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe6e:bd96/64 scope link
 ```

### Support
* For issues related to deploying HAProxy, accessing the deployed software, please visit the [HAProxy Website](//www.haproxy.org/#supp)
* For issues related to cloud infrastructure (VM's, network, etc), or is you experience a problem deploying any Blueprint or Script Package, please open a CenturyLink Cloud Support ticket by emailing [noc@ctl.io](mailto:noc@ctl.io) or [through the CenturyLink Cloud Support website](//t3n.zendesk.com/tickets/new).


text:
$(function () {
    var size = 4;
    var cellWidth = 65;
    var cellHeight = 74;
    var touchSensitivity = 35;
    var width = size * cellWidth;
    var height = size * cellHeight;
    var touchIdentifier;
    var startX;
    var startY;
    var $restart = $('#restart');
    var $battleFieldBorder = $('#battle-field-border');
    var $battleField = $('#battle-field');
    var $scorePlaceholder = $('#score-placeholder');
    var $instructions = $('#instructions');
    var $document = $(document);
    var events = [];
    var bestScore = parseInt(localStorage.getItem('bestScore'),10) || 0;
    var currentGame = localStorage.getItem('currentGame') || '';
    var currentScore = parseInt(localStorage.getItem('currentScore'),10) || 0;

    $('#best-score-placeholder').text(bestScore);

    $battleField.css({
        width: width + 'px',
        height: height + 'px'
    });

    var gridStyle = '';
    var row = 2;

    for (var i = 0; i < size; i++) {
        var column = 5;
        for (var j = 0; j < size; j++) {
            gridStyle += '[data-position="' + i + '-' + j + '"] { -webkit-transform: translate3d(' + column + 'px,' + row + 'px,0); transform: translate3d(' + column + 'px,' + row + 'px,0);}';
            column += cellWidth;
        }
        row += cellHeight;
    }
    $('head').append('');

    var controller = new Controller();
    var grid = new Grid(size, bestScore, currentGame, currentScore);

    var left = function () {
        grid.left();
    }
    var up = function () {
        grid.up();
    }
    var right = function () {
        grid.right();
    }
    var down = function () {
        grid.down();
    }

    var direction = {
        37: left,
        38: up,
        39: right,
        40: down
    }

    var executeEvent = function () {
        if ($battleField.find('.animating').length === 0 && events.length > 0) {
            events[0]();
            events.shift();
        }
        else {
            setTimeout(executeEvent, 50);
        }
    }

    var touchstartHandler = function (event) {
        event.preventDefault();
        var originalEventChangedTouch = event.originalEvent.changedTouches[0];
        touchIdentifier = originalEventChangedTouch.identifier;
        startX = originalEventChangedTouch.pageX;
        startY = originalEventChangedTouch.pageY;
        $battleFieldBorder.on('touchmove', touchmoveHandler);
        $battleFieldBorder.off('touchstart');
    }

    var touchmoveHandler = function (event) {
        event.preventDefault();
        var originalEventChangedTouch = event.originalEvent.changedTouches[0];

        var moveX = originalEventChangedTouch.pageX;
        var moveY = originalEventChangedTouch.pageY;

        var translateAmountX = moveX - startX;
        var translateAmountY = moveY - startY;

        if (Math.abs(translateAmountX) >= touchSensitivity || Math.abs(translateAmountY) >= touchSensitivity) {
            event.stopImmediatePropagation();
            $battleFieldBorder.off('touchmove');
        }
        var action = null;
        if (translateAmountX >= touchSensitivity) {
            action = right;
        }
        else if (translateAmountX <= -touchSensitivity) {
            action = left;
        }
        else if (translateAmountY >= touchSensitivity) {
            action = down;
        }
        else if (translateAmountY <= -touchSensitivity) {
            action = up;
        }
        if (action) {
            events.push(action);
            executeEvent();
        }
    }

    var touchendHandler = function (event) {
        var originalEventChangedTouch = event.originalEvent.changedTouches[0];
        var tempIdentifer = originalEventChangedTouch.identifier;
        if (tempIdentifer === touchIdentifier) {
            $battleFieldBorder.on('touchstart', touchstartHandler);
        }
    };

    var keyupHandler = function (event) {
        if (direction[event.which]) {
            event.preventDefault();
            events.push(direction[event.which]);
            executeEvent();
        }
    }

    var unbindEvents = function () {
        $document.off('keyup');
        $battleFieldBorder.off('touchstart')
            .off('touchend');
    }

    var gameWon = function () {
        if ($battleField.find('.animating').length === 0 && events.length === 0) {
            $battleField.append('

You Win!

');
        }
        else {
            setTimeout(gameWon, 50);
        }
    }

    var gameOver = function () {
        if ($battleField.find('.animating').length === 0 && events.length === 0) {
            $battleField.append('

Game Over!

');
        }
        else {
            setTimeout(gameOver, 50);
        }
    }

    var start = function () {
        unbindEvents();
        $scorePlaceholder.text(currentScore);
        $battleField.empty();
        grid.restart();
        $document.on('keyup', keyupHandler);
        $battleFieldBorder
            .on('touchstart', touchstartHandler)
            .on('touchend', touchendHandler);
    }

    if (localStorage.getItem('instructions') !== 'set') {
        $instructions.show();
    }
    else {
        start();
    }

    $restart.on('click', function () {
        localStorage.setItem('currentGame', '');
        localStorage.setItem('currentScore', 0);
        currentScore = 0;
        start();
    });

    $instructions.on('click', function () {
        $instructions.hide();
        localStorage.setItem('instructions', 'set');
        start();
    });

    $.subscribe('gameOver', function () {
        unbindEvents();
        gameOver();
    });

    $.subscribe('gameWon', function () {
        unbindEvents();
        gameWon();
    });
});


text:
<?php namespace spec\Cornford\Googlmapper;

use Cornford\Googlmapper\Exceptions\MapperArgumentException;
use Cornford\Googlmapper\Exceptions\MapperSearchKeyException;
use Cornford\Googlmapper\Exceptions\MapperSearchResultException;
use Cornford\Googlmapper\Mapper;
use Cornford\Googlmapper\Models\Location;
use Illuminate\View\Factory as View;
use Mockery;
use PhpSpec\ObjectBehavior;

class MapperSpec extends ObjectBehavior
{
    private const STRING = 'test';
    private const LOCATION = 'test';
    private const INTEGER = 10;
    private const FLOAT = 3.33;
    private const BOOLEAN = false;

    private const REGION = 'GB';
    private const LANGUAGE = 'en-gb';
    private const TYPE = 'ROADMAP';

    private const ANIMATION = 'NONE';

    private const GESTURE_HANDLING = 'none';

    private const API_KEY  = 'AIzaSyCTwWv4rTQdNtZAaAd8D1SK5m94eTp8GiQ';

    public function let()
    {
        $location = Mockery::mock(Location::class);

        $view = Mockery::mock(View::class);
        $view->shouldReceive('make')->andReturn($view);
        $view->shouldReceive('withView')->andReturn($view);
        $view->shouldReceive('withOptions')->andReturn($view);
        $view->shouldReceive('withItems')->andReturn($view);
        $view->shouldReceive('render')->andReturn(self::STRING);
        $view->shouldReceive('location')->andReturn($location);

        $this->beConstructedWith($view, ['enabled' => true, 'key' => self::STRING, 'version' => self::FLOAT, 'region' => self::REGION, 'language' => self::LANGUAGE]);
    }

    public function it_is_initializable()
    {
        $this->shouldHaveType(Mapper::class);
    }

    public function it_throws_an_exception_with_incorrect_options()
    {
        $view = Mockery::mock(View::class);
        $this->shouldThrow(MapperArgumentException::class)
            ->during('__construct', [$view]);
    }

    public function it_can_return_a_location_when_a_location_is_searched()
    {
        // Skip due to API Key restrictions
        return;
        $this->setKey(self::API_KEY);
        $this->location(self::LOCATION)->shouldReturnAnInstanceOf(Location::class);
    }

    public function it_throws_an_exception_when_a_blank_location_is_searched()
    {
        $this->setKey(self::API_KEY);
        $this->shouldThrow(MapperArgumentException::class)->during('location', ['']);
    }

    public function it_throws_an_exception_when_an_invalid_location_is_searched()
    {
        // Skip due to API Key restrictions
        return;
        $this->setKey(self::API_KEY);
        $this->shouldThrow(MapperSearchResultException::class)->during('location', ['abcdefghijklmnopqrstuvwxyz']);
    }

    public function it_throws_an_exception_when_an_invalid_key_is_used_to_location_search()
    {
        $this->setKey('123');
        $this->shouldThrow(MapperSearchKeyException::class)->during('location', [self::LOCATION]);
    }

    public function it_can_be_enabled()
    {
        $this->enableMapping();
        $this->isEnabled()->shouldReturn(true);
        $this->render()->shouldReturn(self::STRING);
        $this->getItems()->shouldHaveCount(0);
        $this->getItems()->shouldReturn([]);
    }

    public function it_can_be_disabled()
    {
        $this->disableMapping();
        $this->isEnabled()->shouldReturn(false);
        $this->render()->shouldReturn(null);
        $this->getItems()->shouldHaveCount(0);
    }

    public function it_can_set_and_get_key_option()
    {
        $this->setKey(self::STRING);
        $this->getKey()->shouldReturn(self::STRING);
    }

    public function it_can_set_and_get_version_option()
    {
        $this->setVersion(self::STRING);
        $this->getVersion()->shouldReturn(self::STRING);
    }

    public function it_can_set_and_get_region_option()
    {
        $this->setRegion(self::REGION);
        $this->getRegion()->shouldReturn(self::REGION);
    }

    public function it_can_set_and_get_language_option()
    {
        $this->setLanguage(self::LANGUAGE);
        $this->getLanguage()->shouldReturn(self::LANGUAGE);
    }

    public function it_can_set_and_get_marker_option()
    {
        $this->enableMarkers();
        $this->getMarker()->shouldReturn(true);
        $this->disableMarkers();
        $this->getMarker()->shouldReturn(false);
    }

    public function it_can_set_and_get_center_option()
    {
        $this->enableCenter();
        $this->getCenter()->shouldReturn(true);
        $this->disableCenter();
        $this->getCenter()->shouldReturn(false);
    }

    public function it_can_set_and_get_locate_option()
    {
        $this->enableLocate();
        $this->getLocate()->shouldReturn(true);
        $this->disableLocate();
        $this->getLocate()->shouldReturn(false);
    }

    public function it_can_set_and_get_ui_option()
    {
        $this->enableUi();
        $this->getUi()->shouldReturn(false);
        $this->disableUi();
        $this->getUi()->shouldReturn(true);
    }

    public function it_can_set_and_get_zoom_option()
    {
        $this->setZoom(self::INTEGER);
        $this->getZoom()->shouldReturn(self::INTEGER);
    }

    public function it_can_set_and_get_type_option()
    {
        $this->setType(self::TYPE);
        $this->getType()->shouldReturn(self::TYPE);
    }

    public function it_can_set_and_get_tilt_option()
    {
        $this->setTilt(self::INTEGER);
        $this->getTilt()->shouldReturn(self::INTEGER);
    }

    public function it_can_set_and_get_icon_option()
    {
        $this->setIcon(self::STRING);
        $this->getIcon()->shouldReturn(self::STRING);
    }

    public function it_can_set_and_get_animation_option()
    {
        $this->setAnimation(self::ANIMATION);
        $this->getAnimation()->shouldReturn(self::ANIMATION);
    }

    public function it_can_set_and_get_gesture_handling_option()
    {
        $this->setGestureHandling(self::GESTURE_HANDLING);
        $this->getGestureHandling()->shouldReturn(self::GESTURE_HANDLING);
    }

    public function it_can_set_and_get_cluster_option()
    {
        $this->enableCluster();
        $this->getCluster()->shouldReturn(true);
        $this->disableCluster();
        $this->getCluster()->shouldReturn(false);
    }

    public function it_can_set_and_get_clusters_icon_option()
    {
        $this->setClustersIcon(self::STRING);
        $this->getClustersIcon()->shouldReturn(self::STRING);
    }

    public function it_can_set_and_get_clusters_grid_option()
    {
        $this->setClustersGrid(self::INTEGER);
        $this->getClustersGrid()->shouldReturn(self::INTEGER);
    }

    public function it_can_set_and_get_clusters_zoom_option()
    {
        $this->setClustersZoom(self::INTEGER);
        $this->getClustersZoom()->shouldReturn(self::INTEGER);
    }

    public function it_can_set_and_get_clusters_center_option()
    {
        $this->setClustersCenter(self::BOOLEAN);
        $this->getClustersCenter()->shouldReturn(self::BOOLEAN);
    }

    public function it_can_set_and_get_clusters_size_option()
    {
        $this->setClustersSize(self::INTEGER);
        $this->getClustersSize()->shouldReturn(self::INTEGER);
    }

    public function it_can_create_a_map()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_a_marker()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->marker(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_an_information_window()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->informationWindow(self::INTEGER, self::INTEGER, self::STRING)->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_a_polyline()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->polyline([['latitude' => self::INTEGER, 'longitude' => self::INTEGER]])->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_a_polygon()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->polygon([
            ['latitude' => self::INTEGER, 'longitude' => self::INTEGER],
            ['latitude' => self::INTEGER, 'longitude' => self::INTEGER]
        ])->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_a_rectangle()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->rectangle([
                ['latitude' => self::INTEGER, 'longitude' => self::INTEGER],
                ['latitude' => self::INTEGER, 'longitude' => self::INTEGER]
            ])->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_map_with_a_circle()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->circle([['latitude' => self::INTEGER, 'longitude' => self::INTEGER]])->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_a_streetview_map()
    {
        $this->streetview(self::INTEGER, self::INTEGER, self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->render()->shouldReturn(self::STRING);
    }

    public function it_can_create_javascript_inclusions()
    {
        $this->map(self::INTEGER, self::INTEGER)->shouldReturn($this);
        $this->renderJavascript()->shouldReturn(self::STRING);
    }
}


text:
class CreateArenas < ActiveRecord::Migration
  def change
    create_table :arenas do |t|
      t.string :name
      t.string :address
      t.string :number
      t.text :description
      t.integer :size
      t.decimal :price

      t.timestamps null: false
    end
  end
end


text:
"""Create the initial SQLite DB for android"""

import logging
import os
import sqlite3

from typing import Dict

LOGGER = logging.getLogger(__name__)

TABLE_CREATE_COMMANDS = [
    # Standard android tables
    """CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US')""",
    """INSERT INTO "android_metadata" VALUES ('en_US')""",

    # Lectures
    """CREATE TABLE "lectures" (
    _id INTEGER PRIMARY KEY,
    name TEXT,
    version VARCHAR(10),
    language VARCHAR(6),
    level INTEGER,
    link_name TEXT,
    link_url TEXT,
    instructions VARCHAR(2000)
    )""",

    # Exercises
    """CREATE TABLE "exercises" (
    _id INTEGER PRIMARY KEY,
    name TEXT,
    question_type TEXT,
    lecture INTEGER,
    question_sound TEXT,
    question_image TEXT,
    text TEXT)""",

    # Choice
    """CREATE TABLE "choices" (
    _id INTEGER PRIMARY KEY,
    answer_type TEXT,
    exercise INTEGER,
    correct BOOLEAN,
    sound TEXT,
    image TEXT,
    text TEXT
    )"""

]

class AndroidSqlite():
    """Create a sqlite database for the exercises"""
    # We could probably use Django object mapper to do this with less code

    def __init__(self, fname: str) -> None:
        if os.path.isfile(fname):
            os.unlink(fname)

        self.conn = sqlite3.connect(fname)
        cursor = self.conn.cursor()

        for tcc in TABLE_CREATE_COMMANDS:
            LOGGER.debug("Run sqllite: %s", tcc)
            cursor.execute(tcc)

        self.conn.commit()
        self.lecture_primary_key = 0
        self.exercise_primary_key = 0
        self.choice_primary_key = 0

    def __del__(self) -> None:
        """Close connection to make sure everything is written to db."""
        self.conn.close()

    def insert_lecture(self, doc: Dict, ldoc: Dict, lang) -> int:
        """Insert lecture info into db"""
        self.lecture_primary_key += 1
        cursor = self.conn.cursor()
        cmd = """INSERT INTO lectures VALUES(%d, "%s", "%s", "%s", "%s", "%s", "%s", "%s")""" % (
            self.lecture_primary_key, ldoc["Title"], doc["Version"],
            lang, doc["Level"],
            ldoc["Outside_information"]["name"] if "Outside_information" in ldoc else "",
            ldoc["Outside_information"]["link"] if "Outside_information" in ldoc else "",
            ldoc["Instructions"] if "Instuctions" in ldoc else "")
        LOGGER.debug("%s", cmd)
        cursor.execute(cmd)
        self.conn.commit()
        return self.lecture_primary_key

    def insert_exercise(self, lec_key: int, lang: str, exer: Dict) -> int:
        """Insert exercise into db"""
        self.exercise_primary_key += 1
        cursor = self.conn.cursor()
        cmd = """INSERT INTO exercises VALUES (%d, "%s", "%s", %d, "%s", "%s", "%s")""" %(
            self.exercise_primary_key, exer["name"][lang],
            exer["question_type"], lec_key,
            exer["mp3"], exer["png"],
            exer["text"][lang] if "text" in exer and exer["text"] else "")
        LOGGER.debug("%s", cmd)
        cursor.execute(cmd)
        self.conn.commit()
        return self.exercise_primary_key

    def insert_choice(self, exer_key: int, answer_type: str, lang: str, alt: Dict) -> int:
        """Insert choice into db"""
        self.choice_primary_key += 1
        cursor = self.conn.cursor()
        cmd = """INSERT INTO choices VALUES """\
              """(%d, "%s", %d, %d, "%s", "%s", "%s")""" %(
                  self.choice_primary_key, answer_type,
                  exer_key, 1 if alt["correct"] else 0,
                  alt["mp3"], alt["png"],
                  alt["text"][lang] if "text" in alt and alt["text"] else "")
        LOGGER.debug("%s", cmd)
        cursor.execute(cmd)
        self.conn.commit()
        return self.choice_primary_key