<?php

/**
 * @package Zammad API Client
 * @author  Jens Pfeifer <jens.pfeifer@znuny.com>
 */

namespace ZammadAPIClient\Resource;

class TicketArticle extends AbstractResource
{
    const URLS = [
        'get'                    => 'ticket_articles/{object_id}',
        'get_for_ticket'         => 'ticket_articles/by_ticket/{ticket_id}',
        'get_attachment_content' => 'ticket_attachment/{ticket_id}/{ticket_article_id}/{object_id}',
        'create'                 => 'ticket_articles',
        'update'                 => 'ticket_articles/{object_id}',
        'delete'                 => 'ticket_articles/{object_id}',
    ];

    /**
     * Fetches TicketArticle objects for given ticket ID.
     *
     * @param integer $ticket_id      ID of ticket to fetch article data for.
     *
     * @return array                Array of TicketArticle objects.
     */
    public function getForTicket($ticket_id)
    {
        if ( !empty( $this->getValues() ) ) {
            throw new \RuntimeException('Object already contains values, getForTicket() not possible, use a new object');
        }

        $this->clearError();

        $ticket_id = intval($ticket_id);
        if ( empty($ticket_id) ) {
            throw new \RuntimeException('Missing ticket ID');
        }

        $url = $this->getURL(
            'get_for_ticket',
            [
                'ticket_id' => $ticket_id,
            ]
        );
        $response = $this->getClient()->get(
            $url,
            [
                'expand' => true,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        // Return array of TicketArticle objects.
        $ticket_articles = [];
        foreach ( $response->getData() as $ticket_article_data ) {
            $ticket_article = $this->getClient()->resource( get_class($this) );
            $ticket_article->setRemoteData($ticket_article_data);
            $ticket_articles[] = $ticket_article;
        }

        return $ticket_articles;
    }

    /**
     * Fetches the actual content of the file of a ticket article attachment.
     * Note that the resource object must contain data of saved article (resource object must contain ID of article).
     *
     * @param integer $attachment_id      ID of attachment to fetch content for.
     *
     * @return string                     Attachment content or false on error.
     */
    public function getAttachmentContent($attachment_id)
    {
        $attachment_id = intval($attachment_id);
        if (!$attachment_id) {
            throw new \Exception('Attachment ID is invalid');
        }

        if ( empty( $this->getID() ) ) {
            throw new \Exception('Object data does not contain an ID, getAttachmentContent() not possible');
        }

        $this->clearError();

        $attachments = $this->getValue('attachments');
        if ( !is_array($attachments) || !count($attachments) ) {
            return false;
        }

        // Check if given attachment ID exists for article.
        $attachment_key = array_search( $attachment_id, array_column( $attachments, 'id' ) );
        if ( $attachment_key === false ) {
            return false;
        }
        $attachment = $attachments[$attachment_key];

        // Fetch content.
        $url = $this->getURL(
            'get_attachment_content',
            [
                'ticket_id'         => $this->getValue('ticket_id'),
                'ticket_article_id' => $this->getID(),
                'object_id'         => $attachment['id'],
            ]
        );

        $response = $this->getClient()->get($url);
        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return false;
        }

        $content = $response->getBody();
        return $content;
    }
}


<?php

/**
 * @package Zammad API Client
 * @author  Jens Pfeifer <jens.pfeifer@znuny.com>
 */

namespace ZammadAPIClient\Resource;

use ZammadAPIClient\Exception\AlreadyFetchedObjectException;

abstract class AbstractResource
{
    // API client used for requests to Zammad.
    private $client;

    // Data returned by response from Zammad.
    private $remote_data = [];

    // Values that were set by the API client to be saved in Zammad later by a call to save().
    private $values = [];

    // Error message of last response from Zammad.
    private $error = null;

    /**
     * @param object $client        ZammadAPIClient object
     */
    public function __construct( \ZammadAPIClient\Client $client )
    {
        $this->client = $client;
    }

    /**
     * Returns the ZammadAPIClient object.
     *
     * @return object    ZammadAPIClient object
     */
    protected function getClient()
    {
        return $this->client;
    }

    /**
     * Sets remote data that was returned from Zammad.
     *
     * @param array $remote_data    Data to set.
     *
     * @return object
     */
    protected function setRemoteData( array $remote_data = [] )
    {
        $this->remote_data = $remote_data;
        return $this;
    }

    /**
     * Clears remote data that was returned by Zammad.
     *
     * @return object   This object
     */
    protected function clearRemoteData()
    {
        $this->remote_data = [];
        return $this;
    }

    /**
     * Fetches remote data that was returned by Zammad.
     *
     * @return array Array with the object's data. Might be empty.
     */
    protected function getRemoteData()
    {
        return $this->remote_data;
    }

    /**
     * Sets local values that will be saved later in Zammad by a call to save().
     *
     * Already set values will be merged with the ones given.
     * If you want to completely discard the existing values,
     * call clearValues() before setValues().
     *
     * @param array $values     Values to set (key-value-pairs).
     *
     * @return object   This object
     */
    public function setValues( array $values )
    {
        $this->values = array_merge( $this->values, $values );
        return $this;
    }

    /**
     * Sets a single local value that will be saved later in Zammad by a call to save().
     *
     * @param string $key       Key of value to set.
     * @param mixed $value      Value to set.
     *
     * @return object   This object
     */
    public function setValue( $key, $value )
    {
        $this->values[$key] = $value;
        return $this;
    }

    /**
     * Gets value of object.
     *
     * First, it's been checked if the key exists in the local values that will be saved later in Zammad
     * by a call to save().
     *
     * If not found, additionally the remote data will be searched for the key. This
     * ensures that a value is being returned that hasn't been changed locally.
     *
     * @param string $key       Key to fetch value for.
     *
     * @return mixed            Value or null if not found. Null could also be the set value.
     */
    public function getValue($key)
    {
        if ( array_key_exists( $key, $this->values ) ) {
            return $this->values[$key];
        }

        $remote_data = $this->getRemoteData();
        if ( array_key_exists( $key, $remote_data ) ) {
            return $remote_data[$key];
        }

        return null;
    }

    /**
     * Gets all values of object.
     *
     * Local values will be merged with remote data, where local values overwrite remote data.
     * This ensures that values can be returned that haven't been changed locally.
     *
     * @return mixed        Array with values of object.
     */
    public function getValues()
    {
        $values = array_merge( $this->getRemoteData(), $this->values );
        return $values;
    }

    /**
     * Gets all unsaved values of object.
     *
     * @return mixed        Array with unsaved values of object.
     */
    public function getUnsavedValues()
    {
        return $this->values;
    }

    /**
     * Clears local values, which means that the local changes will be discarded and
     * a call to save() does not update the remote data.
     *
     * @return object   This object
     */
    public function clearUnsavedValues()
    {
        $this->values = [];
        return $this;
    }

    /**
     * Gets the ID of the object.
     * This is a convenience method for getValue('id')
     *
     * @return string       ID of this object, if available.
     */
    public function getID()
    {
        return $this->getValue('id');
    }

    /**
     * Checks if there are unsaved local values.
     *
     * @return boolean          True: dirty, false: not dirty.
     */
    public function isDirty()
    {
        return !empty( $this->getUnsavedValues() );
    }

    /**
     * Sets error message from last action.
     *
     * @param string $error      Error
     *
     * @return object   This object
     */
    protected function setError( $error )
    {
        $this->error = $error;
        return $this;
    }

    /**
     * Gets error message from last action.
     *
     * @return string   Error from last action
     */
    public function getError()
    {
        return $this->error;
    }

    /**
     * Checks if last action resulted in an error.
     *
     * @return boolean
     */
    public function hasError()
    {
        return !empty( $this->getError() );
    }

    /**
     * Clears error message from last action.
     *
     * @return object   This object
     */
    protected function clearError()
    {
        $this->error = null;
        return $this;
    }

