<?php

namespace Stuart\Converters;

use Stuart\Job;
use Stuart\Pricing;

class JsonToJob
{
    public static $STUART_DATE_FORMAT = "Y-m-d\TH:i:s.uO";

    /**
     * Converts a JSON formatted Stuart Job into a Job object.
     *
     * @param String $json
     * @return Job
     */
    public static function convert($json)
    {
        $body = json_decode($json);
        $job = new Job();

        $job->setId($body->id);
        $job->setTransportType(isset($body->transport_type) ? $body->transport_type : null);
        $job->setAssignmentCode(isset($body->assignment_code) ? $body->assignment_code : null);
        $job->setStatus($body->status);
        $job->setDistance($body->distance);
        $job->setDuration($body->duration);

        foreach ($body->deliveries as $delivery) {
            $job
                ->link(
                    $job->addPickup(self::fullTextAddress($delivery->pickup->address))
                        ->setPickupAt(\DateTime::createFromFormat(self::$STUART_DATE_FORMAT, $body->pickup_at))
                        ->setComment($delivery->pickup->comment)
                        ->setContactCompany($delivery->pickup->contact->company_name)
                        ->setContactFirstName($delivery->pickup->contact->firstname)
                        ->setContactLastName($delivery->pickup->contact->lastname)
                        ->setContactPhone($delivery->pickup->contact->phone)
                        ->setContactEmail($delivery->pickup->contact->email),
                    $job->addDropOff(self::fullTextAddress($delivery->dropoff->address))
                        ->setDropoffAt(\DateTime::createFromFormat(self::$STUART_DATE_FORMAT, $body->dropoff_at))
                        ->setPackageType($delivery->package_type)
                        ->setPackageDescription($delivery->package_description)
                        ->setClientReference($delivery->client_reference)
                        ->setComment($delivery->dropoff->comment)
                        ->setContactCompany($delivery->dropoff->contact->company_name)
                        ->setContactFirstName($delivery->dropoff->contact->firstname)
                        ->setContactLastName($delivery->dropoff->contact->lastname)
                        ->setContactPhone($delivery->dropoff->contact->phone)
                        ->setContactEmail($delivery->dropoff->contact->email)
                )
                ->setId($delivery->id)
                ->setStatus($delivery->status)
                ->setTrackingUrl($delivery->tracking_url);
        }

        $pricing = new Pricing();
        if (isset($body->pricing)) {
            if (isset($body->pricing->price_tax_included)) {
                $pricing->setPriceTaxIncluded($body->pricing->price_tax_included);
            }
            if (isset($body->pricing->price_tax_excluded)) {
                $pricing->setPriceTaxExcluded($body->pricing->price_tax_excluded);
            }
        }

        $job->setPricing($pricing);

        return $job;
    }

    /**
     * @param $address
     *
     * @return string
     */
    private static function fullTextAddress($address)
    {
        return implode(', ', (array)$address);
    }
}


<?php

namespace Stuart;

use Stuart\Converters\JobToJson;
use Stuart\Converters\JsonToJob;
use Stuart\Converters\JsonToSchedulingSlots;

class Client
{
    private $httpClient;

    public function __construct($httpClient)
    {
        $this->httpClient = $httpClient;
    }

