<?php

/*
 * Service_WTF_Test
 *
 * Version 0.4
 *
 * A PHP REST client for the Web Testing Framework (WTF) Testing Service API
 * Currently only supports GTmetrix. See:
 *
 *     http://gtmetrix.com/api/
 *
 * for more information on the API and how to contribute to the web testing
 * framework!
 *
 * Copyright Gossamer Threads Inc. (http://gt.net/)
 * License: http://opensource.org/licenses/GPL-2.0 GPL 2
 *
 * This software is free software distributed under the terms of the GNU
 * General Public License 2.0.
 *
 * Changelog:
 *
 *     0.4
 *         - fixed download_resources bug
 *         - added $append parameter to download_resources
 *         - some refactoring for consistency
 *
 *     0.3
 *         - added download_resources method
 *
 *     June 27, 2012
 *         - polling frequency in get_results() made less frantic
 *         - version changed to 0.2
 *
 *     June 5, 2012
 *         - status method added
 *         - user_agent property updated
 *
 *     January 23, 2012
 *         - Initial release
 */

class Services_WTF_Test {
    const api_url = 'https://gtmetrix.com/api/0.1';
    private $username = '';
    private $password = '';
    private $user_agent = 'Services_WTF_Test_php/0.4 (+http://gtmetrix.com/api/)';
    protected $test_id = '';
    protected $result = array( );
    protected $error = '';

    /**
     * Constructor
     *
     * $username    string  username to log in with
     * $password    string  password/apikey to log in with
     */
    public function __construct( $username = '', $password = '' ) {
        $this->username = $username;
        $this->password = $password;
    }

    public function api_username( $username ) {
        $this->username = $username;
    }

    public function api_password( $password ) {
        $this->password = $password;
    }

    /**
     * user_agent()
     *
     * $user_agent    string   in the form of "product name/version number" used to identify the application to the API
     *
     * Optional, defaults to "Services_WTF_Test_php/0.1 (+http://gtmetrix.com/api/)"
     */
    public function user_agent( $user_agent ) {
        $this->user_agent = $user_agent;
    }