    /**
     * Fetches object data for given object ID.
     *
     * @param mixed $object_id      ID of object to fetch, false value or omit to fetch all objects.
     *
     * @return object               This object.
     */
    public function get($object_id)
    {
        if ( !empty( $this->getValues() ) ) {
            throw new AlreadyFetchedObjectException('Object already contains values, get() not possible, use a new object');
        }

        if ( empty($object_id) ) {
            throw new \RuntimeException('Missing object ID');
        }

        $url = $this->getURL(
            'get',
            [
                'object_id' => $object_id,
            ]
        );

        $response = $this->getClient()->get(
            $url,
            [
                'expand' => true,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
        }
        else {
            $this->clearError();
            $this->setRemoteData( $response->getData() );
        }

        return $this;
    }

    /**
     * Fetches object data for all objects of this type.
     * Pagination available.
     *
     * @param integer $page                 Page of objects, optional, if given, $objects_per_page must also be given.
     * @param integer $objects_per_page     Number of objects per page, optional, if given, $page must also be given.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    public function all( $page = null, $objects_per_page = null )
    {
        if ( !empty( $this->getValues() ) ) {
            throw new AlreadyFetchedObjectException('Object already contains values, all() not possible, use a new object');
        }

        if ( isset($page) && $page <= 0 ) {
            throw new \RuntimeException('Parameter page must be > 0');
        }
        if ( isset($objects_per_page) && $objects_per_page <= 0 ) {
            throw new \RuntimeException('Parameter objects_per_page must be > 0');
        }
        if (
            ( isset($page) && !isset($objects_per_page) )
            || ( !isset($page) && isset($objects_per_page) )
        ) {
            throw new \RuntimeException('Parameters page and objects_per_page must both be given');
        }

        if ( !isset($page) || !isset($objects_per_page) ) {
            return $this->allWithoutPagination();
        }

        $url_parameters = [
            'expand' => true,
        ];

        if ( isset($page) && isset($objects_per_page) ) {
            $url_parameters['page']     = $page;
            $url_parameters['per_page'] = $objects_per_page;
        }

        $url      = $this->getURL('all');
        $response = $this->getClient()->get(
            $url,
            $url_parameters
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        $this->clearError();

        // Return array of resource objects if no $object_id was given.
        // Note: the resource object (this object) used to execute get() will be left empty in this case.
        $objects       = [];
        foreach ( $response->getData() as $object_data ) {
            $object = $this->getClient()->resource( get_class($this) );
            $object->setRemoteData($object_data);
            $objects[] = $object;
        }

        return $objects;
    }

    /**
     * Fetches object data for all objects of this type.
     * This method will be used internally and automatically by all() to automate pagination
     * to retrieve all available objects, ignoring the server side limit of fetchable objects.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    private function allWithoutPagination()
    {
        $page             = 1;
        $objects_per_page = 100;
        $objects          = [];
        $objects_of_page  = [];

        do {
            $objects_of_page = $this->all( $page, $objects_per_page );
            if ( !is_array($objects_of_page) ) {
                return $this;
            }

            $objects = array_merge( $objects, $objects_of_page );

            $is_last_page = count($objects_of_page) < $objects_per_page
                || !count($objects_of_page);

            $page++;
        } while ( !$is_last_page );

        return $objects;
    }

    /**
     * Fetches object data for given search term.
     * Pagination available.
     *
     * @param string  $search_term          Search term.
     * @param integer $page                 Page of objects, optional, if given, $objects_per_page must also be given.
     * @param integer $objects_per_page     Number of objects per page, optional, if given, $page must also be given.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    public function search( $search_term, $page = null, $objects_per_page = null )
    {
        if ( !empty( $this->getValues() ) ) {
            throw new AlreadyFetchedObjectException('Object already contains values, search() not possible, use a new object');
        }

        if ( isset($page) && $page <= 0 ) {
            throw new \RuntimeException('Parameter page must be a > 0');
        }
        if ( isset($objects_per_page) && $objects_per_page <= 0 ) {
            throw new \RuntimeException('Parameter objects_per_page must be a > 0');
        }
        if (
            ( isset($page) && !isset($objects_per_page) )
            || ( !isset($page) && isset($objects_per_page) )
        ) {
            throw new \RuntimeException('Parameters page and objects_per_page must both be given');
        }

        if ( !isset($page) || !isset($objects_per_page) ) {
            return $this->searchWithoutPagination($search_term);
        }

        $url_parameters = [
            'expand' => true,
            'query'  => $search_term,
        ];

        if ( isset($page) && isset($objects_per_page) ) {
            $url_parameters['page']     = $page;
            $url_parameters['per_page'] = $objects_per_page;
        }

        $url      = $this->getURL('search');
        $response = $this->getClient()->get(
            $url,
            $url_parameters
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        $this->clearError();

        // Return array of resource objects if no $object_id was given.
        // Note: the resource object (this object) used to execute get() will be left empty in this case.
        $objects = [];
        foreach ( $response->getData() as $object_data ) {
            $object = $this->getClient()->resource( get_class($this) );
            $object->setRemoteData($object_data);
            $objects[] = $object;
        }

        return $objects;
    }

    /**
     * Fetches object data for searched objects of this type.
     * This method will be used internally and automatically by search() to automate pagination
     * to retrieve all available objects, ignoring the server side limit of fetchable objects.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    private function searchWithoutPagination($search_term)
    {
        $page             = 1;
        $objects_per_page = 100;
        $objects          = [];
        $objects_of_page  = [];

        do {
            $objects_of_page = $this->search( $search_term, $page, $objects_per_page );
            if ( !is_array($objects_of_page) ) {
                return $this;
            }

            $objects = array_merge( $objects, $objects_of_page );

            $is_last_page = count($objects_of_page) < $objects_per_page
                || !count($objects_of_page);

            $page++;
        } while ( !$is_last_page );

        return $objects;
    }

    /**
     * Saves object data. Works for objects that are new or will be updated.
     *
     * @return mixed      Save successful (object reference, $this) or not (false).
     */
    public function save()
    {
        if ( empty( $this->getID() ) ) {
            return $this->create();
        }

        return $this->update();
    }

    /**
     * Creates a new object with the current data.
     *
     * @return object      This object
     */
    protected function create()
    {
        if ( !empty( $this->getID() ) ) {
            throw new \Exception('Object already has an ID, create() not possible');
        }

        if ( !$this->isDirty() ) {
            return $this;
        }

        $url      = $this->getURL('create');
        $response = $this->getClient()->post(
            $url,
            $this->getUnsavedValues(),
            [
                'expand' => true,
            ]
         );
        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        $this->clearError();
        $this->setRemoteData( $response->getData() );
        $this->clearUnsavedValues();

        return $this;
    }

    /**
     * Updates an existing object with the current data.
     *
     * @return object      This object
     */
    protected function update()
    {
        $object_id = $this->getID();
        if ( empty($object_id) ) {
            throw new \Exception('Object has no ID, update() not possible');
        }

        if ( !$this->isDirty() ) {
            return $this;
        }

        $url = $this->getURL(
            'update',
            [
                'object_id' => $object_id,
            ]
        );

        $response = $this->getClient()->put(
            $url,
            $this->getUnsavedValues(),
            [
                'expand' => true,
            ]
        );
        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        $this->clearError();
        $this->setRemoteData( $response->getData() );
        $this->clearUnsavedValues();

        return $this;
    }

    /**
     * Deletes the data of this object.
     * If data contains an ID, the object will also be deleted in Zammad.
     *
     * @return object      This object
     */
    public function delete()
    {
        // Delete object in Zammad.
        $object_id = $this->getID();
        if ( !empty($object_id) ) {
            $url = $this->getURL(
                'delete',
                [
                    'object_id' => $object_id,
                ]
            );

            $response = $this->getClient()->delete(
                $url,
                [
                    'expand' => true,
                ]
            );
            if ( $response->hasError() ) {
                $this->setError( $response->getError() );
                return $this;
            }
        }

        // Clear data of this (local) object.
        $this->clearError();
        $this->clearRemoteData();
        $this->clearUnsavedValues();

        return $this;
    }

    /**
     * Returns the URL for the given method name, including its replaced placeholders.
     *
     * @param string $method_name               E. g. 'get', 'all', etc.
     * @param array  $placeholder_values        Array of placeholder => value pairs,
     *                                              e. g. [ 'object_id' => 2 ] will replace
     *                                              {object_id} in URL with 2.
     *
     * @return string                           URL, e. g. 'tickets/10'.
     */
    protected function getURL( $method_name, array $placeholder_values = [] )
    {
        if ( !array_key_exists( $method_name, $this::URLS ) ) {
            throw new \RuntimeException(
                "Method '$method_name' is not supported for "
                . get_class($this)
                . ' resource'
            );
        }

        $url = $this::URLS[$method_name];
        foreach ( $placeholder_values as $placeholder => $value ) {
            $url = preg_replace( "/\{$placeholder\}/", "$value", $url );
        }

        return $url;
    }

    public function can( $method_name )
    {
        return array_key_exists( $method_name, $this::URLS );
    }
}


<?php

/**
 * @package Zammad API Client
 * @author  Alexander Reichardt <alexander.reichardt@icloud.com>
 */

namespace ZammadAPIClient\Resource;

class Tag extends AbstractResource
{
    const URLS = [
        'get'    => 'tags',
        'search' => 'tag_search?term={query}',
        'add'    => 'tags/add',
        'remove' => 'tags/remove'
    ];