    /**
     * @param Job $job
     *
     * @return bool|\stdClass
     */
    public function validateJob(Job $job)
    {
        $body = JobToJson::convert($job);
        $apiResponse = $this->httpClient->performPost($body, '/v2/jobs/validate');

        if ($apiResponse->success()) {
            return json_decode($apiResponse->getBody())->valid;
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function getSchedulingSlotsAtPickup($city, \DateTime $dateTime)
    {
        return $this->getSchedulingSlots($city, 'pickup', $dateTime);
    }

    public function getSchedulingSlotsAtDropoff($city, \DateTime $dateTime)
    {
        return $this->getSchedulingSlots($city, 'dropoff', $dateTime);
    }

    private function getSchedulingSlots($city, $type, \DateTime $dateTime)
    {
        $apiResponse = $this->httpClient->performGet('/v2/jobs/schedules/' . $city . '/' . $type . '/' . $dateTime->format('Y-m-d'));

        if ($apiResponse->success()) {
            return JsonToSchedulingSlots::convert($apiResponse->getBody());
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function createJob($job)
    {
        $body = JobToJson::convert($job);

        $apiResponse = $this->httpClient->performPost($body, '/v2/jobs');
        if ($apiResponse->success()) {
            return JsonToJob::convert($apiResponse->getBody());
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function getJob($jobId)
    {
        $apiResponse = $this->httpClient->performGet('/v2/jobs/' . $jobId);

        if ($apiResponse->success()) {
            return JsonToJob::convert($apiResponse->getBody());
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function cancelJob($jobId)
    {
        $apiResponse = $this->httpClient->performPost('', '/v2/jobs/' . $jobId . '/cancel');

        if ($apiResponse->success()) {
            return true;
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function cancelDelivery($deliveryId)
    {
        $apiResponse = $this->httpClient->performPost('', '/v2/deliveries/' . $deliveryId . '/cancel');

        if ($apiResponse->success()) {
            return true;
        } else {
            return json_decode($apiResponse->getBody());
        }
    }

    public function getPricing($job)
    {
        $body = JobToJson::convert($job);

        $apiResponse = $this->httpClient->performPost($body, '/v2/jobs/pricing');
        return json_decode($apiResponse->getBody());
    }

    public function getEta($job)
    {
        $body = JobToJson::convert($job);

        $apiResponse = $this->httpClient->performPost($body, '/v2/jobs/eta');
        return json_decode($apiResponse->getBody());
    }

    private function validateAddress($address, $type)
    {
        $query = array(
            'address' => $address,
            'type' => $type
        );

        $apiResponse = $this->httpClient->performGet('/v2/addresses/validate', $query);
        return json_decode($apiResponse->getBody());
    }

    public function validatePickupAddress($address)
    {
        return $this->validateAddress($address, 'picking');
    }

    public function validateDropoffAddress($address)
    {
        return $this->validateAddress($address, 'delivering');
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket\Datagram;

use Icicle\Socket;
use Icicle\Socket\Exception\FailureException;

class DefaultDatagramFactory implements DatagramFactory
{
    /**
     * {@inheritdoc}
     */
    public function create(string $host, int $port = null, array $options = []): Datagram
    {
        $autoClose = (bool) ($options['auto_close'] ?? true);

        $context = [];

        $context['socket'] = [];
        $context['socket']['bindto'] = Socket\makeName($host, $port);

        $context = stream_context_create($context);

        $uri = Socket\makeUri('udp', $host, $port);
        // Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure (checked below).
        $socket = @stream_socket_server($uri, $errno, $errstr, STREAM_SERVER_BIND, $context);

        if (!$socket || $errno) {
            throw new FailureException(
                sprintf('Could not create datagram on %s: Errno: %d; %s', $uri, $errno, $errstr)
            );
        }

        return new BasicDatagram($socket, $autoClose);
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket\Connector;

use Icicle\Awaitable\{Delayed, Exception\TimeoutException};
use Icicle\Loop;
use Icicle\Loop\Watcher\Io;
use Icicle\Exception\InvalidArgumentError;
use Icicle\Socket;
use Icicle\Socket\{NetworkSocket, Exception\FailureException};

class DefaultConnector implements Connector
{
    const DEFAULT_CONNECT_TIMEOUT = 10;
    const DEFAULT_ALLOW_SELF_SIGNED = false;
    const DEFAULT_VERIFY_DEPTH = 10;

    /**
     * {@inheritdoc}
     */
    public function connect(string $ip, int $port = null, array $options = []): \Generator
    {
        $protocol = (string) ($options['protocol'] ?? (null === $port ? 'unix' : 'tcp'));
        $autoClose = (bool) ($options['auto_close'] ?? true);
        $allowSelfSigned = (bool) ($options['allow_self_signed'] ?? self::DEFAULT_ALLOW_SELF_SIGNED);
        $timeout = (float) ($options['timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT);
        $verifyDepth = (int) ($options['verify_depth'] ?? self::DEFAULT_VERIFY_DEPTH);
        $cafile = (string) ($options['cafile'] ?? '');
        $name = (string) ($options['name'] ?? '');
        $cn = (string) ($options['cn'] ?? $name);

        $context = [];

        $context['socket'] = [
            'connect' => Socket\makeName($ip, $port),
        ];

        $context['ssl'] = [
            'capture_peer_cert' => true,
            'capture_peer_chain' => true,
            'capture_peer_cert_chain' => true,
            'verify_peer' => true,
            'verify_peer_name' => true,
            'allow_self_signed' => $allowSelfSigned,
            'verify_depth' => $verifyDepth,
            'CN_match' => $cn,
            'SNI_enabled' => true,
            'SNI_server_name' => $name,
            'peer_name' => $name,
            'disable_compression' => true,
            'honor_cipher_order' => true,
        ];

        if ('' !== $cafile) {
            if (!file_exists($cafile)) {
                throw new InvalidArgumentError('No file exists at path given for cafile.');
            }
            $context['ssl']['cafile'] = $cafile;
        }

        $context = stream_context_create($context);

        $uri = Socket\makeUri($protocol, $ip, $port);
        // Error reporting suppressed since stream_socket_client() emits an E_WARNING on failure (checked below).
        $socket = @stream_socket_client(
            $uri,
            $errno,
            $errstr,
            null, // Timeout does not apply for async connect. Timeout enforced by await below.
            STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT,
            $context
        );

        if (!$socket || $errno) {
            throw new FailureException(
                sprintf('Could not connect to %s; Errno: %d; %s', $uri, $errno, $errstr)
            );
        }

        $delayed = new Delayed();

        $await = Loop\await($socket, function ($resource, bool $expired, Io $await) use ($delayed, $autoClose) {
            $await->free();

            if ($expired) {
                $delayed->reject(new TimeoutException('Connection attempt timed out.'));
                return;
            }

            $delayed->resolve(new NetworkSocket($resource, $autoClose));
        });

        $await->listen($timeout);

        try {
            return yield $delayed;
        } finally {
            $await->free();
        }
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket;

use Icicle\Socket\Exception\FailureException;
use Icicle\Stream\Pipe\DuplexPipe;

class NetworkSocket extends DuplexPipe implements Socket
{
    /**
     * @var int
     */
    private $crypto = 0;

    /**
     * @var string
     */
    private $remoteAddress;

    /**
     * @var int
     */
    private $remotePort;

    /**
     * @var string
     */
    private $localAddress;

    /**
     * @var int
     */
    private $localPort;

    /**
     * @param resource $socket Stream socket resource.
     * @param bool $autoClose True to close the resource on destruct, false to leave it open.
     */
    public function __construct($socket, bool $autoClose = true)
    {
        parent::__construct($socket, $autoClose);

        try {
            list($this->remoteAddress, $this->remotePort) = getName($socket, true);
            list($this->localAddress, $this->localPort) = getName($socket, false);
        } catch (FailureException $exception) {
            $this->close();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function enableCrypto(int $method, float $timeout = 0): \Generator
    {
        $resource = $this->getResource();

        if ($method & 1 || 0 === $method) {
            yield from $this->await($timeout);
        } else {
            yield from $this->poll($timeout);

            $raw = stream_socket_recvfrom($resource, 11, STREAM_PEEK);
            if (11 > strlen($raw)) {
                throw new FailureException('Failed to read crypto handshake.');
            }

            $data = unpack('ctype/nversion/nlength/Nembed/nmax-version', $raw);
            if (0x16 !== $data['type']) {
                throw new FailureException('Invalid crypto handshake.');
            }

            $version = $this->selectCryptoVersion($data['max-version']);
            if ($method & $version) { // Check if version was available in $method.
                $method = $version;
            }
        }

        do {
            // Error reporting suppressed since stream_socket_enable_crypto() emits E_WARNING on failure.
            $result = @stream_socket_enable_crypto($resource, (bool) $method, $method);
        } while (0 === $result && !yield from $this->poll($timeout));

        if ($result) {
            $this->crypto = $method;
            return;
        }

        $message = 'Failed to enable crypto.';
        if ($error = error_get_last()) {
            $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']);
        }
        throw new FailureException($message);
    }

    /**
     * Returns highest supported crypto method constant based on protocol version identifier.
     *
     * @param int $version
     *
     * @return int
     */
    private function selectCryptoVersion($version)
    {
        switch ($version) {
            case 0x300: return STREAM_CRYPTO_METHOD_SSLv3_SERVER;
            case 0x301: return STREAM_CRYPTO_METHOD_TLSv1_0_SERVER;
            case 0x302: return STREAM_CRYPTO_METHOD_TLSv1_1_SERVER;
            default:    return STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
        }
    }

    /**
     * @return bool
     */
    public function isCryptoEnabled(): bool
    {
        return 0 !== $this->crypto;
    }

    /**
     * {@inheritdoc}
     */
    public function getRemoteAddress(): string
    {
        return $this->remoteAddress;
    }

    /**
     * {@inheritdoc}
     */
    public function getRemotePort(): int
    {
        return $this->remotePort;
    }

    /**
     * {@inheritdoc}
     */
    public function getLocalAddress(): string
    {
        return $this->localAddress;
    }

    /**
     * {@inheritdoc}
     */
    public function getLocalPort(): int
    {
        return $this->localPort;
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket\Server;

use Icicle\Awaitable\Delayed;
use Icicle\Loop;
use Icicle\Loop\Watcher\Io;
use Icicle\Socket;
use Icicle\Socket\{Socket as ClientSocket, NetworkSocket};
use Icicle\Socket\Exception\{ClosedException, FailureException, UnavailableException};
use Icicle\Stream\StreamResource;

class BasicServer extends StreamResource implements Server
{
    /**
     * Listening hostname or IP address.
     *
     * @var int
     */
    private $address;

    /**
     * Listening port.
     *
     * @var int
     */
    private $port;

    /**
     * @var \SplQueue
     */
    private $queue;

    /**
     * @var \Icicle\Loop\Watcher\Io
     */
    private $poll;

    /**
     * @var \Closure
     */
    private $onCancelled;

    /**
     * @param resource $socket
     * @param bool $autoClose True to close the resource on destruct, false to leave it open.
     */
    public function __construct($socket, bool $autoClose = true)
    {
        parent::__construct($socket, $autoClose);

        $this->queue = new \SplQueue();

        $this->poll = $this->createPoll($socket, $this->queue);

        $this->onCancelled = function () {
            $this->poll->cancel();
            $this->queue->shift();
        };

        try {
            list($this->address, $this->port) = Socket\getName($socket, false);
        } catch (FailureException $exception) {
            $this->close();
        }
    }

    /**
     * Frees resources associated with this object from the loop.
     */
    public function __destruct()
    {
        parent::__destruct();
        $this->free();
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        parent::close();
        $this->free();
    }

    /**
     * Frees resources associated with the server and closes the server.
     *
     * @param \Throwable $exception Reason for closing the server.
     */
    protected function free(\Throwable $exception = null)
    {
        $this->poll->free();

        while (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->shift();
            $delayed->reject($exception ?: new ClosedException('The server was unexpectedly closed.'));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function accept(bool $autoClose = true): \Generator
    {
        while (!$this->queue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->queue->bottom();
            yield $delayed; // Wait for previous accept to complete.
        }

        if (!$this->isOpen()) {
            throw new UnavailableException('The server has been closed.');
        }

        // Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
        $socket = @stream_socket_accept($this->getResource(), 0); // Timeout of 0 to be non-blocking.

        if ($socket) {
            return $this->createSocket($socket, $autoClose);
        }

        $this->queue->push($delayed = new Delayed($this->onCancelled));
        $this->poll->listen();

        return $this->createSocket(yield $delayed, $autoClose);
    }

    /**
     * {@inheritdoc}
     */
    public function getAddress(): string
    {
        return $this->address;
    }

    /**
     * {@inheritdoc}
     */
    public function getPort(): int
    {
        return $this->port;
    }

    /**
     * {@inheritdoc}
     */
    public function rebind()
    {
        $pending = $this->poll->isPending();
        $this->poll->free();

        $this->poll = $this->createPoll($this->getResource(), $this->queue);

        if ($pending) {
            $this->poll->listen();
        }
    }

    /**
     * @param resource $socket Stream socket resource.
     * @param bool $autoClose
     *
     * @return \Icicle\Socket\Socket
     */
    protected function createSocket($socket, bool $autoClose = true): ClientSocket
    {
        return new NetworkSocket($socket, $autoClose);
    }

    /**
     * @param resource $resource
     * @param \SplQueue $queue
     *
     * @return \Icicle\Loop\Watcher\Io
     */
    private function createPoll($resource, \SplQueue $queue): Io
    {
        return Loop\poll($resource, static function ($resource, bool $expired, Io $poll) use ($queue) {
            // Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
            $socket = @stream_socket_accept($resource, 0); // Timeout of 0 to be non-blocking.

            // Having difficulty finding a test to cover this scenario, but it has been seen in production.
            if (!$socket) {
                $poll->listen(); // Accept failed, let's go around again.
                return;
            }

            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $queue->shift();
            $delayed->resolve($socket);
        });
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket\Datagram;

use Icicle\Awaitable\{Delayed, Exception\TimeoutException};
use Icicle\Exception\InvalidArgumentError;
use Icicle\Loop;
use Icicle\Loop\Watcher\Io;
use Icicle\Socket;
use Icicle\Socket\Exception\{ClosedException, FailureException, UnavailableException};
use Icicle\Stream\StreamResource;
use Throwable;

class BasicDatagram extends StreamResource implements Datagram
{
    const MAX_PACKET_SIZE = 512;

    /**
     * @var string
     */
    private $address;

    /**
     * @var int
     */
    private $port;

    /**
     * @var \Icicle\Loop\Watcher\Io
     */
    private $poll;

    /**
     * @var \Icicle\Loop\Watcher\Io
     */
    private $await;

    /**
     * @var \SplQueue
     */
    private $readQueue;

    /**
     * @var \SplQueue
     */
    private $writeQueue;

    /**
     * @var \Closure
     */
    private $onReceiveCancelled;

    /**
     * @var \Closure
     */
    private $onSendCancelled;

    /**
     * @var int
     */
    private $length = 0;

    /**
     * @param resource $socket
     * @param bool $autoClose True to close the resource on destruct, false to leave it open.
     */
    public function __construct($socket, bool $autoClose = true)
    {
        parent::__construct($socket, $autoClose);

        stream_set_read_buffer($socket, 0);
        stream_set_write_buffer($socket, 0);
        stream_set_chunk_size($socket, self::MAX_PACKET_SIZE);

        $this->readQueue = new \SplQueue();
        $this->writeQueue = new \SplQueue();

        $this->poll = $this->createPoll($socket, $this->readQueue);
        $this->await = $this->createAwait($socket, $this->writeQueue);

        $this->onReceiveCancelled = function () {
            $this->poll->cancel();
            $this->readQueue->shift();
        };

        $this->onSendCancelled = function (\Exception $exception) {
            $this->free($exception);
        };

        try {
            list($this->address, $this->port) = Socket\getName($socket, false);
        } catch (FailureException $exception) {
            $this->close();
        }
    }

    /**
     * Frees resources associated with this object from the loop.
     */
    public function __destruct()
    {
        parent::__destruct();
        $this->free();
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        parent::close();
        $this->free();
    }

    /**
     * Frees resources associated with the datagram and closes the datagram.
     *
     * @param \Throwable|null $exception Reason for closing the datagram.
     */
    protected function free(Throwable $exception = null)
    {
        $this->poll->free();

        if (null !== $this->await) {
            $this->await->free();
        }

        while (!$this->readQueue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->readQueue->shift();
            $delayed->resolve();
        }

        while (!$this->writeQueue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            list( , , , $delayed) = $this->writeQueue->shift();
            $delayed->reject(
                $exception = $exception ?: new ClosedException('The datagram was unexpectedly closed.')
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getAddress(): string
    {
        return $this->address;
    }

    /**
     * {@inheritdoc}
     */
    public function getPort(): int
    {
        return $this->port;
    }

    /**
     * {@inheritdoc}
     */
    public function receive(int $length = 0, float $timeout = 0): \Generator
    {
        while (!$this->readQueue->isEmpty()) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $this->readQueue->bottom();
            yield $delayed; // Wait for previous read to complete.
        }

        if (!$this->isOpen()) {
            throw new UnavailableException('The datagram is no longer readable.');
        }

        $this->length = (int) $length;
        if (0 > $this->length) {
            throw new InvalidArgumentError('The length must be a non-negative integer.');
        } elseif (0 === $this->length) {
            $this->length = self::MAX_PACKET_SIZE;
        }

        $this->readQueue->push($delayed = new Delayed($this->onReceiveCancelled));
        $this->poll->listen($timeout);

        return yield $delayed;
    }

    /**
     * {@inheritdoc}
     */
    public function send(string $address, int $port, string $data): \Generator
    {
        if (!$this->isOpen()) {
            throw new UnavailableException('The datagram is no longer writable.');
        }

        $data = (string) $data;
        $length = strlen($data);
        $written = 0;
        $peer = Socket\makeName($address, $port);

        if ($this->writeQueue->isEmpty()) {
            if (0 === $length) {
                return $written;
            }

            $written = stream_socket_sendto($this->getResource(), substr($data, 0, self::MAX_PACKET_SIZE), 0, $peer);

            // Having difficulty finding a test to cover this scenario, but the check seems appropriate.
            if (false === $written || -1 === $written) {
                $message = 'Failed to write to datagram.';
                if ($error = error_get_last()) {
                    $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']);
                }
                throw new FailureException($message);
            }

            if ($length <= $written) {
                return $written;
            }

            $data = substr($data, $written);
        }

        if (null === $this->await) {
            $this->await = $this->createAwait($this->getResource(), $this->writeQueue);
            $this->await->listen();
        } elseif (!$this->await->isPending()) {
            $this->await->listen();
        }

        $delayed = new Delayed($this->onSendCancelled);
        $this->writeQueue->push([$data, $written, $peer, $delayed]);

        try {
            return yield $delayed;
        } catch (Throwable $exception) {
            $this->free($exception);
            throw $exception;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param float|int $timeout
     */
    public function rebind(float $timeout = 0)
    {
        $pending = $this->poll->isPending();
        $this->poll->free();

        $this->poll = $this->createPoll($this->getResource(), $this->readQueue);

        if ($pending) {
            $this->poll->listen($timeout);
        }

        if (null !== $this->await) {
            $pending = $this->await->isPending();
            $this->await->free();

            $this->await = $this->createAwait($this->getResource(), $this->writeQueue);

            if ($pending) {
                $this->await->listen();
            }
        }
    }

    /**
     * @param resource $resource
     * @param \SplQueue $readQueue
     *
     * @return \Icicle\Loop\Watcher\Io
     */
    private function createPoll($resource, \SplQueue $readQueue): Io
    {
        $length = &$this->length;

        return Loop\poll($resource, static function ($resource, bool $expired) use (&$length, $readQueue) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            $delayed = $readQueue->shift();

            try {
                if ($expired) {
                    throw new TimeoutException('The datagram timed out.');
                }

                $data = stream_socket_recvfrom($resource, $length, 0, $peer);

                // Having difficulty finding a test to cover this scenario, but the check seems appropriate.
                if (false === $data) { // Reading failed, so close datagram.
                    $message = 'Failed to read from datagram.';
                    if ($error = error_get_last()) {
                        $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']);
                    }
                    throw new FailureException($message);
                }

                list($address, $port) = Socket\parseName($peer);

                $result = [$address, $port, $data];

                $delayed->resolve($result);
            } catch (Throwable $exception) {
                $delayed->reject($exception);
            }
        });
    }

    /**
     * @param resource $resource
     * @param \SplQueue $writeQueue
     *
     * @return \Icicle\Loop\Watcher\Io
     */
    private function createAwait($resource, \SplQueue $writeQueue): Io
    {
        return Loop\await($resource, static function ($resource, bool $expired, Io $await) use ($writeQueue) {
            /** @var \Icicle\Awaitable\Delayed $delayed */
            list($data, $previous, $peer, $delayed) = $writeQueue->shift();

            $length = strlen($data);

            if (0 === $length) {
                $delayed->resolve($previous);
            } else {
                $written = stream_socket_sendto($resource, substr($data, 0, self::MAX_PACKET_SIZE), 0, $peer);

                // Having difficulty finding a test to cover this scenario, but the check seems appropriate.
                if (false === $written || -1 === $written || 0 === $written) {
                    $message = 'Failed to write to datagram.';
                    if ($error = error_get_last()) {
                        $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']);
                    }
                    $delayed->reject(new FailureException($message));
                    return;
                }

                if ($length <= $written) {
                    $delayed->resolve($written + $previous);
                } else {
                    $data = substr($data, $written);
                    $written += $previous;
                    $writeQueue->unshift([$data, $written, $peer, $delayed]);
                }
            }

            if (!$writeQueue->isEmpty()) {
                $await->listen();
            }
        });
    }
}


<?php

/*
 * This file is part of the socket package for Icicle, a library for writing asynchronous code in PHP.
 *
 * @copyright 2014-2015 Aaron Piotrowski. All rights reserved.
 * @license MIT See the LICENSE file that was distributed with this source code for more information.
 */

namespace Icicle\Socket\Server;

use Icicle\Exception\InvalidArgumentError;
use Icicle\Socket;
use Icicle\Socket\Exception\FailureException;

class DefaultServerFactory implements ServerFactory
{
    const DEFAULT_BACKLOG = 128;

    // Verify peer should normally be off on the server side.
    const DEFAULT_VERIFY_PEER = false;
    const DEFAULT_ALLOW_SELF_SIGNED = false;
    const DEFAULT_VERIFY_DEPTH = 10;

    /**
     * {@inheritdoc}
     */
    public function create(string $host, int $port = null, array $options = []): Server
    {
        $protocol = (string) ($options['protocol'] ?? (null === $port ? 'unix' : 'tcp'));
        $autoClose = (bool) ($options['auto_close'] ?? true);
        $queue = (int) ($options['backlog'] ?? (defined('SOMAXCONN') ? SOMAXCONN : self::DEFAULT_BACKLOG));
        $pem = (string) ($options['pem'] ?? '');
        $passphrase = ($options['passphrase'] ?? '');
        $name = (string) ($options['name'] ?? '');

        $verify = (string) ($options['verify_peer'] ?? self::DEFAULT_VERIFY_PEER);
        $allowSelfSigned = (bool) ($options['allow_self_signed'] ?? self::DEFAULT_ALLOW_SELF_SIGNED);
        $verifyDepth = (int) ($options['verify_depth'] ?? self::DEFAULT_VERIFY_DEPTH);

        $context = [];

        $context['socket'] = [
            'bindto' => Socket\makeName($host, $port),
            'backlog' => $queue,
            'ipv6_v6only' => true,
            'so_reuseaddr' => (bool) ($options['reuseaddr'] ?? false),
            'so_reuseport' => (bool) ($options['reuseport'] ?? false),
        ];

        if ('' !== $pem) {
            if (!file_exists($pem)) {
                throw new InvalidArgumentError('No file found at given PEM path.');
            }

            $context['ssl'] = [
                'verify_peer' => $verify,
                'verify_peer_name' => $verify,
                'allow_self_signed' => $allowSelfSigned,
                'verify_depth' => $verifyDepth,
                'local_cert' => $pem,
                'disable_compression' => true,
                'SNI_enabled' => true,
                'SNI_server_name' => $name,
                'peer_name' => $name,
            ];

            if ('' !== $passphrase) {
                $context['ssl']['passphrase'] = $passphrase;
            }
        }

        $context = stream_context_create($context);

        $uri = Socket\makeUri($protocol, $host, $port);
        // Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure (checked below).
        $socket = @stream_socket_server($uri, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);

        if (!$socket || $errno) {
            throw new FailureException(sprintf('Could not create server %s: Errno: %d; %s', $uri, $errno, $errstr));
        }

        return new BasicServer($socket, $autoClose);
    }
}


<?php

/**
 * An API for creating a Google chart in a dashboard panel
 *
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 * @package Dashbaord
 */
class DashboardChart extends ViewableData {

	/**
	 * @var int A count of the instances, used to create a unique ID for the chart
	 */
	private static $instances = 0;

	/**
	 * @var array The chart data, in x/y pairs. The Y value must be an integer
	 */
	protected $chartData = array ();

	/**
	 * @var int The number of points between each text label on the X axis
	 */
	public $TextInterval = 5;

	/**
	 * @var int The height of the chart in pixels
	 */
	public $Height = 200;

	/**
	 * @var int The size of the circle on each data point, in pixels
	 */
	public $PointSize = 5;

	/**
	 * @var int The font size on the chart
	 */
	public $FontSize = 10;

	/**
	 * @var string The position of the text on the chart
	 */
	public $TextPosition = 'in';

	/**
	 * Creates a new instance of a DashboardChart
	 *
	 * @param string The title of the chart
	 * @param string The label of the X axis
	 * @param string The label for the Y axis
	 * @param array The chart data, in x/y pairs
	 * @return DashboardChart
	 */
	public static function create($title = null, $x_label = null, $y_label = null, $chartData = array ()) {
		self::$instances++;
		return new DashboardChart($title, $x_label, $y_label, $chartData);
	}

	/**
	 * Constructor for the DashboardChart
	 *
	 * @param string The title of the chart
	 * @param string The label of the X axis
	 * @param string The label for the Y axis
	 * @param array The chart data, in x/y pairs
	 */
	public function __construct($title = null, $x_label = null, $y_label = null, $chartData = array ()) {
		if(!is_array($chartData)) {
			user_error("DashboardChart: \$chartData must be an array", E_USER_ERROR);
		}

		$this->chartData = $chartData;
		$this->Title = $title;
		$this->YAxisLabel = $y_label;
		$this->XAxisLabel = $x_label;
	}

	/**
	 * The ID of the chart. Javascript needs to target a specific element
	 *
	 * @return string
	 */
	public function getChartID() {
		return "dashboard-chart-".self::$instances;
	}

	/**
	 * Gets a list of x/y pairs for the template
	 *
	 * @return ArrayData
	 */
	public function getChartData() {
		$list = ArrayList::create(array());
		foreach($this->chartData as $x => $y) {
			$list->push(ArrayData::create(array(
				'XValue' => $x,
				'YValue' => $y
			)));
		}
		return $list;
	}

	/**
	 * Adds a single data point to the chart
	 *
	 * @param string The X value
	 * @param int The Y value
	 */
	public function addData($x, $y) {
		$this->chartData[$x] = $y;
	}

	/**
	 * Sets the chart data, in x/y pairs
	 *
	 * @param array The chart data
	 */
	public function setData($data) {
		$this->chartData = $data;
	}

	/**
	 * Renders the chart and loads the dependencies
	 *
	 * @return SSViewer
	 */
	public function forTemplate() {
		Requirements::javascript("dashboard/javascript/thirdparty/google_jsapi_visualization.js");
		Requirements::javascript("dashboard/javascript/dashboard-chart.js");
		return $this->renderWith('DashboardChart');
	}

}


<?php

/**
 * A custom FormField object used to manage has_many relations to a DashboardPanel.
 *
 * Note: All has_many relations must be descendants of {@link DashboardPanelDataObject}
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */
class DashboardHasManyRelationEditor extends FormField {

	private static $allowed_actions = array(
		"handleItem"
	);

	private static $url_handlers = array (
		'item/$ID' => 'handleItem',
		'$Action!' => '$Action',
	);

	/**
	 * @var DashboardPanel The {@link DashboardPanel} that owns this editor
	 */
	protected $controller;

	/**
	 * @var string The name of the relationship that is managed by this editor
	 */
	protected $relationName;

	/**
	 * @var string The class of the related object
	 */
	protected $relationClass;

	/**
	 * @var DataList The current list of records in the relation
	 */
	protected $records;

	/**
	 * @var string The template that renders the editor
	 */
	protected $template = "DashboardHasManyRelationEditor";

	/**
	 * The contructor for the editor. Sets member properties and checks for major errors.
	 *
	 * @param DashboardPanel The owner of the editor
	 * @param string The name of the relation managed by the editor
	 * @param string The class of the related object managed by the editor
	 * @param string The title (label) of the editor
	 */
	public function __construct($controller, $relationName, $relationClass, $title = null) {
		$this->controller = $controller;
		$this->relationName = $relationName;
		$this->relationClass = $relationClass;
		$this->title = $title;

		if(!$this->controller instanceof DashboardPanel) {
			user_error("DashboardHasManyRelationEditor must be passed an instance of DashboardPanel", E_USER_ERROR);
		}
		if(!$this->controller->has_many($this->relationName)) {
			user_error("DashboardHasManyRelationEditor must be passed a valid has_many relation for the panel. $relationName is not in the has_many array.", E_USER_ERROR);
		}
		if(!is_subclass_of($relationClass, "DashboardPanelDataObject")) {
			user_error("DashbordHasManyRelationEditor can only manage subclasses of DashboardPanelDataObject", E_USER_ERROR);
		}

		$this->records = $this->controller->$relationName();

		parent::__construct($relationName, $title);

	}

	/**
	 * Sets the template of the editor
	 *
	 * @param string The name of the template
	 */
	public function setTemplate($template) {
		$this->template = $template;
	}

	/**
	 * Gets all of the items in the relation and provides edit/delete links for the table
	 *
	 * @return ArrayList
	 */
	public function Items() {
		$items = ArrayList::create(array());
		$labelField = Config::inst()->get($this->relationClass, "label_field", Config::INHERITED);
		foreach($this->records as $record) {
			$items->push(ArrayData::create(array(
				'Label' => $record->$labelField,
				'DeleteLink' => Controller::join_links($this->Link("item"),$record->ID,"delete"),
				'EditLink' => $this->Link("item/{$record->ID}"),
				'ID' => $record->ID
			)));
		}
		return $items;
	}

	/**
	 * Renders the form field
	 *
	 * @return SSViewer
	 */
	public function FieldHolder($attributes = array ()) {
		return $this->renderWith($this->template);
	}

	/**
	 * Handles a request for a record in the table
	 *
	 * @param SS_HTTPRequest
	 * @return SS_HTTPResponse
	 */
	public function handleItem(SS_HTTPRequest $r) {
		if($r->param('ID') == "new") {
			$item = Object::create($this->relationClass);
		}
		else {
			$item = DataList::create($this->relationClass)->byID((int) $r->param('ID'));
		}
		if($item) {
			$handler = DashboardHasManyRelationEditor_ItemRequest::create($this->controller->getDashboard(), $this->controller, $this, $item);
			return $handler->handleRequest($r, DataModel::inst());
		}
		return $this->httpError(404);
	}

	/**
	 * A default controller action that renders the editor
	 *
	 * @param SS_HTTPRequest
	 * @return SSViewer
	 */
	public function index(SS_HTTPRequest $r) {
		return $this->FieldHolder();
	}

	/**
	 * A controller action that handles the reordering of the list
	 *
	 * @param SS_HTTPRequest
	 * @return SS_HTTPResponse
	 */
	public function sort(SS_HTTPRequest $r) {
		if($items = $r->getVar('item')) {
			foreach($items as $position => $id) {
				if($item = DataList::create($this->relationClass)->byID((int) $id)) {
					$item->SortOrder = $position;
					$item->write();
				}
			}
			return new SS_HTTPResponse("OK");
		}
	}

}

/**
 * Defines the {@link RequestHandler} object that handles an item belonging to the editor
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */
class DashboardHasManyRelationEditor_ItemRequest extends RequestHandler {

	private static $allowed_actions = array(
		"edit",
		"delete",
		"DetailForm"
	);

	/**
	 * @var Dashboard The Dashboard controller in the CMS
	 */
	protected $dashboard;

	/**
	 * @var DashboardPanel The dashboard panel that owns the editor that is running the request
	 */
	protected $panel;

	/**
	 * @var DashboardHasManyRelationEditor The editor that is running the request
	 */
	protected $editor;

	/**
	 * @var DashboardPanelDataObject The object that was requested for edit/create/delete
	 */
	protected $item;

	private static $url_handlers = array (
		'$Action!' => '$Action',
		'' => 'edit'
	);

	public function __construct($dashboard, $panel, $editor, $item) {
		$this->dashboard = $dashboard;
		$this->panel = $panel;
		$this->editor = $editor;
		$this->item = $item;
		parent::__construct();
	}

	/**
	 * An action that handles the edit of an object managed by the editor
	 *
	 * @param SS_HTTPRequest
	 * @return SSViewer
	 */
	public function edit(SS_HTTPRequest $r) {
		return $this->renderWith('DashboardHasManyRelationEditorDetailForm');
	}

	/**
	 * An action that handles the deletion of an object managed by the editor
	 *
	 * @param SS_HTTPRequest
	 * @return SSViewer
	 */
	public function delete(SS_HTTPRequest $r) {
		$this->item->delete();
		return new SS_HTTPResponse("OK");
	}

	/**
	 * A link to this item as managed by the editor belonging to a dashbaord panel
	 *
	 * @return string
	 */
	public function Link($action = null) {
		return Controller::join_links($this->editor->Link(),"item",$this->item->ID ? $this->item->ID : "new",$action);
	}

	/**
	 * A link to refresh the editor
	 *
	 * @return string
	 */
	public function RefreshLink() {
		return $this->Link("edit");
	}

	/**
	 * Provides a form to edit or create an object managed by the editor
	 *
	 * @return Form
	 */
	public function DetailForm() {
		$form = Form::create(
			$this,
			"DetailForm",
			Injector::inst()->get($this->editor->relationClass)->getConfiguration(),
			FieldList::create(
				FormAction::create('saveDetail',_t('Dashboard.SAVE','Save'))
					->setUseButtonTag(true)
					->addExtraClass('ss-ui-action-constructive small'),
				FormAction::create('cancel',_t('Dashboard.CANCEL','Cancel'))
					->setUseButtonTag(true)
					->addExtraClass('small')
			)
		);
		$form->setHTMLID("Form_DetailForm_".$this->panel->ID."_".$this->item->ID);
		$form->loadDataFrom($this->item);
		$form->addExtraClass('dashboard-has-many-editor-detail-form-form');
		return $form;
	}

	/**
	 * Saves the DetailForm and writes or creates a new object managed by the editor
	 *
	 * @param array The raw POST data from the form
	 * @param Form The DetailForm object
	 */
	public function saveDetail($data, $form) {
		$item = $this->item;
		if(!$item->exists()) {
			$item->DashboardPanelID = $this->panel->ID;
			$sort = DataList::create($item->class)->max("SortOrder");
			$item->SortOrder = $sort+1;
			$item->write();
		}
		$form->saveInto($item);
		$item->write();
		return new SS_HTTPResponse("OK");
	}

}


<?php

/**
 * Defines the "RSS Feed" dashboard panel type
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */

class DashboardRSSFeedPanel extends DashboardPanel {

	private static $db = array (
		'FeedURL' => 'Text',
		'Count' => 'Int'
	);

	private static $defaults = array (
		'Count' => 10,
		'DateFormat' => '%B %e, %Y'
	);

	private static $icon = "dashboard/images/rss-feed.png";

	private static $priority = 30;

	private static $configure_on_create = true;

	public function getLabel() {
		return _t('Dashboard.RSSFEED','RSS Feed');
	}

	public function getDescription() {
		return _t('Dashboard.RSSFEEDDESCRIPTION','Adds an RSS feed from any public URL');
	}

	public function getConfiguration() {
		$fields = parent::getConfiguration();
		$fields->push(TextField::create("FeedURL", "Link to feed (include http://)"));
		$fields->push(DropdownField::create("DateFormat","Date format", array(
				'%B %e, %Y' => strftime('%B %e, %Y'),
				'%e %B, %Y' => strftime('%e %B, %Y')
			)));

		$fields->push(TextField::create("Count", _t('DashboardRecentEdits.COUNT','Number of pages to display')));
		return $fields;
	}

	/**
	 * Gets a list of all the items in the RSS feed given a user-provided URL, limit, and date format
	 *
	 * @return ArrayList
	 */
	public function RSSItems() {
		if(!$this->FeedURL) return false;
		$doc = new DOMDocument();
		@$doc->load($this->FeedURL);
		$items = $doc->getElementsByTagName('item');
		$feeds = array ();
		foreach ($items as $node) {
			$itemRSS = array (
				'title' => $node->getElementsByTagName('title')->item(0)->nodeValue,
				'desc' => $node->getElementsByTagName('description')->item(0)->nodeValue,
				'link' => $node->getElementsByTagName('link')->item(0)->nodeValue,
				'date' => $node->getElementsByTagName('pubDate')->item(0)->nodeValue
			);
			$feeds[] = $itemRSS;
		}
		$output = ArrayList::create(array());
		$count = 0;
		foreach($feeds as $item) {
			if($count >= $this->Count) break;
			// Cast the Date
			$date = new Date('Date');
			$date->setValue($item['date']);

			// Cast the Title
			$title = new Text('Title');
			$title->setValue($item['title']);

			$output->push(new ArrayData(array(
				'Title'			=> $title,
				'Date'			=> $date->Format($this->DateFormat),
				'Link'			=> $item['link']
			)));
			$count++;
		}

		return $output;

	}
}


<?php

/**
 * Defines the "Recent Edits" dashboard panel type
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */
class DashboardRecentEditsPanel extends DashboardPanel {

	private static $db = array (
		'Count' => 'Int'
	);

	private static $defaults = array (
		'Count' => 10
	);

	private static $icon = "dashboard/images/recent-edits.png";

	private static $priority = 10;

	public function getLabel() {
		return _t('RecentEdits.LABEL','Recent Edits');
	}

	public function getDescription() {
		return _t('RecentEdits.DESCRIPTION','Shows a linked list of recently edited pages');
	}

	public function getConfiguration() {
		$fields = parent::getConfiguration();
		$fields->push(TextField::create("Count",_t('DashboardRecentEdits.COUNT','Number of pages to display')));
		return $fields;
	}

	/**
	 * Gets the recent edited pages, limited to a user provided number of records
	 *
	 * @return ArrayList
	 */
	public function RecentEdits() {
		$records = SiteTree::get()->sort("LastEdited DESC")->limit($this->Count);
		$set = ArrayList::create(array());
		foreach($records as $r) {
			$set->push(ArrayData::create(array(
				'EditLink' => Injector::inst()->get("CMSPagesController")->Link("edit/show/{$r->ID}"),
				'Title' => $r->Title
			)));
		}
		return $set;
	}

}


<?php

/**
 * Decorates the Member object to work with the Dashboard interface
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */
class DashboardMember extends DataExtension {

	private static $db = array (
		'HasConfiguredDashboard' => 'Boolean'
	);

	private static $has_many = array (
		'DashboardPanels' => 'DashboardPanel'
	);

	/**
	 * Removes the DashboardPanels tab from the Security section. Panels should not be managed there.
	 */
	public function updateCMSFields(FieldList $fields) {
		$fields->removeByName("DashboardPanels");
	}

	/**
	 * Ensures that new members get the default dashboard configuration. Once it has been applied,
	 * make sure this doesn't happen again, if for some reason a user insists on having an empty
	 * dashboard.
	 */
	public function onAfterWrite() {
		if(!$this->owner->HasConfiguredDashboard && !$this->owner->DashboardPanels()->exists()) {
			foreach(SiteConfig::current_site_config()->DashboardPanels() as $p) {
				$clone = $p->duplicate();
				$clone->SiteConfigID = 0;
				$clone->MemberID = $this->owner->ID;
				$clone->write();
			}

			DB::query("UPDATE \"Member\" SET \"HasConfiguredDashboard\" = 1 WHERE \"ID\" = {$this->owner->ID}");
			$this->owner->flushCache();
		}
	}
}


<?php

/**
 * Defines a {@link DashboardPanel} object that shows a summary of a GridField instance
 * that manages a relationship on a SiteTree object.
 * Provides create, and "view all" actions.
 *
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 * @package Dashboard
 */
class DashboardGridFieldPanel extends DashboardPanel {

	private static $db = array (
		'Count' => 'Int',
		'GridFieldName' => 'Varchar'
	);

	private static $has_one = array (
		'SubjectPage' => 'SiteTree'
	);

	private static $defaults = array (
		'Count' => 10
	);

	private static $configure_on_create = true;

	/**
	 * Override to check if there is a custom template for this panel, otherwise fall back.
	 *
	 * @return string
	 */
	protected function getTemplate() {
		$templateName = get_class($this) . '_' . $this->SubjectPage()->ClassName . '_' . $this->GridFieldName;
		if(SS_TemplateLoader::instance()->findTemplates($templateName)) {
			return $templateName;
		}
		return parent::getTemplate();
	}

	/**
	 * @var string Overrides the standard request handler to provide custom controller actions
	 */
	protected $requestHandlerClass = "DashboardGridField_PanelRequest";

	public function getLabel() {
		return _t('Dashboard.GRIDFIELDPANELTITLE','Grid Field Editor');
	}

	public function getDescription() {
		return _t('Dashboard.GRIDFIELDPANELDESCRIPTION','Adds a summary view of a GridField instance on a given page.');
	}

	/**
	 * Gets the actions for the top of the panel
	 *
	 * @return FieldList
	 */
	public function getPrimaryActions() {
		if(!$this->SubjectPageID || !$this->GridFieldName) return false;
		$actions = parent::getPrimaryActions();
		$actions->push(DashboardPanelAction::create(
			$this->CreateModelLink(),
			_t('Dashboard.CREATENEWGRIDFIELD','Create new'),
			"good"
		));
		return $actions;
	}

	/**
	 * Gets the actions for the bottom of the panel
	 *
	 * @return FieldList
	 */
	public function getSecondaryActions() {
		if(!$this->SubjectPageID || !$this->GridFieldName) return false;
		$actions = parent::getPrimaryActions();
		$actions->push(DashboardPanelAction::create(
			$this->ViewAllLink(),
			_t('Dashboard.VIEWALLGRIDFIELD','View all')
		));
		return $actions;
	}

	/**
	 * Gets the fields to configure the panel settings
	 *
	 * @return FieldList
	 */
	public function getConfiguration() {
		$fields = parent::getConfiguration();

		$grids = array ();
		if($this->SubjectPage()->exists()) {
			$grids = $this->getGridFieldsFor($this->SubjectPage());
		}
		$fields->push(TextField::create("Count", _t('DashbordModelAdmin.COUNT','Number of records to display')));

		$fields->push(DropdownField::create("SubjectPageID", _t('Dashboard.PAGE','Page'), $this->getHierarchy(0))
			->addExtraClass('no-chzn')
			->setAttribute('data-lookupurl', $this->Link("gridsforpage"))
			->setEmptyString("--- "._t('Dashboard.PLEASESELECT','Please select')." ---")
			->setTemplate("DashboardDropdownField")
		);
		$fields->push(DropdownField::create("GridFieldName", _t('Dashboard.GRIDFIELDNAME','GridField name'), $grids)
			->addExtraClass('no-chzn')
		);

		return $fields;
	}

	/**
	 * A recursive function that gets a hierarchy of the site tree for a dropdown. Cannot use
	 * the {@link TreeDropdownField} here due to UI constraints.
	 *
	 * @param int The ID of the root node of the tree
	 * @param int The level of depth in the hierarchy. Used to create a visial hierarchy.
	 * @return array
	 */
	protected function getHierarchy($parentID, $level = 0) {
		$options = array();
		$class = "SiteTree";
		$filter = array('ParentID' => $parentID);
		$children = DataList::create($class)->filter($filter);
		if($children->exists()) {
			foreach($children as $child) {
				$indent="";
				for($i=0;$i<$level;$i++) $indent .= "  ";
				$text = $child->Title;
				$options[$child->ID] = empty($text) ? "<em>$indent Untitled</em>" : $indent.$text;
				$options += $this->getHierarchy($child->ID, $level+1);
			}
		}
		return $options;
	}

	/**
	 * Gets the link to view all records in a GridField
	 *
	 * @return string
	 */
	public function ViewAllLink() {
		if($grid = $this->getGrid()) {
			return Controller::join_links(
				Injector::inst()->get("CMSMain")->Link("edit"),
				"show",
				$this->SubjectPageID
			)."#Root_".$this->getTabForGrid();
		}
	}

	/**
	 * Gets a link to create a new record in a GridField
	 *
	 * @return string
	 */
	public function CreateModelLink() {
		if($grid = $this->getGrid()) {
			return Controller::join_links(
				Injector::inst()->get("CMSMain")->Link("edit"),
				"EditForm",
				"field",
				$this->GridFieldName,
				"item",
				"new",
				"?ID={$this->SubjectPageID}"
			);
		}
	}

	/**
	 * Gets the GridFields for a given page
	 *
	 * @param SiteTree A given page
	 * @return array
	 */
	public function getGridFieldsFor(SiteTree $page) {
		$grids = array ();
		if(!$page || !$page->exists()) return $grids;

		foreach($page->getCMSFields()->dataFields() as $field) {
			if($field instanceof GridField) {
				$grids[$field->getName()] = $field->Title();
			}
		}
		if(empty($grids)) {
			return array(
				'' => _t('Dashboard.NOGRIDFIELDS','There are no GridFields defined for that page.')
			);
		}
		return $grids;
	}

	/**
	 * Gets the grid field instance given the selected page and GridField name
	 *
	 * @return GridField
	 */
	protected function getGrid() {
		if($this->SubjectPage()->exists() && $this->GridFieldName) {
			if($grid = $this->SubjectPage()->getCMSFields()->dataFieldByName($this->GridFieldName)) {
				$grid->setForm($this->Form());
				return $grid;
			}
		}
	}

	/**
	 * Gets the name of the tab for the selected grid field name. Allows direct linking
	 * to the edit form to expose the grid.
	 *
	 * @return string
	 */
	protected function getTabForGrid() {
		if(!$this->SubjectPage()->exists() || !$this->GridFieldName) return false;
		$fields = $this->SubjectPage()->getCMSFields();
		if($fields->hasTabSet()) {
			foreach($fields->fieldByName("Root")->Tabs() as $tab) {
				if($tab->fieldByName($this->GridFieldName)) {
					return $tab->getName();
				}
			}
		}
		return false;
	}

	/**
	 * Gets the records in this GridField summary. Provides edit links and a title label
	 *
	 * @return ArrayList
	 */
	public function GridFieldItems() {
		if($grid = $this->getGrid()) {
			$list = $grid->getList()
				->limit($this->Count)
				->sort("LastEdited DESC");
			$ret = ArrayList::create(array());
			foreach($list as $record) {
				$record->EditLink = Controller::join_links(
					Injector::inst()->get("CMSMain")->Link("edit"),
					"EditForm",
					"field",
					$this->GridFieldName,
					"item",
					$record->ID,
					"edit",
					"?ID={$this->SubjectPageID}"
				);
				$ret->push($record);
			}
			return $ret;
		}
	}

	/**
	 * Overload the renderer to load requirements
	 *
	 * @return SSViewer
	 */
	public function PanelHolder() {
		Requirements::javascript("dashboard/javascript/dashboard-gridfield-panel.js");
		return parent::PanelHolder();
	}

}

/**
 * This custom request handler allows controller actions that are unique to this panel type
 *
 * @author UncleCheese <unclecheese@leftandmain.com>
 * @package Dashbaord
 */
class DashboardGridField_PanelRequest extends Dashboard_PanelRequest {

	private static $allowed_actions = array(
		"gridsforpage"
	);

	/**
	 * Given a requested page, get the GridField names and provide a JSON response
	 *
	 * @param SS_HTTPRequest The request object
	 * @return string
	 */
	public function gridsforpage(SS_HTTPRequest $r) {
		$pageid = (int) $r->requestVar('pageid');
		return Convert::array2json($this->panel->getGridFieldsFor(SiteTree::get()->byID($pageid)));
	}

}


<?php
/**
 * GAPI - Google Analytics PHP Interface
 *
 * http://code.google.com/p/gapi-google-analytics-php-interface/
 *
 * @copyright Stig Manning 2009
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Stig Manning <stig@sdm.co.nz>
 * @author Joel Kitching <jkitching@mailbolt.com>
 * @author Cliff Gordon <clifton.gordon@gmail.com>
 * @version 2.0
 *
 */

class gapi {
  const account_data_url = 'https://www.googleapis.com/analytics/v3/management/accountSummaries';
  const report_data_url = 'https://www.googleapis.com/analytics/v3/data/ga';
  const interface_name = 'GAPI-2.0';
  const dev_mode = false;

  private $auth_method = null;
  private $account_entries = array();
  private $report_aggregate_metrics = array();
  private $report_root_parameters = array();
  private $results = array();

  /**
   * Constructor function for new gapi instances
   *
   * @param string $client_email Email of OAuth2 service account (XXXXX@developer.gserviceaccount.com)
   * @param string $key_file Location/filename of .p12 key file
   * @param string $delegate_email Optional email of account to impersonate
   * @return gapi
   */
  public function __construct($client_email, $key_file, $delegate_email = null) {
    if (version_compare(PHP_VERSION, '5.3.0') < 0) {
      throw new Exception('GAPI: PHP version ' . PHP_VERSION . ' is below minimum required 5.3.0.');
    }
    $this->auth_method = new gapiOAuth2();
    $this->auth_method->fetchToken($client_email, $key_file, $delegate_email);
  }

  /**
   * Return the auth token string retrieved by Google
   *
   * @return String
   */
  public function getToken() {
    return $this->auth_method->getToken();
  }

  /**
   * Return the auth token information from the Google service
   *
   * @return Array
   */
  public function getTokenInfo() {
    return $this->auth_method->getTokenInfo();
  }

  /**
   * Revoke the current auth token, rendering it invalid for future requests
   *
   * @return Boolean
   */
  public function revokeToken() {
    return $this->auth_method->revokeToken();
  }

  /**
   * Request account data from Google Analytics
   *
   * @param Int $start_index OPTIONAL: Start index of results
   * @param Int $max_results OPTIONAL: Max results returned
   */
  public function requestAccountData($start_index=1, $max_results=1000) {
    $get_variables = array(
      'start-index' => $start_index,
      'max-results' => $max_results,
      );
    $url = new gapiRequest(gapi::account_data_url);
    $response = $url->get($get_variables, $this->auth_method->generateAuthHeader());

    if (substr($response['code'], 0, 1) == '2') {
      return $this->accountObjectMapper($response['body']);
    } else {
      throw new Exception('GAPI: Failed to request account data. Error: "' . strip_tags($response['body']) . '"');
    }
  }

  /**
   * Request report data from Google Analytics
   *
   * $report_id is the Google report ID for the selected account
   *
   * $parameters should be in key => value format
   *
   * @param String $report_id
   * @param Array $dimensions Google Analytics dimensions e.g. array('browser')
   * @param Array $metrics Google Analytics metrics e.g. array('pageviews')
   * @param Array $sort_metric OPTIONAL: Dimension or dimensions to sort by e.g.('-visits')
   * @param String $filter OPTIONAL: Filter logic for filtering results
   * @param String $start_date OPTIONAL: Start of reporting period
   * @param String $end_date OPTIONAL: End of reporting period
   * @param Int $start_index OPTIONAL: Start index of results
   * @param Int $max_results OPTIONAL: Max results returned
   */
  public function requestReportData($report_id, $dimensions=null, $metrics, $sort_metric=null, $filter=null, $start_date=null, $end_date=null, $start_index=1, $max_results=10000) {
    $parameters = array('ids'=>'ga:' . $report_id);

    if (is_array($dimensions)) {
      $dimensions_string = '';
      foreach ($dimensions as $dimesion) {
        $dimensions_string .= ',ga:' . $dimesion;
      }
      $parameters['dimensions'] = substr($dimensions_string, 1);
    } elseif ($dimensions !== null) {
      $parameters['dimensions'] = 'ga:'.$dimensions;
    }

    if (is_array($metrics)) {
      $metrics_string = '';
      foreach ($metrics as $metric) {
        $metrics_string .= ',ga:' . $metric;
      }
      $parameters['metrics'] = substr($metrics_string, 1);
    } else {
      $parameters['metrics'] = 'ga:'.$metrics;
    }

    if ($sort_metric==null&&isset($parameters['metrics'])) {
      $parameters['sort'] = $parameters['metrics'];
    } elseif (is_array($sort_metric)) {
      $sort_metric_string = '';

      foreach ($sort_metric as $sort_metric_value) {
        //Reverse sort - Thanks Nick Sullivan
        if (substr($sort_metric_value, 0, 1) == "-") {
          $sort_metric_string .= ',-ga:' . substr($sort_metric_value, 1); // Descending
        }
        else {
          $sort_metric_string .= ',ga:' . $sort_metric_value; // Ascending
        }
      }

      $parameters['sort'] = substr($sort_metric_string, 1);
    } else {
      if (substr($sort_metric, 0, 1) == "-") {
        $parameters['sort'] = '-ga:' . substr($sort_metric, 1);
      } else {
        $parameters['sort'] = 'ga:' . $sort_metric;
      }
    }

    if ($filter!=null) {
      $filter = $this->processFilter($filter);
      if ($filter!==false) {
        $parameters['filters'] = $filter;
      }
    }

    if ($start_date==null) {
      // Use the day that Google Analytics was released (1 Jan 2005).
      $start_date = '2005-01-01';
    } elseif (is_int($start_date)) {
      // Perhaps we are receiving a Unix timestamp.
      $start_date = date('Y-m-d', $start_date);
    }

    $parameters['start-date'] = $start_date;

    if ($end_date==null) {
      $end_date = date('Y-m-d');
    } elseif (is_int($end_date)) {
      // Perhaps we are receiving a Unix timestamp.
      $end_date = date('Y-m-d', $end_date);
    }

    $parameters['end-date'] = $end_date;

    $parameters['start-index'] = $start_index;
    $parameters['max-results'] = $max_results;

    $parameters['prettyprint'] = gapi::dev_mode ? 'true' : 'false';

    $url = new gapiRequest(gapi::report_data_url);
    $response = $url->get($parameters, $this->auth_method->generateAuthHeader());

    //HTTP 2xx
    if (substr($response['code'], 0, 1) == '2') {
      return $this->reportObjectMapper($response['body']);
    } else {
      throw new Exception('GAPI: Failed to request report data. Error: "' . $this->cleanErrorResponse($response['body']) . '"');
    }
  }

  /**
   * Clean error message from Google API
   *
   * @param String $error Error message HTML or JSON from Google API
   */
  private function cleanErrorResponse($error) {
    if (strpos($error, '<html') !== false) {
      $error = preg_replace('/<(style|title|script)[^>]*>[^<]*<\/(style|title|script)>/i', '', $error);
      return trim(preg_replace('/\s+/', ' ', strip_tags($error)));
    }
    else
    {
      $json = json_decode($error);
      return isset($json->error->message) ? strval($json->error->message) : $error;
    }
  }

  /**
   * Process filter string, clean parameters and convert to Google Analytics
   * compatible format
   *
   * @param String $filter
   * @return String Compatible filter string
   */
  protected function processFilter($filter) {
    $valid_operators = '(!~|=~|==|!=|>|<|>=|<=|=@|!@)';

    $filter = preg_replace('/\s\s+/', ' ', trim($filter)); //Clean duplicate whitespace
    $filter = str_replace(array(',', ';'), array('\,', '\;'), $filter); //Escape Google Analytics reserved characters
    $filter = preg_replace('/(&&\s*|\|\|\s*|^)([a-z0-9]+)(\s*' . $valid_operators . ')/i','$1ga:$2$3',$filter); //Prefix ga: to metrics and dimensions
    $filter = preg_replace('/[\'\"]/i', '', $filter); //Clear invalid quote characters
    $filter = preg_replace(array('/\s*&&\s*/','/\s*\|\|\s*/','/\s*' . $valid_operators . '\s*/'), array(';', ',', '$1'), $filter); //Clean up operators

    if (strlen($filter) > 0) {
      return urlencode($filter);
    }
    else {
      return false;
    }
  }

  /**
   * Report Account Mapper to convert the JSON to array of useful PHP objects
   *
   * @param String $json_string
   * @return Array of gapiAccountEntry objects
   */
  protected function accountObjectMapper($json_string) {
    $json = json_decode($json_string, true);
    $results = array();

    foreach ($json['items'] as $item) {
      foreach ($item['webProperties'] as $property) {
        if (isset($property['profiles'][0]['id'])) {
          $property['ProfileId'] = $property['profiles'][0]['id'];
        }
        $results[] = new gapiAccountEntry($property);
      }
    }

    $this->account_entries = $results;

    return $results;
  }

  /**
   * Report Object Mapper to convert the JSON to array of useful PHP objects
   *
   * @param String $json_string
   * @return Array of gapiReportEntry objects
   */
  protected function reportObjectMapper($json_string) {
    $json = json_decode($json_string, true);

    $this->results = null;
    $results = array();

    $report_aggregate_metrics = array();

    //Load root parameters

    // Start with elements from the root level of the JSON that aren't themselves arrays.
    $report_root_parameters = array_filter($json, function($var){
      return !is_array($var);
    });

    // Get the items from the 'query' object, and rename them slightly.
    foreach($json['query'] as $index => $value) {
      $new_index = lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $index))));
      $report_root_parameters[$new_index] = $value;
    }

    // Now merge in the profileInfo, as this is also mostly useful.
    array_merge($report_root_parameters, $json['profileInfo']);

    //Load result aggregate metrics

    foreach($json['totalsForAllResults'] as $index => $metric_value) {
      //Check for float, or value with scientific notation
      if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value)) {
        $report_aggregate_metrics[str_replace('ga:', '', $index)] = floatval($metric_value);
      } else {
        $report_aggregate_metrics[str_replace('ga:', '', $index)] = intval($metric_value);
      }
    }

    //Load result entries
    if(isset($json['rows'])){
      foreach($json['rows'] as $row) {
        $metrics = array();
        $dimensions = array();
        foreach($json['columnHeaders'] as $index => $header) {
          switch($header['columnType']) {
            case 'METRIC':
              $metric_value = $row[$index];

              //Check for float, or value with scientific notation
              if(preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/',$metric_value)) {
                $metrics[str_replace('ga:', '', $header['name'])] = floatval($metric_value);
              } else {
                $metrics[str_replace('ga:', '', $header['name'])] = intval($metric_value);
              }
              break;
            case 'DIMENSION':
              $dimensions[str_replace('ga:', '', $header['name'])] = strval($row[$index]);
              break;
            default:
              throw new Exception("GAPI: Unrecognized columnType '{$header['columnType']}' for columnHeader '{$header['name']}'");
          }
        }
        $results[] = new gapiReportEntry($metrics, $dimensions);
      }
    }

    $this->report_root_parameters = $report_root_parameters;
    $this->report_aggregate_metrics = $report_aggregate_metrics;
    $this->results = $results;

    return $results;
  }

  /**
   * Get current analytics results
   *
   * @return Array
   */
  public function getResults() {
    return is_array($this->results) ? $this->results : false;
  }

  /**
   * Get current account data
   *
   * @return Array
   */
  public function getAccounts() {
    return is_array($this->account_entries) ? $this->account_entries : false;
  }

  /**
   * Get an array of the metrics and the matching
   * aggregate values for the current result
   *
   * @return Array
   */
  public function getMetrics() {
    return $this->report_aggregate_metrics;
  }

  /**
   * Call method to find a matching root parameter or
   * aggregate metric to return
   *
   * @param $name String name of function called
   * @return String
   * @throws Exception if not a valid parameter or aggregate
   * metric, or not a 'get' function
   */
  public function __call($name, $parameters) {
    if (!preg_match('/^get/', $name)) {
      throw new Exception('No such function "' . $name . '"');
    }

    $name = preg_replace('/^get/', '', $name);

    $parameter_key = gapi::ArrayKeyExists($name, $this->report_root_parameters);

    if ($parameter_key) {
      return $this->report_root_parameters[$parameter_key];
    }

    $aggregate_metric_key = gapi::ArrayKeyExists($name, $this->report_aggregate_metrics);

    if ($aggregate_metric_key) {
      return $this->report_aggregate_metrics[$aggregate_metric_key];
    }

    throw new Exception('No valid root parameter or aggregate metric called "' . $name . '"');
  }

  /**
   * Case insensitive array_key_exists function, also returns
   * matching key.
   *
   * @param String $key
   * @param Array $search
   * @return String Matching array key
   */
  public static function ArrayKeyExists($key, $search) {
    if (array_key_exists($key, $search)) {
      return $key;
    }
    if (!(is_string($key) && is_array($search))) {
      return false;
    }
    $key = strtolower($key);
    foreach ($search as $k => $v) {
      if (strtolower($k) == $key) {
        return $k;
      }
    }
    return false;
  }
}

/**
 * Storage for individual gapi account entries
 *
 */
class gapiAccountEntry {
  private $properties = array();

  /**
   * Constructor function for all new gapiAccountEntry instances
   *
   * @param Array $properties
   * @return gapiAccountEntry
   */
  public function __construct($properties) {
    $this->properties = $properties;
  }

  /**
   * toString function to return the name of the account
   *
   * @return String
   */
  public function __toString() {
    return isset($this->properties['name']) ?
      $this->properties['name']: '';
  }

  /**
   * Get an associative array of the properties
   * and the matching values for the current result
   *
   * @return Array
   */
  public function getProperties() {
    return $this->properties;
  }

  /**
   * Call method to find a matching parameter to return
   *
   * @param $name String name of function called
   * @return String
   * @throws Exception if not a valid parameter, or not a 'get' function
   */
  public function __call($name, $parameters) {
    if (!preg_match('/^get/', $name)) {
      throw new Exception('No such function "' . $name . '"');
    }

    $name = preg_replace('/^get/', '', $name);

    $property_key = gapi::ArrayKeyExists($name, $this->properties);

    if ($property_key) {
      return $this->properties[$property_key];
    }

    throw new Exception('No valid property called "' . $name . '"');
  }
}

/**
 * Storage for individual gapi report entries
 *
 */
class gapiReportEntry {
  private $metrics = array();
  private $dimensions = array();

  /**
   * Constructor function for all new gapiReportEntry instances
   *
   * @param Array $metrics
   * @param Array $dimensions
   * @return gapiReportEntry
   */
  public function __construct($metrics, $dimensions) {
    $this->metrics = $metrics;
    $this->dimensions = $dimensions;
  }

  /**
   * toString function to return the name of the result
   * this is a concatented string of the dimensions chosen
   *
   * For example:
   * 'Firefox 3.0.10' from browser and browserVersion
   *
   * @return String
   */
  public function __toString() {
    return is_array($this->dimensions) ?
      implode(' ', $this->dimensions) : '';
  }

  /**
   * Get an associative array of the dimensions
   * and the matching values for the current result
   *
   * @return Array
   */
  public function getDimensions() {
    return $this->dimensions;
  }

  /**
   * Get an array of the metrics and the matchning
   * values for the current result
   *
   * @return Array
   */
  public function getMetrics() {
    return $this->metrics;
  }

  /**
   * Call method to find a matching metric or dimension to return
   *
   * @param String $name name of function called
   * @param Array $parameters
   * @return String
   * @throws Exception if not a valid metric or dimensions, or not a 'get' function
   */
  public function __call($name, $parameters) {
    if (!preg_match('/^get/', $name)) {
      throw new Exception('No such function "' . $name . '"');
    }

    $name = preg_replace('/^get/', '', $name);

    $metric_key = gapi::ArrayKeyExists($name, $this->metrics);

    if ($metric_key) {
      return $this->metrics[$metric_key];
    }

    $dimension_key = gapi::ArrayKeyExists($name, $this->dimensions);

    if ($dimension_key) {
      return $this->dimensions[$dimension_key];
    }

    throw new Exception('No valid metric or dimesion called "' . $name . '"');
  }
}

/**
 * OAuth2 Google API authentication
 *
 */
class gapiOAuth2 {
  const scope_url = 'https://www.googleapis.com/auth/analytics.readonly';
  const request_url = 'https://www.googleapis.com/oauth2/v3/token';
  const grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
  const header_alg = 'RS256';
  const header_typ = 'JWT';

  private function base64URLEncode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  }

  private function base64URLDecode($data) {
    return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
  }

  /**
   * Authenticate Google Account with OAuth2
   *
   * @param String $client_email
   * @param String $key_file
   * @param String $delegate_email
   * @return String Authentication token
   */
  public function fetchToken($client_email, $key_file, $delegate_email = null) {
    $header = array(
      "alg" => self::header_alg,
      "typ" => self::header_typ,
    );

    $claimset = array(
      "iss" => $client_email,
      "scope" => self::scope_url,
      "aud" => self::request_url,
      "exp" => time() + (60 * 60),
      "iat" => time(),
    );

    if(!empty($delegate_email)) {
      $claimset["sub"] = $delegate_email;
    }

    $data = $this->base64URLEncode(json_encode($header)) . '.' . $this->base64URLEncode(json_encode($claimset));

    if (!file_exists($key_file)) {
      if ( !file_exists(__DIR__ . DIRECTORY_SEPARATOR . $key_file) ) {
        throw new Exception('GAPI: Failed load key file "' . $key_file . '". File could not be found.');
      } else {
        $key_file = __DIR__ . DIRECTORY_SEPARATOR . $key_file;
      }
    }

    $key_data = file_get_contents($key_file);

    if (empty($key_data)) {
      throw new Exception('GAPI: Failed load key file "' . $key_file . '". File could not be opened or is empty.');
    }

    openssl_pkcs12_read($key_data, $certs, 'notasecret');

    if (!isset($certs['pkey'])) {
      throw new Exception('GAPI: Failed load key file "' . $key_file . '". Unable to load pkcs12 check if correct p12 format.');
    }

    openssl_sign($data, $signature, openssl_pkey_get_private($certs['pkey']), "sha256");

    $post_variables = array(
      'grant_type' => self::grant_type,
      'assertion' => $data . '.' . $this->base64URLEncode($signature),
    );

    $url = new gapiRequest(self::request_url);
    $response = $url->post(null, $post_variables);
    $auth_token = json_decode($response['body'], true);

    if (substr($response['code'], 0, 1) != '2' || !is_array($auth_token) || empty($auth_token['access_token'])) {
      throw new Exception('GAPI: Failed to authenticate user. Error: "' . strip_tags($response['body']) . '"');
    }

    $this->auth_token = $auth_token['access_token'];

    return $this->auth_token;
  }

  /**
   * Return the auth token string retrieved from Google
   *
   * @return String
   */
  public function getToken() {
    return $this->auth_token;
  }

  /**
   * Generate authorization token header for all requests
   *
   * @param String $token
   * @return Array
   */
  public function generateAuthHeader($token=null) {
    if ($token == null)
      $token = $this->auth_token;
    return array('Authorization' => 'Bearer ' . $token);
  }
}

/**
 * Google Analytics API request
 *
 */
class gapiRequest {
  const http_interface = 'auto'; //'auto': autodetect, 'curl' or 'fopen'
  const interface_name = gapi::interface_name;

  private $url = null;

  public function __construct($url) {
    $this->url = $url;
  }

  /**
   * Return the URL to be requested, optionally adding GET variables
   *
   * @param Array $get_variables
   * @return String
   */
  public function getUrl($get_variables=null) {
    if (is_array($get_variables)) {
      $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&')));
    } else {
      $get_variables = null;
    }

    return $this->url . $get_variables;
  }

  /**
   * Perform http POST request
   *
   *
   * @param Array $get_variables
   * @param Array $post_variables
   * @param Array $headers
   */
  public function post($get_variables=null, $post_variables=null, $headers=null) {
    return $this->request($get_variables, $post_variables, $headers);
  }

  /**
   * Perform http GET request
   *
   *
   * @param Array $get_variables
   * @param Array $headers
   */
  public function get($get_variables=null, $headers=null) {
    return $this->request($get_variables, null, $headers);
  }

  /**
   * Perform http request
   *
   *
   * @param Array $get_variables
   * @param Array $post_variables
   * @param Array $headers
   */
  public function request($get_variables=null, $post_variables=null, $headers=null) {
    $interface = self::http_interface;

    if (self::http_interface == 'auto')
      $interface = function_exists('curl_exec') ? 'curl' : 'fopen';

    switch ($interface) {
      case 'curl':
        return $this->curlRequest($get_variables, $post_variables, $headers);
      case 'fopen':
        return $this->fopenRequest($get_variables, $post_variables, $headers);
      default:
        throw new Exception('Invalid http interface defined. No such interface "' . self::http_interface . '"');
    }
  }

  /**
   * HTTP request using PHP CURL functions
   * Requires curl library installed and configured for PHP
   *
   * @param Array $get_variables
   * @param Array $post_variables
   * @param Array $headers
   */
  private function curlRequest($get_variables=null, $post_variables=null, $headers=null) {
    $ch = curl_init();

    if (is_array($get_variables)) {
      $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&')));
    } else {
      $get_variables = null;
    }

    curl_setopt($ch, CURLOPT_URL, $this->url . $get_variables);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //CURL doesn't like google's cert

    if (is_array($post_variables)) {
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_variables, '', '&'));
    }

    if (is_array($headers)) {
      $string_headers = array();
      foreach ($headers as $key => $value) {
        $string_headers[] = "$key: $value";
      }
      curl_setopt($ch, CURLOPT_HTTPHEADER, $string_headers);
    }

    $response = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    return array('body' => $response, 'code' => $code);
  }

  /**
   * HTTP request using native PHP fopen function
   * Requires PHP openSSL
   *
   * @param Array $get_variables
   * @param Array $post_variables
   * @param Array $headers
   */
  private function fopenRequest($get_variables=null, $post_variables=null, $headers=null) {
    $http_options = array('method'=>'GET', 'timeout'=>3);

    $string_headers = '';
    if (is_array($headers)) {
      foreach ($headers as $key => $value) {
        $string_headers .= "$key: $value\r\n";
      }
    }

    if (is_array($get_variables)) {
      $get_variables = '?' . str_replace('&', '&', urldecode(http_build_query($get_variables, '', '&')));
    }
    else {
      $get_variables = null;
    }

    if (is_array($post_variables)) {
      $post_variables = str_replace('&', '&', urldecode(http_build_query($post_variables, '', '&')));
      $http_options['method'] = 'POST';
      $string_headers = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($post_variables) . "\r\n" . $string_headers;
      $http_options['header'] = $string_headers;
      $http_options['content'] = $post_variables;
    }
    else {
      $post_variables = '';
      $http_options['header'] = $string_headers;
    }

    $context = stream_context_create(array('http'=>$http_options));
    $response = @file_get_contents($this->url . $get_variables, null, $context);

    return array('body'=>$response!==false?$response:'Request failed, consider using php5-curl module for more information.', 'code'=>$response!==false?'200':'400');
  }
}


<?php

/**
 * Defines the "Recent Files" dashboard panel type
 *
 * @package Dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */

class DashboardRecentFilesPanel extends DashboardPanel {

	private static $db = array (
		'Count' => 'Int'
	);

	private static $defaults = array (
		'Count' => 10
	);

	private static $icon = "dashboard/images/recent-files.png";

	private static $priority = 20;

	public function getLabel() {
		return _t('RecentFiles.LABEL','Recent Files');
	}

	public function getDescription() {
		return _t('RecentFiles.DESCRIPTION','Shows a linked list of recently edited files');
	}

	public function getConfiguration() {
		$fields = parent::getConfiguration();
		$fields->push(TextField::create("Count",_t('DashboardRecentFile.COUNT','Number of files to display')));
		return $fields;
	}

	/**
	 * Gets a list of the recently uploaded files to the CMS
	 *
	 * @return ArrayList
	 */
	public function RecentFiles() {
		$records = File::get()
			->filter(array(
				'ClassName:not' => 'Folder'
			))
			->sort("LastEdited DESC")
			->limit($this->Count);
		$set = ArrayList::create(array());
		foreach($records as $r) {
			$set->push(ArrayData::create(array(
				'EditLink' => Injector::inst()->get("AssetAdmin")->Link("EditForm/field/File/item/{$r->ID}/edit"),
				'Title' => $r->Title
			)));
		}
		return $set;
	}

}


<?php

/**
 * Defines the DashboardPanel dataobject. All dashboard panels must descend from this class.
 *
 * @package dashboard
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 */
class DashboardPanel extends DataObject {

	private static $db = array (
		'Title' => 'Varchar(50)',
		'PanelSize' => "Enum('small,normal,large','normal')",
		'SortOrder' => 'Int'
	);

	private static $has_one = array (
		'Member' => 'Member',
		'SiteConfig' => 'SiteConfig'
	);

	private static $default_sort = "SortOrder ASC";

	/**
	 * @var string The size of the dashboard panel. Options: "small", "normal", and "large"
	 */
	private static $size = "normal";

	/**
	 * @var string The path to the icon image that represents this dashboard panel type
	 */
	private static $icon = "dashboard/images/dashboard-panel-default.png";

	/**
	 * @var int The "weight" of the dashboard panel when listed in the available panels.
	 *			Higher is lower in the list.
	 */
	private static $priority = 100;

	/**
	 * @var bool Show the configure form after creating. Used for panels that require
	 * configuration in order to show data
	 */
	private static $configure_on_create = false;

	/**
	 * @var string The name of the template used for the contents of this panel.
	 */
	protected $template;

	/**
	 * @var string the name of the template used for the wrapper of this panel
	 */
	protected $holderTemplate = "DashboardPanel";

	/**
	 * @var string The name of the request handler class that the Dashbaord controller
	 * will use to communicate with a given panel
	 */
	protected $requestHandlerClass = "Dashboard_PanelRequest";

	/**
	 * Allows the panel to be added
	 *
	 * @return string
	 */
	public function registered() {
		if (is_bool(self::config()->enabled)) {
			return self::config()->enabled;
		}
		return true;
	}

	/**
	 * Gets the template, falls back on a default value of the class name
	 *
	 * @return string
	 */
	protected function getTemplate() {
		return $this->template ? $this->template : $this->class;
	}

	/**
	 * Gets the holder template
	 *
	 * @return string
	 */
	public function getHolderTemplate() {
		return $this->holderTemplate;
	}

	/**
	 * Gets the request handler class
	 *
	 * @return string
	 */
	public function getRequestHandlerClass() {
		return $this->requestHandlerClass;
	}

	/**
	 * Essentially an abstract method. Every panel must have this method defined to provide
	 * a title to the panel selection window
	 *
	 * @return string
	 */
	public function getLabel() {

	}

	/**
	 * Essentially an abstract method. Every panel must have this method defined to provide
	 * a description to the panel selection window
	 *
	 * @return string
	 */
	public function getDescription() {

	}

	/**
	 * An accessor to the Dashboard controller
	 *
	 * @return Dashboard
	 */
	public function getDashboard() {
		return Injector::inst()->get("Dashboard");
	}

	/**
	 * Renders the panel to its template
	 *
	 * @return SSViewer
	 */
	public function render() {
		return $this->renderWith($this->holderTemplate);
	}

	/**
	 * A template accessor for the icon of this panel
	 *
	 * @return string
	 */
	public function Icon() {
		return Config::inst()->get($this->class, "icon", Config::INHERITED);
	}

	/**
	 * Renders the inner contents of the panel. Similar to $Layout in pages.
	 *
	 * @return SSViewer
	 */
	public function Content() {
		return $this->renderWith($this->getTemplate());
	}

	/**
	 * The link to this panel through the Dashboard controller
	 *
	 * @return string
	 */
	public function Link($action = null) {
		return Controller::join_links($this->getDashboard()->Link("panel/{$this->ID}"),$action);
	}

	/**
	 * The link to delete this panel from the dashboard
	 *
	 * @return string
	 */
	public function DeleteLink() {
		return $this->Link("delete");
	}

	/**
	 * The link to create this panel on the dashboard
	 *
	 * @return string
	 */
	public function CreateLink() {
		return Controller::join_links($this->getDashboard()->Link("panel/new"),"?type={$this->class}");
	}

	/**
	 * Template accessor for the $configure_on_create boolean
	 *
	 * @return boolean
	 */
	public function ShowConfigure() {
		return $this->stat('configure_on_create');
	}

	/**
	 * Gets the {@link FieldList} object that is used to configure the fields on this panel.
	 * Similar to getCMSFields().
	 *
	 * @return FieldList
	 */
	public function getConfiguration() {
		return FieldList::create(
			DashboardButtonOptionsField::create("PanelSize",_t('Dashboard.PANELSIZE',''), array(
				'small' => '',
				'normal' => '',
				'large' => ''
			))->setSize("small"),

			TextField::create("Title", _t('Dashboard.TITLE','Title'))
		);
	}

	/**
	 * Gets the primary actions, which may appear in the top of the panel
	 *
	 * @return ArrayList
	 */
	public function getPrimaryActions() {
		return ArrayList::create(array());
	}

	/**
	 * Gets the secondary actions, which may appear in the bottom of the panel
	 *
	 * @return ArrayList
	 */
	public function getSecondaryActions() {
		return ArrayList::create(array());
	}

	/**
	 * Renders the entire panel. Similar to {@link FormField::FieldHolder()}
	 *
	 * @return SSViewer
	 */
	public function PanelHolder() {
		return $this->renderWith($this->holderTemplate);
	}

	/**
	 * For backward compatibility to the old static $size property.
	 *
	 * @return string
	 */
	public function Size() {
		return $this->PanelSize;
	}

	/**
	 * Gets the configuration form for this panel
	 *
	 * @return Form
	 */
	public function Form() {
		return Dashboard_PanelRequest::create($this->getDashboard(), $this)->ConfigureForm();
	}

	/**
	 * Duplicates this panel. Drills down into the has_many relations
	 *
	 * @return DashboardPanel
	 */
	public function duplicate($dowrite = true) {
		$clone = parent::duplicate(true);
		foreach($this->has_many() as $relationName => $relationClass) {
			foreach($this->$relationName() as $relObject) {
				$relClone = $relObject->duplicate(false);
				$relClone->DashboardPanelID = $clone->ID;
				$relClone->write();
			}
		}
		return $clone;
	}

	public function canCreate($member = null) {
		$m = $member ? $member : Member::currentUser();
		return Permission::check("CMS_ACCESS_DashboardAddPanels");
	}

	public function canDelete($member = null) {
		$m = $member ? $member : Member::currentUser();
		return Permission::check("CMS_ACCESS_DashboardDeletePanels") && $this->MemberID == $m->ID;
	}

	public function canEdit($member = null) {
		$m = $member ? $member : Member::currentUser();
		return Permission::check("CMS_ACCESS_DashboardConfigurePanels") && $this->MemberID == $m->ID;
	}

	public function canView($member = null) {
		$m = $member ? $member : Member::currentUser();
		return Permission::check("CMS_ACCESS_Dashboard") && $this->MemberID == $m->ID;
	}

	public function IsConfigured() {
		return true;
	}

}


<?php

/**
 * Defines a {@link DashboardPanel} object that shows a summary of a ModelAdmin interface.
 * Provides create, and "view all" actions.
 *
 * @author Uncle Cheese <unclecheese@leftandmain.com>
 * @package Dashboard
 */
class DashboardModelAdminPanel extends DashboardPanel {

	private static $db = array (
		'Count' => 'Int',
		'ModelAdminClass' => 'Varchar',
		'ModelAdminModel' => 'Varchar'
	);

	private static $defaults = array (
		'Count' => 10
	);

	private static $configure_on_create = true;

	/**
	 * @var string Overrides the standard request handler to provide custom controller actions
	 */
	protected $requestHandlerClass = "DashboardModelAdmin_PanelRequest";

	/**
	 * Override to check if there is a custom template for this panel, otherwise fall back
	 *
	 * @return string
	 */
	protected function getTemplate() {
		$templateName = get_class($this) . '_' . $this->ModelAdminClass . '_' . $this->ModelAdminModel;
		if(SS_TemplateLoader::instance()->findTemplates($templateName)) {
			return $templateName;
		}
		return parent::getTemplate();
	}

	public function getLabel() {
		return _t('Dashboard.MODELADMINPANELTITLE','Model Admin Editor');
	}

	public function getDescription() {
		return _t('Dashboard.MODELADMINPANELDESCRIPTION','Adds a summary view of a Model Admin section of the CMS');
	}

	/**
	 * Gets the actions for the top of the panel
	 *
	 * @return FieldList
	 */
	public function getPrimaryActions() {
		if(!$this->ModelAdminClass || !$this->ModelAdminModel) return false;
		$actions = parent::getPrimaryActions();
		$actions->push(DashboardPanelAction::create(
			$this->CreateModelLink(),
			sprintf(_t('Dashboard.CREATENEW','Create new %s'),$this->SingularModelName()),
			"good"
		));
		return $actions;
	}

	/**
	 * Gets the actions for the bottom of the panel
	 *
	 * @return FieldList
	 */
	public function getSecondaryActions() {
		if(!$this->ModelAdminClass || !$this->ModelAdminModel) return false;
		$actions = parent::getPrimaryActions();
		$actions->push(DashboardPanelAction::create(
			$this->ViewAllLink(),
			sprintf(_t('Dashboard.VIEWALL','View all %s'),$this->PluralModelName())
		));
		return $actions;
	}

	/**
	 * Gets the fields to configure the panel settings
	 *
	 * @return FieldList
	 */
	public function getConfiguration() {
		$fields = parent::getConfiguration();
		$modeladmins = array ();
		$models = $this->getManagedModelsFor($this->ModelAdminClass);
		foreach(SS_ClassLoader::instance()->getManifest()->getDescendantsOf("ModelAdmin") as $class) {
			$SNG = Injector::inst()->get($class);
			if($SNG instanceof TestOnly) continue;
			$title = Config::inst()->get($class, "menu_title", Config::INHERITED);
			$modeladmins[$class] = $title ? $title : $class;
		}

		$fields->push(TextField::create("Count", _t('DashbordModelAdmin.COUNT','Number of records to display')));

		$fields->push(DropdownField::create("ModelAdminClass", _t('Dashboard.MODELADMINCLASS','Model admin tab'), $modeladmins)
			->addExtraClass('no-chzn')
			->setAttribute('data-lookupurl', $this->Link("modelsforpanel"))
			->setEmptyString("--- "._t('Dashboard.PLEASESELECT','Please select')." ---")
		);
		$fields->push(DropdownField::create("ModelAdminModel", _t('Dashboard.MODELADMINMODEL','Model'), $models)
			->addExtraClass('no-chzn')
		);

		return $fields;
	}

	/**
	 * Gets the link to view all records in ModelAdmin
	 *
	 * @return string
	 */
	public function ViewAllLink() {
		if($this->ModelAdminClass && $this->ModelAdminModel) {
			$url_segment = Injector::inst()->get($this->ModelAdminClass)->Link();
			return Controller::join_links($url_segment, $this->ModelAdminModel);
		}
	}

	/**
	 * Gets a link to create a new record in ModelAdmin
	 *
	 * @return string
	 */
	public function CreateModelLink() {
		if($this->ModelAdminClass && $this->ModelAdminModel) {
			$url_segment = Injector::inst()->get($this->ModelAdminClass)->Link();
			return Controller::join_links(
				$url_segment,
				$this->ModelAdminModel,
				"EditForm",
				"field",
				$this->ModelAdminModel,
				"item",
				"new"
			);
		}
	}

	/**
	 * Gets the entries in the $managed_models array for the selected ModelAdmin class
	 *
	 * @param string The name of the ModelAdmin class
	 * @return array
	 */
	public function getManagedModelsFor($class = null) {
		$models = array ();
		if(!$class) return $models;

		$m = Config::inst()->get($class, "managed_models", Config::INHERITED);
		if(is_array($m)) {
			foreach($m as $key => $managed_model) {
				// this covers the case: 'ModelName' => array('title' => 'My Tab Title')
				if (!is_numeric($key)) $managed_model = $key;
				$models[$managed_model] = $managed_model;
			}
		}
		return $models;
	}

	/**
	 * Gets the singular name for the chosen model
	 *
	 * @return string
	 */
	public function SingularModelName() {
		if($this->ModelAdminModel) {
			return Injector::inst()->get($this->ModelAdminModel)->i18n_singular_name();
		}
	}

	/**
	 * Gets the plural name for the chosen model
	 *
	 * @return string
	 */
	public function PluralModelName() {
		if($this->ModelAdminModel) {
			return Injector::inst()->get($this->ModelAdminModel)->i18n_plural_name();
		}
	}

	/**
	 * Gets the records in this ModelAdmin summary. Provides edit links and a title label
	 *
	 * @return ArrayList
	 */
	public function ModelAdminItems() {
		if($this->ModelAdminModel) {
			$SNG= Injector::inst()->get($this->ModelAdminModel);
			if($SNG->hasExtension("Versioned")) {
				Versioned::reading_stage("Stage");
			}
			$records = DataList::create($this->ModelAdminModel)
				->limit($this->Count)
				->sort("LastEdited DESC");
			$url_segment = Injector::inst()->get($this->ModelAdminClass)->Link();
			$ret = ArrayList::create(array());
			foreach($records as $rec) {
				$rec->EditLink = Controller::join_links(
					$url_segment,
					$this->ModelAdminModel,
					"EditForm",
					"field",
					$this->ModelAdminModel,
					"item",
					$rec->ID,
					"edit"
				);
				$ret->push($rec);
			}
			return $ret;
		}
	}

	/**
	 * Overload the renderer to load requirements
	 *
	 * @return SSViewer
	 */
	public function PanelHolder() {
		Requirements::javascript("dashboard/javascript/dashboard-modeladmin-panel.js");
		return parent::PanelHolder();
	}

}

/**
 * This custom request handler allows controller actions that are unique to this panel type
 *
 * @author UncleCheese <unclecheese@leftandmain.com>
 * @package Dashbaord
 */
class DashboardModelAdmin_PanelRequest extends Dashboard_PanelRequest {

	private static $allowed_actions = array(
		"modelsforpanel"
	);

	/**
	 * Given a requested ModelAdmin subclass, get the managed models and provide a JSON response
	 *
	 * @param SS_HTTPRequest The request object
	 * @return string
	 */
	public function modelsforpanel(SS_HTTPRequest $r) {
		$panel = $r->requestVar('modeladminpanel');
		return Convert::array2json($this->panel->getManagedModelsFor($panel));
	}

}