    /**
     * query()
     *
     * Makes curl connection to API
     *
     * $command string                          command to send
     * $req     string  GET|POST|DELETE         request to send API
     * $params  array                           POST data if request is POST
     *
     * returns raw http data (JSON object in most API cases) on success, false otherwise
     */
    protected function query( $command, $req = 'GET', $params = '' ) {
        $ch = curl_init();

        if ( substr( $command, 0, strlen( self::api_url ) - 1 ) == self::api_url ) {
            $URL = $command;
        } else {
            $URL = self::api_url . '/' . $command;
        }

        curl_setopt( $ch, CURLOPT_URL, $URL );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
        curl_setopt( $ch, CURLOPT_USERAGENT, $this->user_agent );
        curl_setopt( $ch, CURLOPT_USERPWD, $this->username . ":" . $this->password );
        curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req );
        // CURLOPT_SSL_VERIFYPEER turned off to avoid failure when cURL has no CA cert bundle: see http://curl.haxx.se/docs/sslcerts.html
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, FALSE );

        if ( $req == 'POST' )
            curl_setopt( $ch, CURLOPT_POSTFIELDS, $params );

        $results = curl_exec( $ch );
        if ( $results === false )
            $this->error = curl_error( $ch );

        curl_close( $ch );

        return $results;
    }

    protected function checkid() {
        if ( empty( $this->test_id ) ) {
            $this->error = 'No test_id! Please start a new test or load an existing test first.';
            return false;
        }

        return true;
    }

    /**
     * error()
     *
     * Returns error message
     */
    public function error() {
        return $this->error;
    }

    /**
     * test()
     *
     * Sends new test to GTMetrix API
     *
     * $data    array   array containing parameters to send API
     *
     * returns the test_id on success, false otherwise;
     */
    public function test( $data ) {

        if ( empty( $data ) ) {
            $this->error = 'Parameters need to be set to start a new test!';
            return false;
        }

        if ( !isset( $data['url'] ) OR empty( $data['url'] ) ) {
            $this->error = 'No URL given!';
            return false;
        }

        // check URL
        if ( !preg_match( '@^https?://@', $data['url'] ) ) {
            $this->error = 'Bad URL.';
            return false;
        }

        if ( !empty( $this->result ) )
            $this->result = array( );

        $data = http_build_query( $data );

        $result = $this->query( 'test', 'POST', $data );

        if ( $result != false ) {
            $result = json_decode( $result, true );
            if ( empty( $result['error'] ) ) {
                $this->test_id = $result['test_id'];

                if ( isset( $result['state'] ) AND !empty( $result['state'] ) )
                    $this->result = $result;

                return $this->test_id;
            } else {
                $this->error = $result['error'];
            }
        }

        return false;
    }

    /**
     * load()
     *
     * Query an existing test from GTMetrix API
     *
     * $test_id  string  The existing test's test ID
     *
     * test_id must be valid, or else all query methods will fail
     */
    public function load( $test_id ) {
        $this->test_id = $test_id;

        if ( !empty( $this->result ) )
            $this->result = array( );
    }

    /**
     * delete()
     *
     * Delete the test from the GTMetrix database
     *
     * Precondition: member test_id is not empty
     *
     * returns message on success, false otherwise
     */
    public function delete() {
        if ( !$this->checkid() )
            return false;

        $command = "test/" . $this->test_id;

        $result = $this->query( $command, "DELETE" );
        if ( $result != false ) {
            $result = json_decode( $result, true );
            return ($result['message']) ? true : false;
        }

        return false;
    }

    /**
     * get_test_id()
     *
     * Returns the test_id, false if test_id is not set
     */
    public function get_test_id() {
        return ($this->test_id) ? $this->test_id : false;
    }

    /**
     * poll_state()
     *
     * polls the state of the test
     *
     * Precondition: member test_id is not empty
     *
     * The class will save a copy of the state object,
     * which contains information such as the test results and resource urls (or nothing if an error occured)
     * so that additional queries to the API is not required.
     *
     * returns true on successful poll, or false on network error or no test_id
     */
    public function poll_state() {
        if ( !$this->checkid() )
            return false;

        if ( !empty( $this->result ) ) {
            if ( $this->result['state'] == "completed" )
                return true;
        }

        $command = "test/" . $this->test_id;

        $result = $this->query( $command );
        if ( $result != false ) {
            $result = json_decode( $result, true );

            if ( !empty( $result['error'] ) AND !isset( $result['state'] ) ) {
                $this->error = $result['error'];
                return false;
            }

            $this->result = $result;
            if ( $result['state'] == 'error' )
                $this->error = $result['error'];

            return true;
        }

        return false;
    }

    /**
     * state()
     *
     * Returns the state of the test (queued, started, completed, error)
     *
     * Precondition: member test_id is not empty
     *
     * returns the state of the test, or false on networking error
     */
    public function state() {
        if ( !$this->checkid() )
            return false;

        if ( empty( $this->result ) )
            return false;

        return $this->result['state'];
    }

    /**
     * completed()
     *
     * returns true if the test is complete, false otherwise
     */
    public function completed() {
        return ($this->state() == 'completed') ? true : false;
    }

    /*
     * get_results()
     *
     * locks and polls API until test results are received
     * waits for 6 seconds before first check, then polls every 2 seconds
     * at the 30 second mark it reduces frequency to 5 seconds
     */

    public function get_results() {
        sleep( 6 );
        $i = 1;
        while ( $this->poll_state() ) {
            if ( $this->state() == 'completed' OR $this->state() == 'error' )
                break;
            sleep( $i++ <= 13 ? 2 : 5  );
        }
    }

    /**
     * locations()
     *
     * Returns a list of GTMetrix server locations accompanied by their location IDs
     * that can be used in newTest() to select a different server location for testing
     *
     * returns the location list in array format, the error message if an error occured,
     * or false if a query error occured.
     */
    public function locations() {
        $result = $this->query( 'locations' );
        if ( $result != false ) {
            $result = json_decode( $result, true );
            if ( empty( $result['error'] ) ) {
                return $result;
            } else {
                $this->error = $result['error'];
            }
        }

        return false;
    }

    /**
     * results()
     *
     * Get test results
     *
     * returns the test results, or false if the test hasn't completed yet
     */
    public function results() {
        if ( !$this->completed() )
            return false;

        return $this->result['results'];
    }

    /**
     * resources()
     *
     * Get test resource URLs
     *
     * returns the test resources, or false if the test hasn't completed yet
     */
    public function resources( $item = 'all' ) {
        if ( !$this->completed() )
            return false;

        return $this->result['resources'];
    }

    /**
     * fetch_resources()
     *
     * Downloads test resources to a specified location
     *
     * $items     string/array        item(s) to download (empty or null will result in all resources downloading)
     * $location string                location to download to
     *
     * returns true if successful, the error message if an error occured
     */
    public function download_resources( $items = null, $location = './', $append_test_id = false ) {

        if ( !$this->completed() )
            return false;

        $resources = $this->result['resources'];
        $resource_types = array(
            'report_pdf' => 'pdf',
            'pagespeed' => 'txt',
            'har' => 'txt',
            'pagespeed_files' => 'tar',
            'yslow' => 'txt',
            'screenshot' => 'jpg',
        );

        if ( !$items or $items == '' ) {
            $items = array_keys( $resource_types );
        }

        if ( !is_array( $items ) ) {
            $items = array( $items );
        }

        if ( !is_writable( $location ) ) {
            $this->error = 'Permission denied in ' . $location;
            return false;
        }

        foreach ( $items as $item ) {

            if ( !array_key_exists( $item, $resources ) ) {
                $this->error = $item . ' does not exist';
                return false;
            }

            $file = fopen( $location . $item . ($append_test_id ? '-' . $this->test_id : '') . '.' . $resource_types[$item], "w" );

            $ch = curl_init();
            curl_setopt( $ch, CURLOPT_URL, $resources[$item] );
            curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
            curl_setopt( $ch, CURLOPT_FILE, $file );
            curl_setopt( $ch, CURLOPT_HEADER, 0 );
            curl_setopt( $ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
            curl_setopt( $ch, CURLOPT_USERAGENT, $this->user_agent );
            curl_setopt( $ch, CURLOPT_USERPWD, $this->username . ":" . $this->password );
            curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, FALSE );

            $results = curl_exec( $ch );
            if ( $results === false )
                $this->error = curl_error( $ch );

            curl_close( $ch );
        }
        return true;
    }

    /**
     * status()
     *
     * Get account status
     *
     * returns credits remaining, and timestamp of next top-up
     */
    public function status() {
        $result = $this->query( 'status' );
        if ( $result != false ) {
            $result = json_decode( $result, true );
            if ( empty( $result['error'] ) ) {
                return $result;
            } else {
                $this->error = $result['error'];
            }
        }
        return false;
    }

}