    /**
     * Fetches tags of an object.
     *
     * @param integer $object_id    ID of the object to fetch tags for.
     * @param string $object_type   Type of object to fetch tags for (e. g. 'Ticket').
     *
     * @return object               This object.
     */
    public function get($object_id, $object_type = 'Ticket')
    {
        $this->clearError();

        $object_id = intval($object_id);
        if ( empty($object_id) ) {
            throw new \RuntimeException('Missing object ID');
        }

        $response = $this->getClient()->get(
            $this->getURL('get'),
            [
                'object' => $object_type,
                'o_id'   => $object_id,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
        }
        else {
            $this->clearError();
            $this->setRemoteData( $response->getData() );
        }

        return $this;
    }

    /**
     * Adds a tag to an object.
     *
     * @param integer $object_id    ID of the object to fetch tags for.
     * @param string $tag           Tag to add.
     * @param string $object_type   Type of object to fetch tags for (e. g. 'Ticket').
     *
     * @return object               This object.
     */
    public function add($object_id, $tag, $object_type = 'Ticket')
    {
        $this->clearError();

        $object_id = intval($object_id);
        if ( empty($object_id) ) {
            throw new \RuntimeException('Missing object ID');
        }

        if ( empty($tag) ) {
            throw new \RuntimeException('Missing tag');
        }

        $response = $this->getClient()->get(
            $this->getURL('add'),
            [
                'object' => $object_type,
                'o_id'   => $object_id,
                'item'   => $tag,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
        }

        return $this;
    }

    /**
     * Fetches tags for given search term.
     * Pagination available.
     *
     * @param string  $search_term          Search term.
     * @param integer $page                 Page of tags, optional, if given, $objects_per_page must also be given
     * @param integer $objects_per_page     Number of tags per page, optional, if given, $page must also be given
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    public function search($search_term, $page = null, $objects_per_page = null)
    {
        $this->clearError();

        if ( empty($search_term) ) {
            throw new \RuntimeException('Missing search term');
        }

        $response = $this->getClient()->get(
            $this->getURL('search'),
            [
                'term' => $search_term,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
        }

        $this->clearError();

        // Return array of resource objects if no $object_id was given.
        // Note: the resource object (this object) used to execute get() will be left empty in this case.
        $objects = [];
        foreach ( $response->getData() as $object_data ) {
            $object = $this->getClient()->resource( get_class($this) );
            $object->setRemoteData($object_data);
            $objects[] = $object;
        }

        return $objects;
    }

    /**
     * Removes a tag from an object.
     *
     * @param integer $object_id    ID of the object to fetch tags for.
     * @param string $tag           Tag to remove.
     * @param string $object_type   Type of object to fetch tags for (e. g. 'Ticket').
     *
     * @return object               This object.
     */
    public function remove($object_id, $tag, $object_type = 'Ticket')
    {
        $this->clearError();

        if ( empty($object_id) ) {
            throw new \RuntimeException('Missing object ID');
        }

        if ( empty($tag) ) {
            throw new \RuntimeException('Missing tag');
        }

        // Delete object in Zammad.
        $response = $this->getClient()->get(
            $this->getURL('remove'),
            [
                'object' => $object_type,
                'o_id'   => $object_id,
                'item'   => $tag,
            ]
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        // Clear data of this (local) object.
        $this->clearError();
        $this->clearRemoteData();
        $this->clearUnsavedValues();

        return $this;
    }
}


<?php

/*
 * Copyright (C) 2005-2016 IP2Location.com
 * All Rights Reserved
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace IP2Proxy;

/**
 * IP2Proxy database class
 *
 */
class Database {

  /**
   * Current module's version
   *
   * @var string
   */
  const VERSION = '1.1.2';

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Error field constants  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Unsupported field message
   *
   * @var string
   */
  const FIELD_NOT_SUPPORTED = 'NOT SUPPORTED';

  /**
   * Unknown field message
   *
   * @var string
   */
  const FIELD_NOT_KNOWN = 'This parameter does not exists. Please verify.';

  /**
   * Invalid IP address message
   *
   * @var string
   */
  const INVALID_IP_ADDRESS = 'INVALID IP ADDRESS';

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Field selection constants  ///////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Country code (ISO 3166-1 Alpha 2)
   *
   * @var int
   */
  const COUNTRY_CODE = 1;

  /**
   * Country name
   *
   * @var int
   */
  const COUNTRY_NAME = 2;

  /**
   * Region name
   *
   * @var int
   */
  const REGION_NAME = 3;

  /**
   * City name
   *
   * @var int
   */
  const CITY_NAME = 4;

  /**
   * ISP name
   *
   * @var int
   */
  const ISP = 5;

  /**
   * Is proxy
   *
   * @var int
   */
  const IS_PROXY = 6;

  /**
   * Proxy type
   *
   * @var int
   */
  const PROXY_TYPE = 7;

  /**
   * Country name and code
   *
   * @var int
   */
  const COUNTRY = 101;

  /**
   * All fields at once
   *
   * @var int
   */
  const ALL = 1001;

  /**
   * Include the IP address of the looked up IP address
   *
   * @var int
   */
  const IP_ADDRESS = 1002;

  /**
   * Include the IP version of the looked up IP address
   *
   * @var int
   */
  const IP_VERSION = 1003;

  /**
   * Include the IP number of the looked up IP address
   *
   * @var int
   */
  const IP_NUMBER = 1004;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Exception code constants  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Generic exception code
   *
   * @var int
   */
  const EXCEPTION = 10000;

  /**
   * No shmop extension found
   *
   * @var int
   */
  const EXCEPTION_NO_SHMOP = 10001;

  /**
   * Failed to open shmop memory segment for reading
   *
   * @var int
   */
  const EXCEPTION_SHMOP_READING_FAILED = 10002;

  /**
   * Failed to open shmop memory segment for writing
   *
   * @var int
   */
  const EXCEPTION_SHMOP_WRITING_FAILED = 10003;

  /**
   * Failed to create shmop memory segment
   *
   * @var int
   */
  const EXCEPTION_SHMOP_CREATE_FAILED = 10004;

  /**
   * The specified database file was not found
   *
   * @var int
   */
  const EXCEPTION_DBFILE_NOT_FOUND = 10005;

  /**
   * Not enough memory to load database file
   *
   * @var int
   */
  const EXCEPTION_NO_MEMORY = 10006;

  /**
   * No candidate databse files found
   *
   * @var int
   */
  const EXCEPTION_NO_CANDIDATES = 10007;

  /**
   * Failed to open database file
   *
   * @var int
   */
  const EXCEPTION_FILE_OPEN_FAILED = 10008;

  /**
   * Failed to determine the current path
   *
   * @var int
   */
  const EXCEPTION_NO_PATH = 10009;

  /**
   * BCMath extension not installed
   *
   * @var int
   */
  const EXCEPTION_BCMATH_NOT_INSTALLED = 10010;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Caching method constants  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Directly read from the databse file
   *
   * @var int
   */
  const FILE_IO = 100001;

  /**
   * Read the whole database into a variable for caching
   *
   * @var int
   */
  const MEMORY_CACHE = 100002;

  /**
   * Use shared memory objects for caching
   *
   * @var int
   */
  const SHARED_MEMORY = 100003;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Shared memory constants  /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Share memory segment's permissions (for creation)
   *
   * @var int
   */
  const SHM_PERMS = 0600;

  /**
   * Number of bytes to read/write at a time in order to load the shared memory cache (512k)
   *
   * @var int
   */
  const SHM_CHUNK_SIZE = 524288;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Static data  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Column offset mapping
   *
   * Each entry contains an array mapping databse version (0-3) to offset within a record.
   * A value of 0 means the column is not present in the given database version.
   *
   * @access private
   * @static
   * @var array
   */
  private static $columns = [
      self::COUNTRY_CODE => [8, 12, 12, 12],
      self::COUNTRY_NAME => [8, 12, 12, 12],
      self::REGION_NAME  => [0,  0, 16, 16],
      self::CITY_NAME    => [0,  0, 20, 20],
      self::ISP          => [0,  0,  0, 24],
      self::PROXY_TYPE   => [0,  8,  8,  8],
  ];

  /**
   * Column name mapping
   *
   * @access private
   * @static
   * @var array
   */
  private static $names = [
      self::COUNTRY_CODE => 'countryCode',
      self::COUNTRY_NAME => 'countryName',
      self::REGION_NAME  => 'regionName',
      self::CITY_NAME    => 'cityName',
      self::ISP          => 'isp',
      self::IS_PROXY     => 'isProxy',
      self::PROXY_TYPE   => 'proxyType',
      self::IP_ADDRESS   => 'ipAddress',
      self::IP_VERSION   => 'ipVersion',
      self::IP_NUMBER    => 'ipNumber',
  ];

  /**
   * Database names, in order of preference for file lookup
   *
   * @var array
   */
  private static $databases = [
      // IPv4 databases
      'IP2PROXY-IP-PROXYTYPE-COUNTRY-REGION-CITY-ISP',
      'IP2PROXY-IP-PROXYTYPE-COUNTRY-REGION-CITY',
      'IP2PROXY-IP-PROXYTYPE-COUNTRY',
      'IP2PROXY-IP-COUNTRY',

      // IPv6 databases
      'IPV6-PROXYTYPE-COUNTRY-REGION-CITY-ISP',
      'IPV6-PROXYTYPE-COUNTRY-REGION-CITY',
      'IPV6-PROXYTYPE-COUNTRY',
      'IPV6-COUNTRY',
  ];

  /**
   * Static memory buffer to use for MEMORY_CACHE mode, the keys will be BIN filenames and the values their contents
   *
   * @access private
   * @static
   * @var array
   */
  private static $buffer = [];

  /**
   * The machine's float size
   *
   * @access private
   * @static
   * @var int
   */
  private static $floatSize = null;

  /**
   * The configured memory limit
   *
   * @access private
   * @static
   * @var int
   */
  private static $memoryLimit = null;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Caching backend controls  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Caching mode to use (one of FILE_IO, MEMORY_CACHE, or SHARED_MEMORY)
   *
   * @access private
   * @var int
   */
  private $mode;

  /**
   * File pointer to use for FILE_IO mode, BIN filename for MEMORY_CACHE mode, or shared memory id to use for SHARED_MEMORY mode
   *
   * @access private
   * @var resource|int|false
   */
  private $resource = false;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Database metadata  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Database's compilation date
   *
   * @access private
   * @var int
   */
  private $date;

  /**
   * Database's type (0-3)
   *
   * @access private
   * @var int
   */
  private $type;

  /**
   * Database's register width (as an array mapping 4 to IPv4 width, and 6 to IPv6 width)
   *
   * @access private
   * @var array
   */
  private $columnWidth = [];

  /**
   * Database's pointer offset (as an array mapping 4 to IPv4 offset, and 6 to IPv6 offset)
   *
   * @access private
   * @var array
   */
  private $offset = [];

  /**
   * Amount of IP address ranges the database contains (as an array mapping 4 to IPv4 count, and 6 to IPv6 count)
   *
   * @access private
   * @var array
   */
  private $ipCount = [];

  /**
   * Offset withing the database where IP data begins (as an array mapping 4 to IPv4 base, and 6 to IPv6 base)
   *
   * @access private
   * @var array
   */
  private $ipBase = [];

  //hjlim
  private $indexBaseAddr = [];
  private $year;
  private $month;
  private $day;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Default fields  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Default fields to return during lookup
   *
   * @access private
   * @var int|array
   */
  private $defaultFields = self::ALL;

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Administrative public interface  /////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Constructor
   *
   * @access public
   */
  public function __construct() {
  }

  /**
   * Destructor
   *
   * @access public
   */
  public function __destruct() {
  }

  /**
   * @access public
   * @param string $file  Filename of the BIN database to load
   * @param int $mode  Caching mode (one of FILE_IO, MEMORY_CACHE, or SHARED_MEMORY)
   * @throws \Exception
   */
  public function open($file = null, $mode = self::FILE_IO, $defaultFields = self::ALL) {
	if (!function_exists('bcadd')) {
      throw new \Exception(__CLASS__ . ": BCMath extension is not installed.", self::EXCEPTION_BCMATH_NOT_INSTALLED);
	}

    // find the referred file and its size
    $rfile = self::findFile($file);
    $size  = filesize($rfile);

    // initialize caching backend
    switch ($mode) {
      case self::SHARED_MEMORY:
        // verify the shmop extension is loaded
        if (!extension_loaded('shmop')) {
          throw new \Exception(__CLASS__ . ": Please make sure your PHP setup has the 'shmop' extension enabled.", self::EXCEPTION_NO_SHMOP);
        }

        $limit = self::getMemoryLimit();
        if (false !== $limit && $size > $limit) {
          throw new \Exception(__CLASS__ . ": Insufficient memory to load file '{$rfile}'.", self::EXCEPTION_NO_MEMORY);
        }

        $this->mode = self::SHARED_MEMORY;
        $shmKey     = self::getShmKey($rfile);

        // try to open the shared memory segment
        $this->resource = @shmop_open($shmKey, 'a', 0, 0);
        if (false === $this->resource) {
          // the segment did not exist, create it and load the database into it
          $fp = fopen($rfile, 'rb');
          if (false === $fp) {
            throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
          }

          // try to open the memory segment for exclusive access
          $shmId = @shmop_open($shmKey, 'n', self::SHM_PERMS, $size);
          if (false === $shmId) {
            throw new \Exception(__CLASS__ . ": Unable to create shared memory block '{$shmKey}'.", self::EXCEPTION_SHMOP_CREATE_FAILED);
          }

          // load SHM_CHUNK_SIZE bytes at a time
          $pointer = 0;
          while ($pointer < $size) {
            $buf = fread($fp, self::SHM_CHUNK_SIZE);
            shmop_write($shmId, $buf, $pointer);
            $pointer += self::SHM_CHUNK_SIZE;
          }
          shmop_close($shmId);
          fclose($fp);

          // now open the memory segment for readonly access
          $this->resource = @shmop_open($shmKey, 'a', 0, 0);
          if (false === $this->resource) {
            throw new \Exception(__CLASS__ . ": Unable to access shared memory block '{$shmKey}' for reading.", self::EXCEPTION_SHMOP_READING_FAILED);
          }
        }
        break;

      case self::FILE_IO:
        $this->mode     = self::FILE_IO;
        $this->resource = @fopen($rfile, 'rb');
        if (false === $this->resource) {
          throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
        }
        break;

      case self::MEMORY_CACHE:
        $this->mode     = self::MEMORY_CACHE;
        $this->resource = $rfile;
        if (!array_key_exists($rfile, self::$buffer)) {
          $limit = self::getMemoryLimit();
          if (false !== $limit && $size > $limit) {
            throw new \Exception(__CLASS__ . ": Insufficient memory to load file '{$rfile}'.", self::EXCEPTION_NO_MEMORY);
          }

          self::$buffer[$rfile] = @file_get_contents($rfile);
          if (false === self::$buffer[$rfile]) {
            throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
          }
        }
        break;

      default:
    }

    // determine the platform's float size
    //
    // NB: this should be a constant instead, and some unpack / typebanging magic
    //     should be used to accomodate different float sizes, but, as the libreary
    //     is written, this is the sanest thing to do anyway
    //
    if (null === self::$floatSize) {
      self::$floatSize = strlen(pack('f', M_PI));
    }

    // set default fields to retrieve
    $this->defaultFields = $defaultFields;

    // extract database metadata
    $this->type             = $this->readByte(1) - 1;
    $this->columnWidth[4]   = $this->readByte(2) * 4;
    $this->columnWidth[6]   = $this->columnWidth[4] + 12;
    $this->offset[4]        = -4;
    $this->offset[6]        = 8;
    $this->year             = 2000 + $this->readByte(3);
    $this->month            = $this->readByte(4);
    $this->day              = $this->readByte(5);
    $this->date             = date('Y-m-d', strtotime("{$this->year}-{$this->month}-{$this->day}"));
    $this->ipCount[4]       = $this->readWord(6);
    $this->ipBase[4]        = $this->readWord(10);
    $this->ipCount[6]       = $this->readWord(14);
    $this->ipBase[6]        = $this->readWord(18);
    $this->indexBaseAddr[4] = $this->readWord(22);
    $this->indexBaseAddr[6] = $this->readWord(26);
  }

  /**
   * Close
   *
   * @access public
   */
  public function close() {
    switch ($this->mode) {
      case self::FILE_IO:
        // free the file pointer
        if (false !== $this->resource) {
          fclose($this->resource);
          $this->resource = false;
        }
        break;
      case self::SHARED_MEMORY:
        // detach from the memory segment
        if (false !== $this->resource) {
          shmop_close($this->resource);
          $this->resource = false;
        }
        break;
    }
  }

  /**
   * Tear down a shared memory segment created for the given file
   *
   * @access protected
   * @param string $file  Filename of the BIN database whise segment must be deleted
   * @throws \Exception
   */
  protected function shmTeardown($file) {
    // verify the shmop extension is loaded
    if (!extension_loaded('shmop')) {
      throw new \Exception(__CLASS__ . ": Please make sure your PHP setup has the 'shmop' extension enabled.", self::EXCEPTION_NO_SHMOP);
    }

    // Get actual file path
    $rfile = realpath($file);

  // If the file cannot be found, except away
    if (false === $rfile) {
      throw new \Exception(__CLASS__ . ": Database file '{$file}' does not seem to exist.", self::EXCEPTION_DBFILE_NOT_FOUND);
    }

    $shmKey = self::getShmKey($rfile);

  // Try to open the memory segment for writing
    $shmId  = @shmop_open($shmKey, 'w', 0, 0);
    if (false === $shmId) {
      throw new \Exception(__CLASS__ . ": Unable to access shared memory block '{$shmKey}' for writing.", self::EXCEPTION_SHMOP_WRITING_FAILED);
    }

    // Delete and close the descriptor
    shmop_delete($shmId);
    shmop_close($shmId);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Static tools  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Get memory limit from the current PHP settings (return false if no memory limit set)
   *
   * @access private
   * @static
   * @return int|boolean
   */
  private static function getMemoryLimit() {
    // Get values if no cache
    if (null === self::$memoryLimit) {
      $limit = ini_get('memory_limit');

    // Feal with defaults
      if ('' === (string) $limit) {
        $limit = '128M';
      }

      $value = (int) $limit;

    // Deal with "no-limit"
      if ($value < 0) {
        $value = false;
      } else {
        // Deal with shorthand bytes
        switch (strtoupper(substr($limit, -1))) {
          case 'G': $value *= 1024;
          case 'M': $value *= 1024;
          case 'K': $value *= 1024;
        }
      }
      self::$memoryLimit = $value;
    }
    return self::$memoryLimit;
  }

  /**
   * Return the realpath of the given file or look for the first matching database option
   *
   * @param string $file  File to try to find, or null to try the databases in turn on the current file's path
   * @return string
   * @throws \Exception
   */
  private static function findFile($file = null) {
    if (null !== $file) {
      // Get actual file path
      $rfile = realpath($file);

      // If the file cannot be found, except away
      if (false === $rfile) {
        throw new \Exception(__CLASS__ . ": Database file '{$file}' does not seem to exist.", self::EXCEPTION_DBFILE_NOT_FOUND);
      }

      return $rfile;
    } else {
      // Try to get current path
      $current = realpath(dirname(__FILE__));

    if (false === $current) {
        throw new \Exception(__CLASS__ . ": Cannot determine current path.", self::EXCEPTION_NO_PATH);
      }
      // Try each database in turn
      foreach (self::$databases as $database) {
        $rfile = realpath("{$current}/{$database}.BIN");
        if (false !== $rfile) {
          return $rfile;
        }
      }
      // No candidates found
      throw new \Exception(__CLASS__ . ": No candidate database files found.", self::EXCEPTION_NO_CANDIDATES);
    }
  }

  /**
   * Make the given number positive by wrapping it to 8 bit values
   *
   * @access private
   * @static
   * @param int $x  Number to wrap
   * @return int
   */
  private static function wrap8($x) {
    return $x + ($x < 0 ? 256 : 0);
  }

  /**
   * Make the given number positive by wrapping it to 32 bit values
   *
   * @access private
   * @static
   * @param int $x  Number to wrap
   * @return int
   */
  private static function wrap32($x) {
    return $x + ($x < 0 ? 4294967296 : 0);
  }

  /**
   * Generate a unique and repeatable shared memory key for each instance to use
   *
   * @access private
   * @static
   * @param string $filename  Filename of the BIN file
   * @return int
   */
  private static function getShmKey($filename) {
    // This will create a shared memory key that deterministically depends only on
    // the current file's path and the BIN file's path
    return (int) sprintf('%u', self::wrap32(crc32(__FILE__ . ':' . $filename)));
  }

  /**
   * Determine whether the given IP number of the given version lies between the given bounds
   *
   * This function will return 0 if the given ip number falls within the given bounds
   * for the given version, -1 if it falls below, and 1 if it falls above.
   *
   * @access private
   * @static
   * @param int $version  IP version to use (either 4 or 6)
   * @param int|string $ip  IP number to check (int for IPv4, string for IPv6)
   * @param int|string $low  Lower bound (int for IPv4, string for IPv6)
   * @param int|string $high  Uppoer bound (int for IPv4, string for IPv6)
   * @return int
   */
  private static function ipBetween($version, $ip, $low, $high) {
    if (4 === $version) {
      // Use normal PHP ints
      if ($low <= $ip) {
        if ($ip < $high) {
          return 0;
        } else {
          return 1;
        }
      } else {
        return -1;
      }
    } else {
      // Use BCMath
      if (bccomp($low, $ip, 0) <= 0) {
        if (bccomp($ip, $high, 0) <= -1) {
          return 0;
        } else {
          return 1;
        }
      } else {
        return -1;
      }
    }
  }

  /**
   * Get the IP version and number of the given IP address
   *
   * This method will return an array, whose components will be:
   * - first: 4 if the given IP address is an IPv4 one, 6 if it's an IPv6 one,
   *          or fase if it's neither.
   * - second: the IP address' number if its version is 4, the number string if
   *           its version is 6, false otherwise.
   *
   * @access private
   * @static
   * @param string $ip  IP address to extract the version and number for
   * @return array
   */
  private static function ipVersionAndNumber($ip) {
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
      return [4, sprintf('%u', ip2long($ip))];
    } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
    $result = 0;

    foreach (str_split(bin2hex(inet_pton($ip)), 8) as $word) {
        $result = bcadd(bcmul($result, '4294967296', 0), self::wrap32(hexdec($word)), 0);
      }

    return [6, $result];
    } else {
      // Invalid IP address, return falses
      return [false, false];
    }
  }

  /**
   * Return the decimal string representing the binary data given
   *
   * @access private
   * @static
   * @param string $data  Binary data to parse
   * @return string
   */
  private static function bcBin2Dec($data) {
  $parts = array(
    unpack('V', substr($data, 12, 4)),
    unpack('V', substr($data, 8, 4)),
    unpack('V', substr($data, 4, 4)),
    unpack('V', substr($data, 0, 4)),
  );

  foreach($parts as &$part)
    if($part[1] < 0)
      $part[1] += 4294967296;

  $result = bcadd(bcadd(bcmul($parts[0][1], bcpow(4294967296, 3)), bcmul($parts[1][1], bcpow(4294967296, 2))), bcadd(bcmul($parts[2][1], 4294967296), $parts[3][1]));

    return $result;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Caching backend abstraction  /////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Low level read function to abstract away the caching mode being used
   *
   * @access private
   * @param int $pos  Position from where to start reading
   * @param int $len  Read this many bytes
   * @return string
   */
  private function read($pos, $len) {
    switch ($this->mode) {
      case self::SHARED_MEMORY:
        return shmop_read($this->resource, $pos, $len);

      case self::MEMORY_CACHE:
        return $data = substr(self::$buffer[$this->resource], $pos, $len);

      default:
        fseek($this->resource, $pos, SEEK_SET);
        return fread($this->resource, $len);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Low-level read functions  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Low level function to fetch a string from the caching backend
   *
   * @access private
   * @param int $pos  Position to read from
   * @param int $additional  Additional offset to apply
   * @return string
   */
  private function readString($pos, $additional = 0) {
    // Get the actual pointer to the string's head
    $spos = $this->readWord($pos) + $additional;

  // Read as much as the length (first "string" byte) indicates
    return $this->read($spos + 1, $this->readByte($spos + 1));
  }

  /**
   * Low level function to fetch a float from the caching backend
   *
   * @access private
   * @param int $pos  Position to read from
   * @return float
   */
  private function readFloat($pos) {
    // Unpack a float's size worth of data
    return unpack('f', $this->read($pos - 1, self::$floatSize))[1];
  }

  /**
   * Low level function to fetch a quadword (128 bits) from the caching backend
   *
   * @access private
   * @param int $pos  Position to read from
   * @return string
   */
  private function readQuad($pos) {
    // Use BCMath ints to get a quad's (128-bit) value
    return self::bcBin2Dec($this->read($pos - 1, 16));
  }

  /**
   * Low level function to fetch a word (32 bits) from the caching backend
   *
   * @access private
   * @param int $pos  Position to read from
   * @return int
   */
  private function readWord($pos) {
    // Unpack a long's worth of data
    return self::wrap32(unpack('V', $this->read($pos - 1, 4))[1]);
  }

  /**
   * Low level function to fetch a byte from the caching backend
   *
   * @access private
   * @param int $pos  Position to read from
   * @return string
   */
  private function readByte($pos) {
    // Unpack a byte's worth of data
    return self::wrap8(unpack('C', $this->read($pos - 1, 1))[1]);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  High-level read functions  ///////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * High level function to fetch the country name and code
   *
   * @access private
   * @param int|boolean $pointer  Position to read from, if false, return self::INVALID_IP_ADDRESS
   * @return array
   */
  private function readCountryNameAndCode($pointer) {
    if (false === $pointer) {
      // Deal with invalid IPs
      $countryCode = self::INVALID_IP_ADDRESS;
      $countryName = self::INVALID_IP_ADDRESS;
    } elseif (0 === self::$columns[self::COUNTRY_CODE][$this->type]) {
      // If the field is not suported, return accordingly
      $countryCode = self::FIELD_NOT_SUPPORTED;
      $countryName = self::FIELD_NOT_SUPPORTED;
    } else {
      // Read the country code and name (the name shares the country's pointer,
      // but it must be artificially displaced 3 bytes ahead: 2 for the country code, one
      // for the country name's length)
      $countryCode = $this->readString($pointer + self::$columns[self::COUNTRY_CODE][$this->type]);
      $countryName = $this->readString($pointer + self::$columns[self::COUNTRY_NAME][$this->type], 3);
    }

    return [$countryName, $countryCode];
  }

  /**
   * High level function to fetch the region name
   *
   * @access private
   * @param int $pointer  Position to read from, if false, return self::INVALID_IP_ADDRESS
   * @return string
   */
  private function readRegionName($pointer) {
    if (false === $pointer) {
      // Deal with invalid IPs
      $regionName = self::INVALID_IP_ADDRESS;
    } elseif (0 === self::$columns[self::REGION_NAME][$this->type]) {
      // If the field is not suported, return accordingly
      $regionName = self::FIELD_NOT_SUPPORTED;
    } else {
      // Read the region name
      $regionName = $this->readString($pointer + self::$columns[self::REGION_NAME][$this->type]);
    }
    return $regionName;
  }

  /**
   * High level function to fetch the city name
   *
   * @access private
   * @param int $pointer  Position to read from, if false, return self::INVALID_IP_ADDRESS
   * @return string
   */
  private function readCityName($pointer) {
    if (false === $pointer) {
      // Deal with invalid IPs
      $cityName = self::INVALID_IP_ADDRESS;
    } elseif (0 === self::$columns[self::CITY_NAME][$this->type]) {
      // If the field is not suported, return accordingly
      $cityName = self::FIELD_NOT_SUPPORTED;
    } else {
      // Read the city name
      $cityName = $this->readString($pointer + self::$columns[self::CITY_NAME][$this->type]);
    }
    return $cityName;
  }

  /**
   * High level function to fetch the ISP name
   *
   * @access private
   * @param int $pointer  Position to read from, if false, return self::INVALID_IP_ADDRESS
   * @return string
   */
  private function readIsp($pointer) {
    if (false === $pointer) {
      // Deal with invalid IPs
      $isp = self::INVALID_IP_ADDRESS;
    } elseif (0 === self::$columns[self::ISP][$this->type]) {
      // If the field is not suported, return accordingly
      $isp = self::FIELD_NOT_SUPPORTED;
    } else {
      // Read isp name
      $isp = $this->readString($pointer + self::$columns[self::ISP][$this->type]);
    }
    return $isp;
  }

  /**
   * High level function to fetch the proxy type
   *
   * @access private
   * @param int $pointer  Position to read from, if false, return self::INVALID_IP_ADDRESS
   * @return string
   */
  private function readProxyType($pointer) {
    if (false === $pointer) {
      // Deal with invalid IPs
      $proxyType = self::INVALID_IP_ADDRESS;
    } elseif (0 === self::$columns[self::PROXY_TYPE][$this->type]) {
      // If the field is not suported, return accordingly
      $proxyType = self::FIELD_NOT_SUPPORTED;
    } else {
      // Read proxy type
      $proxyType = $this->readString($pointer + self::$columns[self::PROXY_TYPE][$this->type]);
    }
    return $proxyType;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Binary search and support functions  /////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * High level fucntion to read an IP address of the given version
   *
   * @access private
   * @param int $version  IP version to read (either 4 or 6, returns false on anything else)
   * @param int $pos  Position to read from
   * @return int|string|boolean
   */
  private function readIp($version, $pos) {
    if (4 === $version) {
      // Read a standard PHP int
      return self::wrap32($this->readWord($pos));
    } elseif (6 === $version) {
      // Read as BCMath int (quad)
      return $this->readQuad($pos);
    } else {
      // unrecognized
      return false;
    }
  }

  /**
   * Perform a binary search on the given IP number and return a pointer to its record
   *
   * @access private
   * @param int $version  IP version to use for searching
   * @param int $ipNumber  IP number to look for
   * @return int|boolean
   */
  private function binSearch($version, $ipNumber) {
    if (false === $version) {
      // unrecognized version
      return false;
    }

    // initialize fields
    $base   = $this->ipBase[$version];
    $offset = $this->offset[$version];
    $width  = $this->columnWidth[$version];
    $high   = $this->ipCount[$version];
    $low    = 0;

  //hjlim
  $indexBaseStart = $this->indexBaseAddr[$version];
  if ($indexBaseStart > 0){
    $indexPos = 0;
    switch($version){
      case 4:
        $ipNum1_2 = intval($ipNumber / 65536);
        $indexPos = $indexBaseStart + ($ipNum1_2 << 3);

        break;

      case 6:
        $ipNum1 = intval(bcdiv($ipNumber, bcpow('2', '112')));
        $indexPos = $indexBaseStart + ($ipNum1 << 3);

        break;

      default:
        return false;
    }

    $low = $this->readWord($indexPos);
    $high = $this->readWord($indexPos + 4);
  }

    // as long as we can narrow down the search...
  while ($low <= $high) {
      $mid     = (int) ($low + (($high - $low) >> 1));

    // Read IP ranges to get boundaries
      $ip_from = $this->readIp($version, $base + $width * $mid);
      $ip_to   = $this->readIp($version, $base + $width * ($mid + 1));

      // determine whether to return, repeat on the lower half, or repeat on the upper half
      switch (self::ipBetween($version, $ipNumber, $ip_from, $ip_to)) {
        case 0:
      return $base + $offset + $mid * $width;
        case -1:
          $high = $mid - 1;
          break;
        case 1:
          $low  = $mid + 1;
          break;
      }
    }

    // nothing found
    return false;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  Public interface  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Get the database's package (1-4)
   *
   * @access public
   * @return int
   */
  public function getPackageVersion() {
    return $this->type + 1;
  }

  /**
   * Return this database's available fields
   *
   * @access protected
   * @param boolean $asNames  Whether to return the mapped names intead of numbered constants
   * @return array
   */
  protected function getFields($asNames = false) {
    $result = array_keys(array_filter(self::$columns, function ($field) {
      return 0 !== $field[$this->type];
    }));

    if ($asNames) {
      $return = [];
      foreach ($result as $field) {
        $return[] = self::$names[$field];
      }
      return $return;
    } else {
      return $result;
    }
  }

  /**
   * Return the version of module
   */
  public function getModuleVersion() {
    return self::VERSION;
  }

  /**
   * Return the version of module
   */
  public function getDatabaseVersion() {
    return $this->year . '.' . $this->month . '.' . $this->day;
  }

  /**
   *  Return -1, 0, 1, 2
  */
  public function isProxy($ip) {
    return self::lookup($ip, self::IS_PROXY);
  }

  /*
   * Return string
  */
  public function getCountryShort($ip) {
    return self::lookup($ip, self::COUNTRY_CODE);
  }

  /*
   * Return string
  */
  public function getCountryLong($ip) {
    return self::lookup($ip, self::COUNTRY_NAME);
  }

  /*
   * Return string
  */
  public function getRegion($ip) {
    return self::lookup($ip, self::REGION_NAME);
  }

  /*
   * Return string
  */
  public function getCity($ip) {
    return self::lookup($ip, self::CITY_NAME);
  }

  /*
   * Return string
  */
  public function getISP($ip) {
    return self::lookup($ip, self::ISP);
  }

  /*
   * Return string
  */
  public function getProxyType($ip) {
    return self::lookup($ip, self::PROXY_TYPE);
  }

  /*
   * Return array
  */
  public function getAll($ip) {
    return self::lookup($ip, self::ALL);
  }

  /**
   * This function will look the given IP address up in the database and return the result(s) asked for
   *
   * If a single, SINGULAR, field is specified, only its mapped value is returned.
   * If many fields are given (as an array) or a MULTIPLE field is specified, an
   * array whith the returned singular field names as keys and their corresponding
   * values is returned.
   *
   * @access protected
   * @param string $ip  IP address to look up
   * @param int|array $fields  Field(s) to return
   * @param boolean $asNamed  Whether to return an associative array instead
   * @return mixed|array|boolean
   */
  protected function lookup($ip, $fields = null, $asNamed = true) {
    // extract IP version and number
    list($ipVersion, $ipNumber) = self::ipVersionAndNumber($ip);
    // perform the binary search proper (if the IP address was invalid, binSearch will return false)
    $pointer = $this->binSearch($ipVersion, $ipNumber);

    // apply defaults if needed
    if (null === $fields) {
      $fields = $this->defaultFields;
    }

    // turn fields into an array in case it wasn't already
    $ifields = (array) $fields;
    // add fields if needed
    if (in_array(self::ALL, $ifields)) {
      $ifields[] = self::REGION_NAME;
      $ifields[] = self::CITY_NAME;
      $ifields[] = self::ISP;
      $ifields[] = self::IS_PROXY;
      $ifields[] = self::PROXY_TYPE;
      $ifields[] = self::COUNTRY;
      $ifields[] = self::IP_ADDRESS;
      $ifields[] = self::IP_VERSION;
      $ifields[] = self::IP_NUMBER;
    }
    // turn into a uniquely-valued array the fast way
    // (see: http://php.net/manual/en/function.array-unique.php#77743)
    $afields = array_keys(array_flip($ifields));
    // sorting them in reverse order warrants that by the time we get to
    // SINGULAR fields, its MULTIPLE counterparts, if at all present, have
    // already been retrieved
    rsort($afields);

    // maintain a list of already retrieved fields to avoid doing it twice
    $done    = [
        self::COUNTRY_CODE => false,
        self::COUNTRY_NAME => false,
        self::REGION_NAME  => false,
        self::CITY_NAME    => false,
        self::ISP          => false,
        self::IS_PROXY     => false,
        self::PROXY_TYPE   => false,
        self::COUNTRY      => false,
        self::IP_ADDRESS   => false,
        self::IP_VERSION   => false,
        self::IP_NUMBER    => false,
    ];
    // results are empty to begin with
    $results = [];

    // treat each field in turn
    foreach ($afields as $afield) {
      switch ($afield) {
        // purposefully ignore self::ALL, we already dealt with it
        case self::ALL: break;
        //
        case self::COUNTRY:
          if (!$done[self::COUNTRY]) {
            list($results[self::COUNTRY_NAME], $results[self::COUNTRY_CODE]) = $this->readCountryNameAndCode($pointer);
            $done[self::COUNTRY]      = true;
            $done[self::COUNTRY_CODE] = true;
            $done[self::COUNTRY_NAME] = true;
          }
          break;
        case self::COUNTRY_CODE:
          if (!$done[self::COUNTRY_CODE]) {
            $results[self::COUNTRY_CODE] = $this->readCountryNameAndCode($pointer)[1];
            $done[self::COUNTRY_CODE]    = true;
          }
          break;
        case self::COUNTRY_NAME:
          if (!$done[self::COUNTRY_CODE]) {
            $results[self::COUNTRY_CODE] = $this->readCountryNameAndCode($pointer)[0];
            $done[self::COUNTRY_CODE]    = true;
          }
          break;
        case self::REGION_NAME:
          if (!$done[self::REGION_NAME]) {
            $results[self::REGION_NAME] = $this->readRegionName($pointer);
            $done[self::REGION_NAME]    = true;
          }
          break;
        case self::CITY_NAME:
          if (!$done[self::CITY_NAME]) {
            $results[self::CITY_NAME] = $this->readCityName($pointer);
            $done[self::CITY_NAME]    = true;
          }
          break;
        case self::ISP:
          if (!$done[self::ISP]) {
            $results[self::ISP] = $this->readIsp($pointer);
            $done[self::ISP]    = true;
          }
          break;
    case self::PROXY_TYPE:
          if (!$done[self::PROXY_TYPE]) {
            $results[self::PROXY_TYPE] = $this->readProxyType($pointer);
            $done[self::PROXY_TYPE]    = true;
          }
          break;
    case self::IS_PROXY:
          if (!$done[self::IS_PROXY]) {
      // px1
      if ($this->type == 0) {
        $countryCode = $this->readCountryNameAndCode($pointer)[1];

        $results[self::IS_PROXY] = ($countryCode == '-') ? 0 : 1;

        if (strlen($countryCode) > 2) {
          $results[self::IS_PROXY] = -1;
        }
      }
      else{
        $proxyType = $this->readProxyType($pointer);

        $results[self::IS_PROXY] = ($proxyType == '-') ? 0 : (($proxyType == 'DCH') ? 2 : 1);

        if (strlen($proxyType) > 3) {
          $results[self::IS_PROXY] = -1;
        }
      }

            $done[self::IS_PROXY]    = true;
          }
          break;
        case self::IP_ADDRESS:
          if (!$done[self::IP_ADDRESS]) {
            $results[self::IP_ADDRESS] = $ip;
            $done[self::IP_ADDRESS]    = true;
          }
          break;
        case self::IP_VERSION:
          if (!$done[self::IP_VERSION]) {
            $results[self::IP_VERSION] = $ipVersion;
            $done[self::IP_VERSION]    = true;
          }
          break;
        case self::IP_NUMBER:
          if (!$done[self::IP_NUMBER]) {
            $results[self::IP_NUMBER] = $ipNumber;
            $done[self::IP_NUMBER]    = true;
          }
          break;
        //
        default:
          $results[$afield] = self::FIELD_NOT_KNOWN;
      }
    }

    // If we were asked for an array, or we have multiple results to return...
    if (is_array($fields) || count($results) > 1) {
      // return array
      if ($asNamed) {
        // apply translations if needed
        $return = [];
        foreach ($results as $key => $val) {
          if (array_key_exists($key, static::$names)) {
            $return[static::$names[$key]] = $val;
          } else {
            $return[$key] = $val;
          }
        }
        return $return;
      } else {
        return $results;
      }
    } else {
      // return a single value
      return array_values($results)[0];
    }
  }
}


<?php
/**
 * Sastrawi Tokenizer (https://github.com/sastrawi/tokenizer)
 *
 * @link      http://github.com/sastrawi/tokenizer for the canonical source repository
 * @license   https://github.com/sastrawi/tokenizer/blob/master/LICENSE The MIT License (MIT)
 */

namespace Sastrawi\Tokenizer;

use Sastrawi\Tokenizer\CharAnalyzer\AnalyzerInterface;
use Sastrawi\Trka\Finder\EntityFinderFactory;

/**
 * Default tokenizer for Bahasa Indonesia.
 *
 * @author Andy Librian
 */
class DefaultTokenizer implements TokenizerInterface
{
    /**
     * @var \Sastrawi\Tokenizer\CharAnalyzer\AnalyzerInterface[]
     */
    private $analyzers = array();

    private $entityFinder;

    public function __construct()
    {
        $this->addAnalyzer(new CharAnalyzer\Alphanumeric());
        $this->addAnalyzer(new CharAnalyzer\Whitespace());
        $this->addAnalyzer(new CharAnalyzer\Punctuation());
        $this->addAnalyzer(new CharAnalyzer\Hyphen());

        $entityFinderFactory = new EntityFinderFactory();
        $this->entityFinder  = $entityFinderFactory->create();
    }

    public function addAnalyzers(array $analyzers)
    {
        foreach ($analyzers as $analyzer) {
            $this->addAnalyzer($analyzer);
        }
    }

    public function addAnalyzer(AnalyzerInterface $analyzer)
    {
        $this->analyzers[] = $analyzer;
    }

    public function getAnalyzers()
    {
        return $this->analyzers;
    }

    /**
     * {@inheritdoc}
     */
    public function tokenize($text)
    {
        $tokens = array();
        $tokenBuffer = '';

        $entities = $this->entityFinder->find($text);

        for ($i = 0; $i < strlen($text); $i++) {
            $model = new CharAnalyzer\Model($text, $i);

            $analyzerResultFalse = 0;
            $analyzerResultTrue  = 0;
            $analyzerResultNull  = 0;
            $analyzerResult = null;

            foreach ($this->analyzers as $analyzer) {
                $analyzerResult = $analyzer->shouldSplit($model);

                if ($analyzerResult === true) {
                    $analyzerResultTrue++;
                } elseif ($analyzerResult === false) {
                    $analyzerResultFalse++;
                } elseif ($analyzerResult === null) {
                    $analyzerResultNull++;
                }
            }

            if ($this->isCharEntity($i, $entities)) {
                // don't split entity
                $tokenBuffer .= $model->getCurrentChar();
            } elseif ($analyzerResultTrue > 0 && $analyzerResultTrue >= $analyzerResultFalse) {
                if ($model->getCurrentChar() !== ' ') {
                    if (!empty($tokenBuffer)) {
                        $tokens[] = $tokenBuffer;
                        $tokenBuffer = '';
                    }

                    $tokenBuffer = $model->getCurrentChar();
                } else {
                    if (!empty($tokenBuffer)) {
                        $tokens[] = $tokenBuffer;
                        $tokenBuffer = '';
                    }
                }
            } else {
                $tokenBuffer .= $model->getCurrentChar();
            }
        }

        if (!empty($tokenBuffer)) {
            $tokens[] = $tokenBuffer;
        }

        return $tokens;
    }

    private function isCharEntity($charPos, array $entities)
    {
        foreach ($entities as $span) {
            if ($span->containsIndex($charPos)) {
                return true;
            }
        }

        return false;
    }
}


<?php
namespace execut\widget;
use execut\yii\jui\Widget;
use yii\base\Exception;
use yii\helpers\Html;

/**
 * Bootstrap Tree View wrapper for yii2
 *
 * @property string $size Widget size mode TreeView::SIZE_SMALL | TreeView::SIZE_MIDDLE | TreeView::SIZE_NORMAL
 * @property string $defaultIcon Default icon for each data item
 * @property array $data Tree data
 *
 * @author eXeCUT
 */
class TreeView extends Widget {
    const SIZE_SMALL = 'small';
    const SIZE_MIDDLE = 'middle';
    const SIZE_NORMAL = 'normal';
    const TEMPLATE_ADVANCED = '

{header}

            {search}

            {tree}

';
    const TEMPLATE_SIMPLE = '{tree}';

    /**
     * @var string Widget size
     */
    public $size = TreeView::SIZE_NORMAL;

    /**
     * @var string Icon class from bootstrap
     */
    public $defaultIcon = 'none';

    /**
     * @var array Items list for widget
     */
    public $data = [];

    /**
     * Main container html options
     *
     * @var array
     */
    public $containerOptions = [];

    /**
     * Searvh widget options
     *
     * @var string
     */
    public $searchOptions = [];

    /**
     * Widget header
     *
     * @var string
     */
    public $header = 'Select from tree';

    /**
     * Template for render widget
     *
     * @var string
     */
    public $template = self::TEMPLATE_ADVANCED;

    /**
     * Run widget
     */
    public function run() {
        if ($this->size !== self::SIZE_NORMAL) {
            $this->options['class'] = $this->size;
        }

        $parts = [
            '{tree}' =>  Html::tag('div', '', $this->options)
        ];
        $template = $this->template;
        if (strpos($this->template, '{tree}') === false) {
            throw new Exception('{tree} not found in widget template');
        }

        $parts = [
            '{tree}' => Html::tag('div', '', $this->options),
            '{header}' => $this->header,
        ];

        if (strpos($this->template, '{search}') !== false) {
            $parts['{search}'] = $this->renderSearchWidget();
        }

        echo Html::tag('div', strtr($this->template, $parts), $this->containerOptions);

        $this->_initDefaultIcon($this->data);
        $this->clientOptions['data'] = $this->data;

        $this->registerWidget('treeview');
    }

    protected function renderSearchWidget() {
        $options = $this->searchOptions;
        $options['treeViewId'] = $this->id;
        if (empty($options['inputOptions'])) {
            $options['inputOptions'] = [];
        }

        if (empty($options['inputOptions']['placeholder'])) {
            $options['inputOptions']['placeholder'] = 'Search...';
        }

        return TreeFilterInput::widget($options);
    }

    /**
     * @param &array $data
     */
    protected function _initDefaultIcon(&$data)
    {
        foreach ($data as &$row) {
            if (empty($row['icon'])) {
                $row['icon'] = $this->defaultIcon;
            }

            if (isset($row['nodes'])) {
                $this->_initDefaultIcon($row['nodes']);
            }
        }
    }
}


<?php

namespace getjump\Vk\Wrapper;

use getjump\Vk\Constants\LongPolling as LP;
use getjump\Vk\Model;

/**
 * Class LongPoll
 * Implements LongPolling part of API.
 */
class LongPoll extends BaseWrapper
{
    const URL_CONNECTION_INFO = 'http://%s?act=a_check&key=%s&ts=%s&wait=25&mode=2';
    const LONG_POOL_REQUEST_METHOD = 'messages.getLongPollServer';

    public $userCache = [];
    public $userMap = [];

    /**
     * Return connection info for long polling.
     *
     * @param $d
     *
     * @return string
     */
    public function getConnectionInfo($d)
    {
        return sprintf(self::URL_CONNECTION_INFO, $d->server, $d->key, $d->ts);
    }

    /**
     * Get connection info for long polling from api.
     *
     * @return array|bool
     */
    public function getServerData()
    {
        return $this->vk->request(self::LONG_POOL_REQUEST_METHOD)->response->getResponse();
    }

    /**
     * LongPoll loop.
     *
     * @todo make something other than putting data to STDOUT
     */
    public function doLoop()
    {
        $server = $this->getServerData();
        $initial = $this->getConnectionInfo($server);

        $user = new User($this->vk);

        $userMap = [];
        $userCache = [];

        /*
         * @param $id
         * @return Model\User
         */
        $fetchData = function ($id) use ($user, &$userMap, &$userCache) {
            if (!isset($userMap[$id])) {
                $userMap[$id] = count($userCache);
                $userCache[] = $user->get($id)->response->get();
            }

            return $userCache[$userMap[$id]];
        };

        while ($data = $this->guzzle->get($initial)->json(['object' => true])) {
            $server->ts = $data->ts;
            $initial = $this->getConnectionInfo($server);

            foreach ($data->updates as $update) {
                /*
                 * @var $user Model\User
                 */
                switch ($update[0]) {
                    case LP::MESSAGE_ADD:
                        $user = $fetchData($update[3]);
                        if ($update[2] & 2) {
                            continue;
                        }
                        printf("New message from %s '%s'\n", $user->getName(), $update[6]);
                        var_dump($update);
                        break;
                    case LP::MESSAGE_WRITE:
                        $user = $fetchData($update[1]);
                        printf("User %s writing\n", $user->getName());
                        break;
                    case LP::DATA_ADD:
                        break;
                    case LP::FRIEND_OFFLINE:
                        $user = $fetchData($update[1] * -1);
                        $status = $update[2] == 1 ? 'AFK' : 'Exit';
                        printf("User %s offline(%s)\n", $user->getName(), $status);
                        break;
                    case LP::FRIEND_ONLINE:
                        $user = $fetchData($update[1] * -1);
                        printf("User %s online\n", $user->getName());
                        break;
                    default:
                        printf("Unknown event %s {%s}\n", $update[0], implode(',', $update));
                        break;
                }
            }
        }
    }
}


<?php

namespace getjump\Vk;

use getjump\Vk\Response\Api;

/**
 * Class BatchTransaction
 * Used for processing data from api methods that supports count and offset.
 */
class BatchTransaction implements \Iterator
{
    private $position = 0;
    private $count = 0;
    private $transaction = null;
    private $eof = false;

    public function __construct(RequestTransaction $transaction, $count = 0)
    {
        $this->transaction = $transaction;
        $this->position = 0;
        $this->count = $count;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Return the current element.
     *
     * @link http://php.net/manual/en/iterator.current.php
     *
     * @return Api
     */
    public function current()
    {
        $this->transaction->incrementOffset($this->position * $this->count);
        $d = $this->transaction->fetchData();
        $count = count($d->response->items);
        $this->eof = $count > 0 && $count >= $this->count ? false : true;

        return $d;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Move forward to next element.
     *
     * @link http://php.net/manual/en/iterator.next.php
     *
     * @return void Any returned value is ignored.
     */
    public function next()
    {
        $this->position++;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Return the key of the current element.
     *
     * @link http://php.net/manual/en/iterator.key.php
     *
     * @return mixed scalar on success, or null on failure.
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Checks if current position is valid.
     *
     * @link http://php.net/manual/en/iterator.valid.php
     *
     * @return bool The return value will be casted to boolean and then evaluated.
     *              Returns true on success or false on failure.
     */
    public function valid()
    {
        return !$this->eof;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Rewind the Iterator to the first element.
     *
     * @link http://php.net/manual/en/iterator.rewind.php
     *
     * @return void Any returned value is ignored.
     */
    public function rewind()
    {
        $this->position = 0;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: getju_000
 * Date: 04.05.14
 * Time: 13:47.
 */
namespace getjump\Vk;

use getjump\Vk\Response\Api;
use stdClass;

/**
 * Class VkJs
 * Used to construct pseudo JavaScript.
 */
class VkJs
{
    /**
     * Will contain pseudo JavaScript.
     *
     * @var bool|string
     */
    public $dataString = false;

    /**
     * Here we store instances of requests.
     *
     * @var bool|array
     */
    private $requests = false;

    /**
     * Instance of Core class.
     *
     * @var bool|Core
     */
    private $vk = false;

    /**
     * Callable that used against every element in returning array.
     *
     * @var bool
     */
    private $callback = false;

    /**
     * Just a constructor.
     *
     * @param Core       $vk
     * @param string     $methodName
     * @param bool|array $args
     */
    public function __construct(Core $vk, $methodName, $args = false)
    {
        $arg = [];
        if ($args) {
            foreach ($args as $k => $v) {
                $arg[] = sprintf('"%s" : "%s"', $k, $v);
            }
        }
        if ($vk instanceof Core) {
            $this->vk = $vk;
        }
        $this->dataString = sprintf('API.%s({%s})', $methodName, implode(', ', $arg));
    }

    /**
     * We wanna send our current VkJs to Vk execute.
     *
     * @return RequestTransaction|Api
     */
    public function execute()
    {
        $execute = $this->dataString;
        if ($this->requests) {
            foreach ($this->requests as $request) {
                $execute .= ','.$request->dataString;
            }
            $execute = '['.$execute.']';

            if (!$this->callback && !$this->vk->jsCallback) {
                $this->vk->jsCallback = true;
                $oldCallback = $this->vk->callback;

                $this->callback = function ($data) use (&$oldCallback) {
                    $std = new stdClass();
                    $std->response = $data;

                    return new Api($std, $oldCallback);
                };

                $this->vk->createAs($this->callback);
            }
        }

        return $this->vk->request('execute', ['code' => sprintf('return %s;', $execute)]);
    }

    /**
     * Method will append one VkJs object, to another one.
     *
     * @param VkJs $js
     *
     * @return $this
     */
    public function append(VkJs $js)
    {
        $this->requests[] = $js;

        return $this;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: getju_000
 * Date: 02.05.14
 * Time: 17:19.
 */
namespace getjump\Vk\Wrapper;

use getjump\Vk\Model\User;
use getjump\Vk\RequestTransaction;
use getjump\Vk\Response\Api;

/**
 * Class Friends
 * Implements some logic for API part that working with friends.
 */
class Friends extends BaseWrapper
{
    /**
     * Return friends of a user with $userId.
     *
     * @param int|string $userId
     * @param bool       $fields
     *
     * @return Api|RequestTransaction
     */
    public function get($userId, $fields = false)
    {
        return $this->vk->param('user_id', $userId)
            ->param('fields', $this->fieldsToString($fields), null)
            ->param('order', 'hints')
            ->createAs(function ($d) {
                return new User($d);
            })
            ->request('friends.get');
    }

    /**
     * @param $data
     *
     * @return null|string
     *
     * @todo Put this to another place (duplicate with User)
     */
    public function fieldsToString($data)
    {
        if (!$data) {
            return;
        }

        return implode(',', $data);
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: getju_000
 * Date: 02.05.14
 * Time: 16:25.
 */
namespace getjump\Vk\Response;

use Closure;

/**
 * Class Response.
 */
class Response implements \ArrayAccess, \Countable, \Iterator
{
    /**
     * @var bool|array
     */
    public $items = false;
    public $count = false;

    /**
     * They can return just an response array.
     *
     * @var bool|array
     */
    public $data = false;

    private $pointer = 0;
    private $extendedFields = [];

    /**
     * Response constructor.
     *
     * @param $data
     * @param bool|Closure $callback
     */
    public function __construct($data, $callback = false)
    {
        foreach ($data as $key => $value) {
            if (property_exists($this, $key)) {
                continue;
            }

            $this->{$key} = $value;
            $this->extendedFields[] = $key;
        }

        if (is_callable($callback) && isset($data->items)) {
            foreach ($data->items as $d) {
                $this->items[] = call_user_func_array($callback, [$d]);
            }
        } else {
            $this->items = !isset($data->items) ? false : $data->items;
        }
        $this->count = !isset($data->count) ? false : $data->count;
        if (is_array($data) || !isset($data->items)) {
            $this->count = count($data);
            if (is_array($data) && is_callable($callback)) {
                foreach ($data as $d) {
                    $this->data[] = call_user_func_array($callback, [$d]);
                }
            } else {
                $this->data = $data;
            }
        }

        // TODO: Avoid hack
        if ($this->data) {
            $this->items = &$this->data;
        }
        if ($this->items) {
            $this->data = &$this->items;
        }

        if (is_object($data) && is_callable($callback)) {
            $this->data = call_user_func_array($callback, [$data]);
        }
    }

    /**
     * This method takes Closure as argument, so every element from response will go into this Closure.
     *
     * @param Closure $callback
     */
    public function each(Closure $callback)
    {
        if (!is_callable($callback)) {
            return;
        }
        $data = [];
        $this->items ? $data = &$this->items : (!$this->data ?: $data = &$this->data);
        foreach ($data as $k => $v) {
            call_user_func_array($callback, [$k, $v]);
        }
    }

    /**
     * This method will return one element if id is not specified or element of array otherwise.
     *
     * @param bool|int $id
     *
     * @return mixed
     */
    public function get($id = false)
    {
        if (!$id) {
            if (is_array($this->data)) {
                return $this->data[0];
            } elseif (isset($this->items) && $this->items !== false) {
                return $this->items[0];
            } else {
                return $this->data;
            }
        } else {
            return $this->data[$id];
        }
    }

    public function extended()
    {
        $temp = [];

        foreach ($this->extendedFields as $key) {
            $temp[$key] = $this->{$key};
        }

        return (object) $temp;
    }

    /**
     * This magic method try to return field from response.
     *
     * @param $name
     *
     * @return bool
     */
    public function __get($name)
    {
        if (!is_array($this->data)) {
            return $this->data->{$name};
        } elseif (count($this->data) == 0 && is_object($this->data[0])) {
            return $this->data[0]->{$name};
        }

        return false;
    }

    /**
     * Just wrap over Response->get().
     *
     * @return mixed
     */
    public function one()
    {
        return $this->get();
    }

    /**
     * This method return raw Response->data.
     *
     * @return array|bool
     */
    public function getResponse()
    {
        return $this->data;
    }

    public function offsetExists($offset)
    {
        return isset($this->items[$offset]);
    }

    public function offsetGet($offset)
    {
        return $this->items[$offset];
    }

    public function offsetSet($offset, $value)
    {
        $this->itmes[$offset] = $value;
    }

    public function offsetUnset($offset)
    {
        unset($this->items[$offset]);
    }

    public function count()
    {
        return count($this->items);
    }

    public function rewind()
    {
        $this->pointer = 0;
    }

    public function current()
    {
        return $this->items[$this->pointer];
    }

    public function key()
    {
        return $this->pointer;
    }

    public function next()
    {
        $this->pointer++;
    }

    public function valid()
    {
        return isset($this[$this->pointer]);
    }
}


<?php

namespace getjump\Vk\Model;

/**
 * Class Wall
 * Used to work with wall.
 */
class Wall extends BaseModel
{
    /**
     * Return wall id.
     *
     * @return int|bool
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Return true if this is repost.
     *
     * @return bool
     */
    public function isRepost()
    {
        return $this->copy_history !== false;
    }

    /**
     * Return recursive wall instance.
     *
     * @param int $id
     *
     * @return bool|Wall
     */
    public function getSource($id = 0)
    {
        if ($this->copy_history === false) {
            return false;
        }
        if (!isset($this->copy_history[$id])) {
            return false;
        }

        return new self($this->copy_history[0 + $id]);
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: getju_000
 * Date: 03.05.14
 */
namespace getjump\Vk;

use getjump\Vk\Response\Api;

/**
 * Class RequestTransaction
 * Is used as last front between API and Library.
 */
class RequestTransaction
{
    /**
     * URL Api VK.
     */
    const URL_VK_API = 'https://api.vk.com/method/';

    /**
     * This variable get magic works.
     *
     * @var bool
     */
    private $init = false;

    /**
     * Instance of GuzzleHttp\Client.
     *
     * @var \GuzzleHttp\Client
     */
    private $guzzle = false;

    /**
     * Here we store Access Token.
     *
     * @var bool
     */
    private $accessToken = false;

    /**
     * Here we store Method Name.
     *
     * @var bool|string
     */
    private $methodName = false;
    /**
     * Here we store arguments.
     *
     * @var array|bool
     */
    private $args = [];

    /**
     * This one is needed to convert every element of array response to something another.
     *
     * @var bool
     */
    private $callback = false;

    /**
     * Secret for nohttps requests.
     *
     * @var bool|string
     */
    private $noHttpsSecret = false;

    /**
     * @param string      $methodName
     * @param array|bool  $args
     * @param bool        $accessToken
     * @param bool        $callback
     * @param bool|string $noHttpsSecret
     */
    public function __construct($methodName, $args = false, $accessToken = false, $callback = false, $noHttpsSecret = false)
    {
        $this->methodName = $methodName;
        $this->args = $args;
        $this->accessToken = $accessToken;
        $this->callback = $callback;
        $this->noHttpsSecret = $noHttpsSecret;
    }

    /**
     * Batch method, for pulling just $count data at a time with foreach loop.
     *
     * @param int $count
     *
     * @return BatchTransaction
     */
    public function batch($count = 10)
    {
        $this->args['count'] = $count;
        $this->args['offset'] = 0;

        return new BatchTransaction($this, $count);
    }

    public function incrementOffset($offset = 0)
    {
        isset($this->args['offset']) ? $this->args['offset'] += $offset : $this->args['offset'] = 0;
    }

    /**
     * We are overriding get, so when we will try to access response, our data will get from a server.
     *
     * @param string $name
     *
     * @return bool
     */
    public function __get($name)
    {
        if (!$this->init) {
            return $this->fetchData()->$name;
        }

        return false;
    }

    /**
     * We are overriding call, so when we will call to each, our data will get from a server.
     *
     * @param string $name
     * @param array  $arguments
     *
     * @return bool|mixed
     */
    public function __call($name, array $arguments)
    {
        if (!$this->init) {
            return call_user_func_array([$this->fetchData(), $name], $arguments);
        }

        return false;
    }

    /**
     * Querying API for a data.
     *
     * @return Api
     */
    public function fetchData()
    {
        if (!$this->guzzle) {
            $this->guzzle = new \GuzzleHttp\Client();
        }

        $args = $this->args;
        if ($this->accessToken) {
            $args['access_token'] = $this->accessToken;
        }

        if ((empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') && false !== $this->noHttpsSecret) {
            $args['sig'] = md5('/method/'.$this->methodName.'?'.http_build_query($args).$this->noHttpsSecret);
        }

        $data = $this->guzzle->post(self::URL_VK_API . $this->methodName, ['form_params' => $args])->getBody();
        $data = json_decode($data);
        $c = new Api($data, $this->callback);

        return $c;
    }

    /**
     * We want just execute, without getting any data.
     */
    public function execute()
    {
        $this->fetchData();
    }

    /**
     * Will return VkJs object.
     *
     * @return VkJs
     */
    public function toJs()
    {
        return new VkJs(Core::getInstance(), $this->methodName, $this->args);
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: getju_000
 * Date: 07.05.14
 * Time: 13:46.
 */
namespace getjump\Vk\Wrapper;

/**
 * Class Account
 * Account wrapper.
 */
class Account extends BaseWrapper
{
    /**
     * Returns current app permission.
     *
     * @return array|bool|mixed
     */
    public function getAppPermissions()
    {
        return $this->vk->request('account.getAppPermissions')->response->data;
    }

    /**
     * Used to validate rights.
     *
     * @param $permissions
     * @param int|array $bitmask
     *
     * @return int
     */
    public function validateRights($permissions, $bitmask = 0)
    {
        $valid = 0;
        if (is_array($bitmask)) {
            foreach ($bitmask as $bit) {
                $valid = $this->validateRights($permissions, $bit);
            }
        } else {
            $valid = $permissions & $bitmask;
        }

        return $valid;
    }
}


<?php

namespace getjump\Vk\Wrapper;

use getjump\Vk\RequestTransaction;
use getjump\Vk\Response\Api;

/**
 * Class User
 * Implements user part of API.
 */
class User extends BaseWrapper
{
    /**
     * Get user with $userId.
     *
     * @param $userId
     * @param bool|array $fields
     *
     * @return RequestTransaction|Api
     */
    public function get($userId, $fields = false)
    {
        if (is_array($userId)) {
            $userId = implode(',', $userId);
        }

        return $this->vk->param('user_ids', $userId)
            ->param('fields', $this->fieldsToString($fields), null)
            ->createAs(function ($d) {
                return new \getjump\Vk\Model\User($d);
            })
            ->request('users.get');
    }

    /**
     * @param bool|array $data
     *
     * @return bool|string
     */
    public function fieldsToString($data = false)
    {
        if (!is_array($data)) {
            return false;
        }

        return implode(',', $data);
    }
}