<?php
/**
 * Contains information about a page or file the crawler found and received during the crawling-process.
 *
 * @package phpcrawl
 */
class PHPCrawlerDocumentInfo
{
  /**
   * The complete, full qualified URL of the page or file, e.g. "http://www.foo.com/bar/page.html?x=y".
   *
   * @var string
   * @section 1 URL-related information
   */
  public $url = "";

  /**
   * The protocol-part of the URL of the page or file, e.g. "http://"
   *
   * @var string
   * @section 1 URL-related information
   */
  public $protocol = "";

  /**
   * The host-part of the URL of the requested page or file, e.g. "www.foo.com".
   *
   * @var string
   * @section 1 URL-related information
   */
  public $host = "";

  /**
   * The path in the URL of the requested page or file, e.g. "/page/".
   *
   * @var string
   * @section 1 URL-related information
   */
  public $path = "";

  /**
   * The name of the requested page or file, e.g. "page.html".
   *
   * @var string
   * @section 1 URL-related information
   */
  public $file = "";

  /**
   * The query-part of the URL of the requested page or file, e.g. "?x=y".
   *
   * @var string
   * @section 1 URL-related information
   */
  public $query = "";

  /**
   * The port of the URL the request was send to, e.g. 80
   *
   * @var int
   * @section 1 URL-related information
   */
  public $port;

  /**
   * The complete HTTP-header the webserver responded with this page or file.
   *
   * @var string
   * @section 2 Content-related information
   */
  public $header = "";

  /**
   * The complete HTTP-header the webserver responded with this page or file as a PHPCrawlerResponseHeader-object.
   *
   * @var PHPCrawlerResponseHeader
   * @section 2 Content-related information
   */
  public $responseHeader;

  /**
   * The complete HTTP-request-header the crawler sent to the server (debugging info).
   *
   * @var string
   */
  public $header_send = "";

  /**
   * Flag indicating whether content was received from the page or file.
   *
   * @var bool TRUE if the crawler received at least some source/content of this page or file.
   * @section 2 Content-related information
   */
  public $received = false;

  /**
   * Flag indicating whether content was completely received from the page or file.
   *
   * The conten of the current document may not be received comepletely due to settings made
   * with {@link PHPCrawler::setContentSizeLimit()) and/or {@link PHPCrawler::setTrafficLimit()}.
   *
   * @var bool TRUE if the crawler received the complete source/content of this page or file.
   * @section 2 Content-related information
   */
  public $received_completely = false;

  /**
   * Alias for received_completely, was spelled wrong in prevoius versions of phpcrawl.
   *
   * @deprecated
   * @section 11 Deprecated
   */
  public $received_completly = false;

  /**
   * Will be true if the content was received into local memory.
   *
   * You will have access to the content of the current page or file through $pageInfo->source.
   *
   * @section 2 Content-related information
   * @var bool
   */
  public $received_to_memory = false;

  /**
   * Will be true if the content was received into temporary file.
   *
   * The content is stored in the temporary file $pageInfo->content_tmp_file in this case.
   *
   * @section 2 Content-related information
   * @var bool
   */
  public $received_to_file = false;

  /**
   * The number of bytes the crawler received of the content of the document.
   *
   * @var int Received bytes
   * @section 2 Content-related information
   */
  public $bytes_received = 0;

  /**
   * The content-type of the page or file, e.g. "text/html" or "image/gif".
   *
   * @var string The content-type
   * @section 2 Content-related information
   */
  public $content_type = "";

  /**
   * The content of the requested document (html-sourcecode or content of file).
   *
   * Will be empty if "received" is FALSE and the source won't be complete if "received_completly" is FALSE!
   *
   * @var string
   * @section 2 Content-related information
   */
  public $content = "";

  /**
   * Same as "content", the content of the requested document.
   *
   * @var string
   * @section 2 Content-related information
   */
  public $source = "";

  /**
   * The temporary file to which the content was received.
   *
   * Will be NULL if the content wasn't received to the temporary file.
   *
   * @var string
   * @section 2 Content-related information
   */
  public $content_tmp_file = null;

  /**
   * The HTTP-statuscode the webserver responded for the request, e.g. 200 (OK) or 404 (file not found).
   *
   * @var int
   * @section 2 Content-related information
   */
  public $http_status_code = null;

  /**
   * Cookies send by the server.
   *
   * @var array Numeric array containing all send cookies as {@link PHPCrawlerCookieDescriptor}-objects.
   * @section 2 Content-related information
   */
  public $cookies = array();

  /**
   * An numeric array containing information about all links that were found in the source of the page.
   *
   * Every element of that numeric array contains the following keys again:
   *
   * link_raw - contains the raw link as it was found
   * url_rebuild - contains the full qualified URL the link leads to
   * linkcode - the html-codepart that contained the link.
   * linktext - the linktext the link was layed over (may be empty).
   *
   * So e.g $page_data["links_found"][5]["link_raw"] contains the fifth link that was found in the current page.
   * (May be something like "../../foo.html").
   *
   * @var array
   * @section 3 Information about found links
   */
  public $links_found = array();

  /**
   * An numeric array containing a PHPCrawlerURLDescriptor-object for every link that was found in the page.
   *
   * Example: Printing the second raw link that was found on the page
   *
   * echo $PageInfo->links_found_url_descriptors[2]->link_raw;
   *
   *
   * @var array Numneric array containing {@link PHPCrawlerURLDescriptor}-objects
   * @section 3 Information about found links
   */
  public $links_found_url_descriptors = array();

  /**
   * The complete URL of the page that contained the link to this document.
   *
   * @var string
   * @section 7 Referer information
   */
  public $referer_url = null;

  /**
   * The html-sourcecode that contained the link to the current document.
   *
   * (E.g. LINKTEXT)
   *
   * @var string
   * @section 7 Referer information
   */
  public $refering_linkcode = null;

  /**
   * Contains the raw link as it was found in the content of the refering URL. (E.g. "../foo.html")
   *
   * @var string
   * @section 7 Referer information
   */
  public $refering_link_raw = null;

  /**
   * The linktext of the link that "linked" to this document.
   *
   * E.g. if the refering link was LINKTEXT, the refering linktext is "LINKTEXT".
   * May contain html-tags of course.
   *
   * @var string
   * @section 7 Referer information
   */
  public $refering_linktext = null;

  /**
   * Indicates whether an error occured while requesting/receiving the document.
   *
   * @var bool TRUE if an error occured.
   * @section 8 Error-handling
   */
  public $error_occured = false;

  /**
   * The code of the error that perhaps occured while requesting/receiving the document.
   * (See PHPCrawlerRequestErrors::ERROR_... - constants)
   *
   * @var int One of the {@link PHPCrawlerRequestErrors}::ERROR_ ... constants.
   * @section 8 Error-handling
   */
  public $error_code = null;

  /**
   * A representig, human readable string for the error that perhaps occured while requesting/receiving the document.
   *
   * @var string A human readable error-string.
   * @section 8 Error-handling
   */
  public $error_string = null;

  /**
   * Indicated whether the traffic-limit set by the user was reached after downloading this document.
   *
   * @var bool  TRUE if traffic-limit was reached.
   */
  public $traffic_limit_reached = false;

  /**
   * The time it took to receive the document.
   *
   * @var float The time seconds
   * @section 10 Benchmarks
   */
  public $data_transfer_time = null;

  /**
   * The average data-transferrate for this document.
   *
   * @var float The rate in bytes per seconds.
   * @section 10 Benchmarks
   */
  public $data_transfer_rate = null;

  /**
   * Some internal benchmak-results as array.
   *
   * @var array Array containing some interlnal benchmark-results for receiving and processing this document.
   *            The keys are the identifiers, the values are the benchmark-times.
   * @section 10 Benchmarks
   * @internal
   */
  public $benchmarks = array();

  /**
   * All meta-tag atteributes found in the source of the document.
   *
   * @var array Assoziative array conatining all found meta-attributes.
   *            The keys are the meta-names, the values the content of the attributes.
   *            (like $tags["robots"] = "nofollow")
   * @section 2 Content-related information
   *
   */
  public $meta_attributes = array();

  /**
   * Workaround-method, copies and converts the array $links_found_url_descriptors to $links_found.
   *
   * @internal
   */
  public function setLinksFoundArray()
  {
    $cnt = count($this->links_found_url_descriptors);
    for ($x=0; $x<$cnt; $x++)
    {
      $UrlDescriptor = $this->links_found_url_descriptors[$x];

      // Convert $UrlDescriptor-object to an array
      $object_vars = get_object_vars($UrlDescriptor);

      $this->links_found[] = $object_vars;
    }
  }

  /**
   * Returns an array with all properties of this class.
   *
   * @return array
   * @internal
   */
  public function toArray()
  {
    return get_object_vars($this);
  }
}
?>


<?php
/**
 * Abstract baseclass for implemented URL-caching classes.
 *
 * @package phpcrawl
 * @internal
 */
abstract class PHPCrawlerURLCacheBase
{
  protected $url_priorities = array();

  /**
   * Defines which property of an URL is used to ensure that each URL is only cached once.
   *
   * @var int One of the URLHASH_.. constants
   */
  public $url_distinct_property = self::URLHASH_URL;

  const URLHASH_URL = 1;
  const URLHASH_RAWLINK= 2;
  const URLHASH_NONE = 3;

  /**
   * Returns the next URL from the cache that should be crawled.
   *
   * @return PhpCrawlerURLDescriptor
   */
  abstract public function getNextUrl();

  /**
   * Returns all URLs currently cached in the URL-cache.
   *
   * @return array Numeric array containing all URLs as PHPCrawlerURLDescriptor-objects
   */
  abstract public function getAllURLs();

  /**
   * Removes all URLs and all priority-rules from the URL-cache.
   */
  abstract public function clear();

  /**
   * Adds an URL to the url-cache
   *
   * @param PHPCrawlerURLDescriptor $UrlDescriptor
   */
  abstract public function addURL(PHPCrawlerURLDescriptor $UrlDescriptor);

  /**
   * Adds an bunch of URLs to the url-cache
   *
   * @param array $urls  A numeric array containing the URLs as PHPCrawlerURLDescriptor-objects
   */
  abstract public function addURLs($urls);

  /**
   * Checks whether there are URLs left in the cache or not.
   *
   * @return bool
   */
  abstract public function containsURLs();

  /**
   * Marks the given URL in the cache as "followed"
   *
   * @param PHPCrawlerURLDescriptor $UrlDescriptor
   */
  abstract public function markUrlAsFollowed(PHPCrawlerURLDescriptor $UrlDescriptor);

  /**
   * Do cleanups after the cache is not needed anymore
   */
  abstract public function cleanup();

  /**
   * Cleans/purges the URL-cache from inconsistent entries.
   */
  abstract public function purgeCache();

  /**
   * Returns the distinct-hash for the given URL that ensures that no URLs a cached more than one time.
   *
   * @return string The hash or NULL if no distinct-hash should be used.
   */
  protected function getDistinctURLHash(PHPCrawlerURLDescriptor $UrlDescriptor)
  {
    if ($this->url_distinct_property == self::URLHASH_URL)
      return md5($UrlDescriptor->url_rebuild);
    elseif ($this->url_distinct_property == self::URLHASH_RAWLINK)
      return md5($UrlDescriptor->link_raw);
    else
      return null;
  }

  /**
   * Gets the priority-level of the given URL
   */
  protected function getUrlPriority($url)
  {
    $cnt = count($this->url_priorities);
    for ($x=0; $x<$cnt; $x++)
    {
      if (preg_match($this->url_priorities[$x]["match"], $url))
      {
        return $this->url_priorities[$x]["level"];
      }
    }

    return 0;
  }

  /**
   * Adds a Link-Priority-Level
   *
   * @param string $regex
   * @param int    $level
   */
  public function addLinkPriority($regex, $level)
  {
    $c = count($this->url_priorities);
    $this->url_priorities[$c]["match"] = trim($regex);
    $this->url_priorities[$c]["level"] = trim($level);

    // Sort url-priortie-array so that high priority-levels come firts.
    PHPCrawlerUtils::sort2dArray($this->url_priorities, "level", SORT_DESC);
  }

  /**
   * Adds a bunch of link-priorities
   *
   * @param array $priority_array Numeric array containing the subkeys "match" and "level"
   */
  public function addLinkPriorities($priority_array)
  {
    for ($x=0; $x<count($priority_array); $x++)
    {
      $this->addLinkPriority($priority_array[$x]["match"], $priority_array[$x]["level"]);
    }
  }
}
?>


<?php
/**
 * Queue for PHPCrawlerDocumentInfo-objects
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerDocumentInfoQueue
{
  protected $PDO;

  protected $sqlite_db_file;

  protected $prepared_statements_created = false;

  protected $working_directory = null;

  protected $queue_max_size = 20;

  /**
   * Initiates a PHPCrawlerDocumentInfoQueue
   *
   * @param string $file            The SQLite-fiel to use.
   * @param  bool  $create_tables   Defines whether all necessary tables should be created
   */
  public function __construct($file, $create_tables = false)
  {
    $this->sqlite_db_file = $file;
    $this->working_directory = dirname($file)."/";
    $this->openConnection($create_tables);
  }

  /**
   * Returns the current number of PHPCrawlerDocumentInfo-objects in the queue
   */
  public function getDocumentInfoCount()
  {
    $Result = $this->PDO->query("SELECT count(id) as sum FROM document_infos;");
    $row = $Result->fetch(PDO::FETCH_ASSOC);
    $Result->closeCursor();

    return $row["sum"];
  }

  /**
   * Adds a PHPCrawlerDocumentInfo-object to the queue
   */
  public function addDocumentInfo(PHPCrawlerDocumentInfo $DocInfo)
  {
    // If queue is full -> wait a little
    while ($this->getDocumentInfoCount() >= $this->queue_max_size)
    {
      sleep(0.5);
    }

    $this->createPreparedStatements();

    $ser = serialize($DocInfo);

    $this->PDO->exec("BEGIN EXCLUSIVE TRANSACTION");
    $this->prepared_insert_statement->bindParam(1, $ser, PDO::PARAM_LOB);
    $this->prepared_insert_statement->execute();
    $this->prepared_select_statement->closeCursor();
    $this->PDO->exec("COMMIT");
  }

   /**
   * Returns a PHPCrawlerDocumentInfo-object from the queue
   */
  public function getNextDocumentInfo()
  {
    $this->createPreparedStatements();

    $this->prepared_select_statement->execute();
    $this->prepared_select_statement->bindColumn("document_info", $doc_info, PDO::PARAM_LOB);
    $this->prepared_select_statement->bindColumn("id", $id);
    $row = $this->prepared_select_statement->fetch(PDO::FETCH_BOUND);
    $this->prepared_select_statement->closeCursor();

    if ($id == null)
    {
      return null;
    }

    $this->PDO->exec("DELETE FROM document_infos WHERE id = ".$id.";");

    $DocInfo = unserialize($doc_info);

    return $DocInfo;
  }

  protected function createPreparedStatements()
  {
    if ($this->prepared_statements_created == false)
    {
      $this->prepared_insert_statement = $this->PDO->prepare("INSERT INTO document_infos (document_info) VALUES (?);");
      $this->prepared_select_statement = $this->PDO->prepare("SELECT * FROM document_infos limit 1;");

      $this->prepared_statements_created = true;
    }
  }

  /**
   * Creates the sqlite-db-file and opens connection to it.
   *
   * @param bool $create_tables Defines whether all necessary tables should be created
   */
  protected function openConnection($create_tables = false)
  {
    // Open sqlite-file
    try
    {
      $this->PDO = new PDO("sqlite:".$this->sqlite_db_file);
    }
    catch (Exception $e)
    {
      throw new Exception("Error creating SQLite-cache-file, ".$e->getMessage().", try installing sqlite3-extension for PHP.");
    }

    $this->PDO->exec("PRAGMA journal_mode = OFF");

    $this->PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
    $this->PDO->setAttribute(PDO::ATTR_TIMEOUT, 100);

    if ($create_tables == true)
    {
      $this->PDO->exec("CREATE TABLE IF NOT EXISTS document_infos (id integer PRIMARY KEY AUTOINCREMENT,
                                                                   document_info blob);");
      $this->PDO->exec("ANALYZE;");
    }
  }
}
?>


<?php
/**
 * Describes the single parts of an URL.
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerUrlPartsDescriptor
{
  public $protocol;

  public $host;

  public $path;

  public $file;

  public $domain;

  public $port;

  public $auth_username;

  public $auth_password;

  /**
   * Returns the PHPCrawlerUrlPartsDescriptor-object for the given URL.
   *
   * @return PHPCrawlerUrlPartsDescriptor
   */
  public static function fromURL($url)
  {
    $parts = PHPCrawlerUtils::splitURL($url);

    $tmp = new PHPCrawlerUrlPartsDescriptor();

    $tmp->protocol = $parts["protocol"];
    $tmp->host = $parts["host"];
    $tmp->path = $parts["path"];
    $tmp->file = $parts["file"];
    $tmp->domain = $parts["domain"];
    $tmp->port = $parts["port"];
    $tmp->auth_username = $parts["auth_username"];
    $tmp->auth_password = $parts["auth_password"];

    return $tmp;
  }

  public function toArray()
  {
    return get_object_vars($this);
  }
}
?>


<?php
/**
 * Class for finding links in HTML-documents.
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerLinkFinder
{
  /**
   * Numeric array containing all tags to extract links from
   *
   * @var array
   */
  public $extract_tags = array("href", "src", "url", "location", "codebase", "background", "data", "profile", "action", "open");

  /**
   * Specifies whether links will also be searched outside of HTML-tags
   *
   * @var bool
   */
  public $aggressive_search = true;

  /**
   * Specifies whether redirect-links set in http-headers should get found.
   *
   * @var bool
   */
  public $find_redirect_urls = true;

  /**
   * The URL of the html-source to find links from
   *
   * @var PHPCrawlerURLDescriptor
   */
  protected $SourceUrl;

  /**
   * Cache for storing found links/urls
   *
   * @var PHPCrawlerURLCache
   */
  protected $LinkCache;

  /**
   * Flag indicating whether the top lines of the HTML-source were processed.
   */
  protected $top_lines_processed = false;

  /**
   * Parts of the base-url as PHPCrawlerUrlPartsDescriptor-object
   *
   * @var PHPCrawlerUrlPartsDescriptor
   */
  protected $baseUrlParts;

  protected $found_links_map = array();

  /**
   * Meta-attributes found in the html-source.
   *
   * @var array
   */
  protected $meta_attributes = array();

  public function __construct()
  {
    if (!class_exists("PHPCrawlerMemoryURLCache")) include_once(dirname(__FILE__)."/UrlCache/PHPCrawlerMemoryURLCache.class.php");
    $this->LinkCache = new PHPCrawlerMemoryURLCache();
    $this->LinkCache->url_distinct_property = PHPCrawlerURLCacheBase::URLHASH_URL;
  }

  /**
   * Sets the source-URL of the document to find links in
   *
   * @param PHPCrawlerURLDescriptor $SourceUrl
   */
  public function setSourceUrl(PHPCrawlerURLDescriptor $SourceUrl)
  {
    $this->SourceUrl = $SourceUrl;
    $this->baseUrlParts = PHPCrawlerUrlPartsDescriptor::fromURL($SourceUrl->url_rebuild);
  }

  /**
   * Processes the response-header of the document.
   *
   * @param &string $header The response-header of the document.
   */
  public function processHTTPHeader(&$header)
  {
    if ($this->find_redirect_urls == true)
    {
      $this->findRedirectLinkInHeader($header);
    }
  }

  /**
   * Resets/clears the internal link-cache.
   */
  public function resetLinkCache()
  {
    $this->LinkCache->clear();
    $this->top_lines_processed = false;
  }

  /**
   * Checks for a redirect-URL in the given http-header and adds it to the internal link-cache.
   */
  protected function findRedirectLinkInHeader(&$http_header)
  {
    PHPCrawlerBenchmark::start("checking_for_redirect_link");

    // Get redirect-URL or link from header
    $redirect_link = PHPCrawlerUtils::getRedirectURLFromHeader($http_header);

    // Add redirect-URL to linkcache
    if ($redirect_link != null)
    {
      // Rebuild URL
      $url_rebuild = PHPCrawlerUtils::buildURLFromLink($redirect_link, $this->baseUrlParts);

      // Add URL to cache
      $UrlDescriptor = new PHPCrawlerURLDescriptor($url_rebuild, $redirect_link, "", "", $this->SourceUrl->url_rebuild);
      $UrlDescriptor->is_redirect_url = true;
      $this->LinkCache->addURL($UrlDescriptor);
    }

     PHPCrawlerBenchmark::stop("checking_for_redirect_link");
  }

  /**
   * Searches for links in the given HTML-chunk and adds found links the the internal link-cache.
   */
  public function findLinksInHTMLChunk(&$html_source)
  {
    PHPCrawlerBenchmark::start("searching_for_links_in_page");

    // Check for meta-base-URL and meta-tags in top of HTML-source
    if ($this->top_lines_processed == false)
    {
      $meta_base_url = PHPCrawlerUtils::getBaseUrlFromMetaTag($html_source);
      if ($meta_base_url != null)
      {
        $base_url = PHPCrawlerUtils::buildURLFromLink($meta_base_url, $this->baseUrlParts);
        $this->baseUrlParts = PHPCrawlerUrlPartsDescriptor::fromURL($base_url);
      }

      // Get all meta-tags
      $this->meta_attributes = PHPCrawlerUtils::getMetaTagAttributes($html_source);

      // Set flag that top-lines of source were processed
      $this->top_lines_processed == true;
    }

    // Build the RegEx-part for html-tags to search links in
    $tag_regex_part = "";
    $cnt = count($this->extract_tags);
    for ($x=0; $x<$cnt; $x++)
    {
      $tag_regex_part .= "|".$this->extract_tags[$x];
    }
    $tag_regex_part = substr($tag_regex_part, 1);

    // 1. LINKTEXT (well formed link with  at the end and quotes around the link)
    // Get the link AND the linktext from these tags
    // This has to be done FIRST !!
    preg_match_all("#<\s*a\s[^<>]*(?<=\s)(?:".$tag_regex_part.")\s*=\s*".
                   "(?|\"([^\"]+)\"|'([^']+)'|([^\s><'\"]+))[^<>]*>".
                   "((?:(?!<\s*\/a\s*>).){0,500})".
                   "<\s*\/a\s*># is", $html_source, $matches);

    $cnt = count($matches[0]);
    for ($x=0; $x<$cnt; $x++)
    {
      $link_raw = trim($matches[1][$x]);
      $linktext = $matches[2][$x];
      $linkcode = trim($matches[0][$x]);

      if (!empty($link_raw)) $this->addLinkToCache($link_raw, $linkcode, $linktext);
    }

    // Second regex (everything that could be a link inside of <>-tags)
    preg_match_all("#<[^<>]*\s(?:".$tag_regex_part.")\s*=\s*".
                   "(?|\"([^\"]+)\"|'([^']+)'|([^\s><'\"]+))[^<>]*># is", $html_source, $matches);

    $cnt = count($matches[0]);
    for ($x=0; $x<$cnt; $x++)
    {
      $link_raw = trim($matches[1][$x]);
      $linktext = "";
      $linkcode = trim($matches[0][$x]);

      if (!empty($link_raw)) $this->addLinkToCache($link_raw, $linkcode, $linktext);
    }

    // Now, if agressive_mode is set to true, we look for some
    // other things
    $pregs = array();
    if ($this->aggressive_search == true)
    {
      // Links like "...:url("animage.gif")..."
      $pregs[]="/[\s\.:;](?:".$tag_regex_part.")\s*\(\s*([\"|']{0,1})([^\"'\) ]{1,500})['\"\)]/ is";

      // Everything like "...href="bla.html"..." with qoutes
      $pregs[]="/[\s\.:;](?:".$tag_regex_part.")\s*=\s*([\"|'])(.{0,500}?)\\1/ is";

      // Everything like "...href=bla.html..." without qoutes
      $pregs[]="/[\s\.:;](?:".$tag_regex_part.")\s*(=)\s*([^\s\">']{1,500})/ is";

      for ($x=0; $x<count($pregs); $x++)
      {
        unset($matches);
        preg_match_all($pregs[$x], $html_source, $matches);

        $cnt = count($matches[0]);
        for ($y=0; $y<$cnt; $y++)
        {
          $link_raw = trim($matches[2][$y]);
          $linkcode = trim($matches[0][$y]);
          $linktext = "";

          $this->addLinkToCache($link_raw, $linkcode, $linktext);
        }
      }
    }

    $this->found_links_map = array();

    PHPCrawlerBenchmark::stop("searching_for_links_in_page");
  }

  protected function addLinkToCache($link_raw, $link_code, $link_text = "")
  {
    //PHPCrawlerBenchmark::start("preparing_link_for_cache");

    // If liks already was found and processed -> skip this link
    if (isset($this->found_links_map[$link_raw])) return;

    // Rebuild URL from link
    $url_rebuild = PHPCrawlerUtils::buildURLFromLink($link_raw, $this->baseUrlParts);

    // If link coulnd't be rebuild
    if ($url_rebuild == null) return;

    // Create an PHPCrawlerURLDescriptor-object with URL-data
    $UrlDescriptor = new PHPCrawlerURLDescriptor($url_rebuild, $link_raw, $link_code, $link_text, $this->SourceUrl->url_rebuild);

    // Add the PHPCrawlerURLDescriptor-object to LinkCache
    $this->LinkCache->addURL($UrlDescriptor);

    // Add the PHPCrawlerURLDescriptor-object to found-links-array
    $map_key = $link_raw;
    $this->found_links_map[$map_key] = true;

    //PHPCrawlerBenchmark::stop("preparing_link_for_cache");
  }

  /**
   * Returns all URLs/links found so far in the document.
   *
   * @return array Numeric array containing all URLs as PHPCrawlerURLDescriptor-objects
   */
  public function getAllURLs()
  {
    return $this->LinkCache->getAllURLs();
  }

  /**
   * Returns all meta-tag attributes found so far in the document.
   *
   * @return array Assoziative array conatining all found meta-attributes.
   *               The keys are the meta-names, the values the content of the attributes.
   *               (like $tags["robots"] = "nofollow")
   *
   */
  public function getAllMetaAttributes()
  {
    return $this->meta_attributes;
  }
}
?>


<?php
/**
 * Cache for storing user-data to send with requests, like cookies, post-data
 * and basic-authentications.
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerUserSendDataCache
{
  /**
   * Array containing basic-authentications to send.
   *
   * @var array
   */
  protected $basic_authentications = array();

  /**
   * Array containing post-data to send.
   *
   * @var array
   */
  protected $post_data = array();

  /**
   * Adds post-data together with an URL-regex to the list of post-data to send with requests.
   *
   * @param string $url_regex       Regular expression defining the URL(s) the post-data should be send to.
   * @param array  $post_data_array Post-data-array, the keys are the post-data-keys, the values the post-values.
   *                                (like array("key1" => "value1", "key2" => "value2")
   */
  public function addPostData($url_regex, $post_data_array)
  {
    // Check regex
    $regex_okay = PHPCrawlerUtils::checkRegexPattern($url_regex);

    if ($regex_okay == true)
    {
      @reset($post_data_array);
      while (list($key, $value) = @each($post_data_array))
      {
        // Add data to post_data-array
        $tmp = array();
        $tmp["url_regex"] = $url_regex;
        $tmp["key"] = $key;
        $tmp["value"] = $value;

        $this->post_data[] = $tmp;
      }

      return true;
    }
    else return false;
  }

  /**
   * Returns the post-data (key and value) that should be send to the given URL.
   *
   * @param string $url The URL.
   * @return array Array containing the post_keys as keys and the values as values.
   *               (like array("key1" => "value1", "key2" => "value2")
   */
  public function getPostDataForUrl($url)
  {
    $post_data_array = array();

    $cnt = count($this->post_data);
    for ($x=0; $x<$cnt; $x++)
    {
      if (preg_match($this->post_data[$x]["url_regex"], $url))
      {
        $post_data_array[$this->post_data[$x]["key"]] = $this->post_data[$x]["value"];
      }
    }

    return $post_data_array;
  }

  /**
   * Adds a basic-authentication (username and password) to the list of authentications that will be send
   * with requests.
   *
   * @param string $url_regex Regular expression defining the URL(s) the authentication should be send to.
   * @param string $username  The username
   * @param string $password  The password
   *
   * @return bool
   */
  public function addBasicAuthentication($url_regex, $username, $password)
  {
    // Check regex
    $regex_okay = PHPCrawlerUtils::checkRegexPattern($url_regex);

    if ($regex_okay == true)
    {
      // Add authentication to basic_authentications-array
      $tmp = array();
      $tmp["url_regex"] = $url_regex;
      $tmp["username"] = $username;
      $tmp["password"] = $password;

      $this->basic_authentications[] = $tmp;
      return true;
    }
    else return false;
  }

  /**
   * Returns the basic-authentication (username and password) that should be send to the given URL.
   *
   * @param string $url The URL.
   * @return array Array containing the keys "username" and "password".
   *               Returns NULL if no authentication was found in cache for the given URL.
   */
  public function getBasicAuthenticationForUrl($url)
  {
    for ($x=0; $x<count($this->basic_authentications); $x++)
    {
      if (preg_match($this->basic_authentications[$x]["url_regex"], $url))
      {
        $tmp = array();
        $tmp["username"] = $this->basic_authentications[$x]["username"];
        $tmp["password"] = $this->basic_authentications[$x]["password"];

        return $tmp;
      }
    }

    return null;
  }
}
?>


<?php
/**
 * Class containing methods for process handling and communication
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerProcessCommunication
{
  protected $crawler_uniqid;

  protected $multiprocess_mode;

  protected $working_directory;

  protected $crawlerStatus;

  /**
   * Flag indicating whether resumtion is activated
   *
   * @var PHPCrawlerDocumentInfoQueue
   */
  protected $resumtion_enabled = false;

  /**
   * Initiates a new PHPCrawlerProcessCommunication-object.
   *
   * @param string $crawler_uniqid     UID of the crawler
   * @param int    $multiprocess_mode  Multprocess-mode the crawler is running (one of the PHPCrawlerMultiProcessModes-constants)
   * @param string $working_directory  Working-dir of the crawler
   * @param bool   $enable_resumtion   TRUE if resumption of crawling-processes should be possible
   */
  public function __construct($crawler_uniqid, $multiprocess_mode, $working_directory, $enable_resumtion)
  {
    $this->crawler_uniqid = $crawler_uniqid;
    $this->multiprocess_mode = $multiprocess_mode;
    $this->working_directory = $working_directory;
    $this->resumtion_enabled = $enable_resumtion;

    $this->crawlerStatus = new PHPCrawlerStatus();
  }

  /**
   * Sets/writes the current crawler-status
   *
   * @param PHPCrawlerStatus $crawler_status The status to set
   */
  public function setCrawlerStatus(PHPCrawlerStatus $crawler_status)
  {
    $this->crawlerStatus = $crawler_status;

    // Write crawler-status back to file if crawler is multiprocessed
    if ($this->multiprocess_mode == PHPCrawlerMultiProcessModes::MPMODE_CHILDS_EXECUTES_USERCODE || $this->resumtion_enabled == true)
    {
      PHPCrawlerUtils::serializeToFile($this->working_directory."crawlerstatus.tmp", $crawler_status);
    }
  }

  /**
   * Returns/reads the current crawler-status
   *
   * @return PHPCrawlerStatus The current crawlerstatus as a PHPCrawlerStatus-object
   */
  public function getCrawlerStatus()
  {
    // Get crawler-status from file if crawler is multiprocessed
    if ($this->multiprocess_mode == PHPCrawlerMultiProcessModes::MPMODE_CHILDS_EXECUTES_USERCODE || $this->resumtion_enabled == true)
    {
      $this->crawlerStatus = PHPCrawlerUtils::deserializeFromFile($this->working_directory."crawlerstatus.tmp");
      if ($this->crawlerStatus == null) $this->crawlerStatus = new PHPCrawlerStatus();
    }

    return $this->crawlerStatus;
  }

  /**
   * Updates the status of the crawler
   *
   * @param PHPCrawlerDocumentInfo $PageInfo          The PHPCrawlerDocumentInfo-object of the last received document
   *                                                  or NULL if no document was received.
   * @param int                    $abort_reason      One of the PHPCrawlerAbortReasons::ABORTREASON-constants if the crawling-process
   *                                                  should get aborted, otherwise NULL
   * @param string                 $first_content_url The first URL some content was found in (or NULL if no content was found so far).
   */
  public function updateCrawlerStatus($PageInfo, $abort_reason = null, $first_content_url = null)
  {
    PHPCrawlerBenchmark::start("updating_crawler_status");

    // Set semaphore if crawler is multiprocessed
    if ($this->multiprocess_mode == PHPCrawlerMultiProcessModes::MPMODE_CHILDS_EXECUTES_USERCODE || $this->resumtion_enabled == true)
    {
      $sem_key = sem_get($this->crawler_uniqid);
      sem_acquire($sem_key);
    }

    // Get current Status
    $crawler_status = $this->getCrawlerStatus();

    // Update status
    if ($PageInfo != null)
    {
      // Increase number of followed links
      $crawler_status->links_followed++;

      // Increase documents_received-counter
      if ($PageInfo->received == true) $crawler_status->documents_received++;

      // Increase bytes-counter
      $crawler_status->bytes_received += $PageInfo->bytes_received;
    }

    // Set abortreason
    if ($abort_reason !== null) $crawler_status->abort_reason = $abort_reason;

    // Set first_content_url
    if ($first_content_url !== null) $crawler_status->first_content_url = $first_content_url;

    // Write crawler-status back
    $this->setCrawlerStatus($crawler_status);

    // Remove semaphore if crawler is multiprocessed
    if ($this->multiprocess_mode == PHPCrawlerMultiProcessModes::MPMODE_CHILDS_EXECUTES_USERCODE || $this->resumtion_enabled == true)
    {
      sem_release($sem_key);
    }

    PHPCrawlerBenchmark::stop("updating_crawler_status");
  }

  /**
   * Registers the PID of a child-process
   *
   * @param int The IPD
   */
  public function registerChildPID($pid)
  {
    $sem_key = sem_get($this->crawler_uniqid);
    sem_acquire($sem_key);

    file_put_contents($this->working_directory."pids", $pid."\n", FILE_APPEND);

    sem_release($sem_key);
  }

  /**
   * Returns alls PIDs of all running child-processes
   *
   * @param int $process_count If set, this function tries to get the child-PIDs until the gievn number of PIDs
   *                           was determinated.
   * @return array Numeric array conatining the PIDs
   */
  public function getChildPIDs($process_count = null)
  {
    $child_pids = array();
    $try = true;

    while ($try == true)
    {
      if (file_exists($this->working_directory."pids"))
      {
        $ct = file_get_contents($this->working_directory."pids");
        $child_pids = preg_split("#\n#", $ct, -1, PREG_SPLIT_NO_EMPTY);

        if ($process_count == null) $try = false;
        if (count($child_pids) == $process_count) $try = false;
      }

      sleep(0.2);
    }

    return $child_pids;

  }

  /**
   * Kills all running child-processes
   */
  public function killChildProcesses()
  {
    $child_pids = $this->getChildPIDs();
    for ($x=0; $x<count($child_pids); $x++)
    {
      posix_kill($child_pids[$x], SIGKILL);
    }
  }

  /**
   * Checks wehther any child-processes a (still) running.
   *
   * @return bool
   */
  public function childProcessAlive()
  {
    $pids = $this->getChildPIDs();
    $cnt = count($pids);

    for ($x=0; $x<$cnt; $x++)
    {
      if (posix_getsid($pids[$x]) != false)
      {
        return true;
      }
    }

    return false;
  }
}
?>


<?php
/**
 * Simple DNS-cache used by phpcrawl.
 *
 * @package phpcrawl
 * @internal
 */
class PHPCrawlerDNSCache
{
  /**
   * Array for caching IPs of the requested hostnames
   *
   * @var array Associative array, keys = hostnames, values = IPs.
   */
  protected $host_ip_array;

  public function __construct()
  {
  }

  /**
   * Returns the IP for the given hostname.
   *
   * @return string The IP-address.
   */
  public function getIP($hostname)
  {
    // If host already was queried
    if (isset($this->host_ip_array[$hostname]))
    {
      return $this->host_ip_array[$hostname];
    }

    // Else do DNS-query
    else
    {
      $ip = gethostbyname($hostname);
      $this->host_ip_array[$hostname] = $ip;
      return $ip;
    }
  }

  /**
   * Checks whether a hostname is already cached.
   *
   * @param string $hostname The hostname
   * @return bool
   */
  public function hostInCache($hostname)
  {
    if (isset($this->host_ip_array[$hostname])) return true;
    else return false;
  }

  /**
   * Checks whether the hostname of the given URL is already cached
   *
   * @param PHPCrawlerURLDescriptor $URL The URL
   * @return bool
   */
  public function urlHostInCache(PHPCrawlerURLDescriptor $URL)
  {
    $url_parts = PHPCrawlerUtils::splitURL($URL->url_rebuild);
    return $this->hostInCache($url_parts["host"]);
  }
}
?>


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\Literal;

/**
 * A logical selector.
 *
 * Evaluates an expression for elements of a structured value.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
abstract class Selector extends Literal
{
    /**
     * @var Expression
     */
    protected $expr;

    /**
     * Checks whether a value selected from the evaluated value matches an
     * expression.
     *
     * @param Expression $expr The expression to evaluate for the selected value.
     */
    public function __construct(Expression $expr)
    {
        $this->expr = $expr;
    }

    /**
     * Returns the expression that is evaluated for the selected value.
     *
     * @return Expression The inner expression.
     */
    public function getExpression()
    {
        return $this->expr;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (get_class($this) !== get_class($other)) {
            return false;
        }

        /* @var Selector $other */
        return $this->expr->equivalentTo($other->expr);
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\AndX;
use Webmozart\Expression\Logic\OrX;

/**
 * Checks whether the value of a property matches an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Property extends Selector
{
    /**
     * @var string
     */
    private $propertyName;

    /**
     * Creates the expression.
     *
     * @param string     $propertyName The name of the property.
     * @param Expression $expr         The expression to evaluate for the result.
     */
    public function __construct($propertyName, Expression $expr)
    {
        parent::__construct($expr);

        $this->propertyName = $propertyName;
    }

    /**
     * Returns the property name.
     *
     * @return string The property name.
     */
    public function getPropertyName()
    {
        return $this->propertyName;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_object($value)) {
            return false;
        }

        $propertyName = $this->propertyName;

        if (!property_exists($value, $propertyName)) {
            return false;
        }

        return $this->expr->evaluate($value->$propertyName);
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (!parent::equivalentTo($other)) {
            return false;
        }

        /* @var static $other */
        return $this->propertyName === $other->propertyName;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        $exprString = $this->expr->toString();

        if ($this->expr instanceof AndX || $this->expr instanceof OrX) {
            return $this->propertyName.'{'.$exprString.'}';
        }

        // Append "functions" with "."
        if (isset($exprString[0]) && ctype_alpha($exprString[0])) {
            return $this->propertyName.'.'.$exprString;
        }

        return $this->propertyName.$exprString;
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Traversal;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\AndX;
use Webmozart\Expression\Logic\Not;
use Webmozart\Expression\Logic\OrX;
use Webmozart\Expression\Selector\Key;

/**
 * Traverses {@link Expression} instances.
 *
 * You can attach {@link ExpressionVisitor} instances to the traverse which
 * will be invoked for every node of the expression tree.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ExpressionTraverser
{
    /**
     * @var ExpressionVisitor[]
     */
    private $visitors = array();

    /**
     * Adds a visitor to the traverser.
     *
     * The visitors are invoked in the order in which they are added.
     *
     * @param ExpressionVisitor $visitor The visitor to add.
     */
    public function addVisitor(ExpressionVisitor $visitor)
    {
        $this->visitors[] = $visitor;
    }

    /**
     * Removes a visitor from the traverser.
     *
     * If the visitor was added multiple times, all instances are removed.
     *
     * @param ExpressionVisitor $visitor The visitor to remove.
     */
    public function removeVisitor(ExpressionVisitor $visitor)
    {
        while (false !== ($key = array_search($visitor, $this->visitors, true))) {
            unset($this->visitors[$key]);
        }

        $this->visitors = array_values($this->visitors);
    }

    /**
     * Returns the visitors of the traverser.
     *
     * @return ExpressionVisitor[] The visitors.
     */
    public function getVisitors()
    {
        return $this->visitors;
    }

    /**
     * Traverses an expression.
     *
     * @param Expression $expr The expression to traverse.
     *
     * @return Expression The modified expression. May be `null` if the
     *                    expression was removed entirely.
     */
    public function traverse(Expression $expr)
    {
        // Do one full traversal per visitor. If any of the visitors removes
        // the expression entirely, subsequent visitors are not invoked.
        foreach ($this->visitors as $visitor) {
            $expr = $this->traverseForVisitor($expr, $visitor);

            if (!$expr) {
                return null;
            }
        }

        return $expr;
    }

    private function traverseForVisitor(Expression $expr, ExpressionVisitor $visitor)
    {
        $expr = $visitor->enterExpression($expr);

        if ($expr instanceof Key) {
            $expr = $this->traverseKey($expr);
        } elseif ($expr instanceof Not) {
            $expr = $this->traverseNot($expr);
        } elseif ($expr instanceof AndX) {
            $expr = $this->traverseConjunction($expr);
        } elseif ($expr instanceof OrX) {
            $expr = $this->traverseDisjunction($expr);
        }

        if ($expr) {
            $expr = $visitor->leaveExpression($expr);
        }

        return $expr;
    }

    private function traverseKey(Key $expr)
    {
        $innerExpr1 = $expr->getExpression();
        $innerExpr2 = $this->traverse($innerExpr1);

        if ($innerExpr1 === $innerExpr2) {
            return $expr;
        }

        return $innerExpr2 ? new Key($expr->getKey(), $innerExpr2) : null;
    }

    private function traverseNot(Not $expr)
    {
        $negatedExpr1 = $expr->getNegatedExpression();
        $negatedExpr2 = $this->traverse($negatedExpr1);

        if ($negatedExpr1 === $negatedExpr2) {
            return $expr;
        }

        return $negatedExpr2 ? new Not($negatedExpr2) : null;
    }

    private function traverseConjunction(AndX $expr)
    {
        $conjuncts1 = $expr->getConjuncts();
        $conjuncts2 = array();

        foreach ($conjuncts1 as $conjunct) {
            if ($conjunct = $this->traverse($conjunct)) {
                $conjuncts2[] = $conjunct;
            }
        }

        if ($conjuncts1 === $conjuncts2) {
            return $expr;
        }

        return $conjuncts2 ? new AndX($conjuncts2) : null;
    }

    private function traverseDisjunction(OrX $expr)
    {
        $disjuncts1 = $expr->getDisjuncts();
        $disjuncts2 = array();

        foreach ($disjuncts1 as $disjunct) {
            if ($disjunct = $this->traverse($disjunct)) {
                $disjuncts2[] = $disjunct;
            }
        }

        if ($disjuncts1 === $disjuncts2) {
            return $expr;
        }

        return $disjuncts2 ? new OrX($disjuncts2) : null;
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression;

use ArrayAccess;
use InvalidArgumentException;
use Traversable;
use Webmozart\Expression\Constraint\Contains;
use Webmozart\Expression\Constraint\EndsWith;
use Webmozart\Expression\Constraint\Equals;
use Webmozart\Expression\Constraint\GreaterThan;
use Webmozart\Expression\Constraint\GreaterThanEqual;
use Webmozart\Expression\Constraint\In;
use Webmozart\Expression\Constraint\IsEmpty;
use Webmozart\Expression\Constraint\IsInstanceOf;
use Webmozart\Expression\Constraint\KeyExists;
use Webmozart\Expression\Constraint\KeyNotExists;
use Webmozart\Expression\Constraint\LessThan;
use Webmozart\Expression\Constraint\LessThanEqual;
use Webmozart\Expression\Constraint\Matches;
use Webmozart\Expression\Constraint\NotEquals;
use Webmozart\Expression\Constraint\NotSame;
use Webmozart\Expression\Constraint\Same;
use Webmozart\Expression\Constraint\StartsWith;
use Webmozart\Expression\Logic\AlwaysFalse;
use Webmozart\Expression\Logic\AlwaysTrue;
use Webmozart\Expression\Logic\AndX;
use Webmozart\Expression\Logic\Not;
use Webmozart\Expression\Logic\OrX;
use Webmozart\Expression\Selector\All;
use Webmozart\Expression\Selector\AtLeast;
use Webmozart\Expression\Selector\AtMost;
use Webmozart\Expression\Selector\Count;
use Webmozart\Expression\Selector\Exactly;
use Webmozart\Expression\Selector\Key;
use Webmozart\Expression\Selector\Method;
use Webmozart\Expression\Selector\Property;

/**
 * Factory for {@link Expression} instances.
 *
 * Use this class to build expressions:ons:
 *
 * ```php
 * $expr = Expr::greaterThan(20)->orLessThan(10);
 * ```
 *
 * You can evaluate the expression with another value {@link Expression::evaluate()}:
 *
 * ```php
 * if ($expr->evaluate($value)) {
 *     // do something...
 * }
 * ```
 *
 * You can also evaluate expressions by arrays by passing the array keys as
 * second argument:
 *
 * ```php
 * $expr = Expr::greaterThan(20, 'age')
 *     ->andStartsWith('Thomas', 'name');
 *
 * $values = array(
 *     'age' => 35,
 *     'name' => 'Thomas Edison',
 * );
 *
 * if ($expr->evaluate($values)) {
 *     // do something...
 * }
 * ```
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Expr
{
    /**
     * Filter a collection for entries matching the expression.
     *
     * @param array|ArrayAccess|Traversable $collection An array or an object
     *                                                  implementing Traversable
     *                                                  and ArrayAccess.
     * @param Expression                    $expr       The expression to
     *                                                  evaluate for each entry.
     *
     * @return array|ArrayAccess|Traversable The filtered collection.
     */
    public static function filter($collection, Expression $expr)
    {
        if (is_array($collection)) {
            return array_filter($collection, array($expr, 'evaluate'));
        }

        if (!($collection instanceof Traversable && $collection instanceof ArrayAccess)) {
            throw new InvalidArgumentException(sprintf(
                'Expected an array or an instance of Traversable and ArrayAccess. Got: %s',
                is_object($collection) ? get_class($collection) : gettype($collection)
            ));
        }

        $clone = clone $collection;

        foreach ($collection as $key => $value) {
            if (!$expr->evaluate($value)) {
                unset($clone[$key]);
            }
        }

        return $clone;
    }

    /**
     * Returns the expression.
     *
     * Facilitates usage of expressions on PHP < 7.
     *
     * @param Expression $expr An expression.
     *
     * @return Expression The expression.
     */
    public static function expr(Expression $expr)
    {
        return $expr;
    }

    /**
     * Negate an expression.
     *
     * @param Expression $expr The negated expression.
     *
     * @return Not The created negation.
     */
    public static function not(Expression $expr)
    {
        return new Not($expr);
    }

    /**
     * Create a conjunction.
     *
     * @param Expression[] $conjuncts The conjuncts.
     *
     * @return AndX The created conjunction.
     */
    public static function andX(array $conjuncts)
    {
        return new AndX($conjuncts);
    }

    /**
     * Create a disjunction.
     *
     * @param Expression[] $disjuncts The disjuncts.
     *
     * @return OrX The created disjunction.
     */
    public static function orX(array $disjuncts)
    {
        return new OrX($disjuncts);
    }

    /**
     * Always true (tautology).
     *
     * @return AlwaysTrue The created expression.
     */
    public static function true()
    {
        return new AlwaysTrue();
    }

    /**
     * Always false (contradiction).
     *
     * @return AlwaysFalse The created expression.
     */
    public static function false()
    {
        return new AlwaysFalse();
    }

    /**
     * Check that the value of an array key matches an expression.
     *
     * @param string|int $key  The array key.
     * @param Expression $expr The evaluated expression.
     *
     * @return Key The created expression.
     */
    public static function key($key, Expression $expr)
    {
        return new Key($key, $expr);
    }

    /**
     * Check that the result of a method call matches an expression.
     *
     * @param string     $methodName The name of the method to call.
     * @param mixed      $args...    The method arguments.
     * @param Expression $expr       The evaluated expression.
     *
     * @return Method The created expression.
     */
    public static function method($methodName, $args)
    {
        $args = func_get_args();
        $methodName = array_shift($args);
        $expr = array_pop($args);

        return new Method($methodName, $args, $expr);
    }

    /**
     * Check that the value of a property matches an expression.
     *
     * @param string     $propertyName The name of the property.
     * @param Expression $expr         The evaluated expression.
     *
     * @return Method The created expression.
     */
    public static function property($propertyName, Expression $expr)
    {
        return new Property($propertyName, $expr);
    }

    /**
     * Check that at least N entries of a traversable value match an expression.
     *
     * @param int        $count The minimum number of entries that need to match.
     * @param Expression $expr  The evaluated expression.
     *
     * @return AtLeast The created expression.
     */
    public static function atLeast($count, Expression $expr)
    {
        return new AtLeast($count, $expr);
    }

    /**
     * Check that at most N entries of a traversable value match an expression.
     *
     * @param int        $count The maximum number of entries that need to match.
     * @param Expression $expr  The evaluated expression.
     *
     * @return AtMost The created expression.
     */
    public static function atMost($count, Expression $expr)
    {
        return new AtMost($count, $expr);
    }

    /**
     * Check that exactly N entries of a traversable value match an expression.
     *
     * @param int        $count The number of entries that need to match.
     * @param Expression $expr  The evaluated expression.
     *
     * @return Exactly The created expression.
     */
    public static function exactly($count, Expression $expr)
    {
        return new Exactly($count, $expr);
    }

    /**
     * Check that all entries of a traversable value match an expression.
     *
     * @param Expression $expr The evaluated expression.
     *
     * @return All The created expression.
     */
    public static function all(Expression $expr)
    {
        return new All($expr);
    }

    /**
     * Check that the count of a collection matches an expression.
     *
     * @param Expression $expr The evaluated expression.
     *
     * @return Count The created expression.
     */
    public static function count(Expression $expr)
    {
        return new Count($expr);
    }

    /**
     * Check that a value is null.
     *
     * @return Same The created expression.
     */
    public static function null()
    {
        return new Same(null);
    }

    /**
     * Check that a value is not null.
     *
     * @return NotSame The created expression.
     */
    public static function notNull()
    {
        return new NotSame(null);
    }

    /**
     * Check that a value is empty.
     *
     * @return IsEmpty The created expression.
     */
    public static function isEmpty()
    {
        return new IsEmpty();
    }

    /**
     * Check that a value is not empty.
     *
     * @return Not The created expression.
     */
    public static function notEmpty()
    {
        return new Not(new IsEmpty());
    }

    /**
     * Check that a value is an instance of a given class.
     *
     * @param string $className The class name.
     *
     * @return IsEmpty The created expression.
     */
    public static function isInstanceOf($className)
    {
        return new IsInstanceOf($className);
    }

    /**
     * Check that a value equals another value.
     *
     * @param mixed $value The compared value.
     *
     * @return Equals The created expression.
     */
    public static function equals($value)
    {
        return new Equals($value);
    }

    /**
     * Check that a value does not equal another value.
     *
     * @param mixed $value The compared value.
     *
     * @return NotEquals The created expression.
     */
    public static function notEquals($value)
    {
        return new NotEquals($value);
    }

    /**
     * Check that a value is identical to another value.
     *
     * @param mixed $value The compared value.
     *
     * @return Same The created expression.
     */
    public static function same($value)
    {
        return new Same($value);
    }

    /**
     * Check that a value is not identical to another value.
     *
     * @param mixed $value The compared value.
     *
     * @return NotSame The created expression.
     */
    public static function notSame($value)
    {
        return new NotSame($value);
    }

    /**
     * Check that a value is greater than another value.
     *
     * @param mixed $value The compared value.
     *
     * @return GreaterThan The created expression.
     */
    public static function greaterThan($value)
    {
        return new GreaterThan($value);
    }

    /**
     * Check that a value is greater than or equal to another value.
     *
     * @param mixed $value The compared value.
     *
     * @return GreaterThanEqual The created expression.
     */
    public static function greaterThanEqual($value)
    {
        return new GreaterThanEqual($value);
    }

    /**
     * Check that a value is less than another value.
     *
     * @param mixed $value The compared value.
     *
     * @return LessThan The created expression.
     */
    public static function lessThan($value)
    {
        return new LessThan($value);
    }

    /**
     * Check that a value is less than or equal to another value.
     *
     * @param mixed $value The compared value.
     *
     * @return LessThanEqual The created expression.
     */
    public static function lessThanEqual($value)
    {
        return new LessThanEqual($value);
    }

    /**
     * Check that a value occurs in a list of values.
     *
     * @param array $values The compared values.
     *
     * @return In The created expression.
     */
    public static function in(array $values)
    {
        return new In($values);
    }

    /**
     * Check that a value matches a regular expression.
     *
     * @param string $regExp The regular expression.
     *
     * @return Matches The created expression.
     */
    public static function matches($regExp)
    {
        return new Matches($regExp);
    }

    /**
     * Check that a value starts with a given string.
     *
     * @param string $prefix The prefix string.
     *
     * @return StartsWith The created expression.
     */
    public static function startsWith($prefix)
    {
        return new StartsWith($prefix);
    }

    /**
     * Check that a value ends with a given string.
     *
     * @param string $suffix The suffix string.
     *
     * @return EndsWith The created expression.
     */
    public static function endsWith($suffix)
    {
        return new EndsWith($suffix);
    }

    /**
     * Check that a value contains a given string.
     *
     * @param string $string The sub-string.
     *
     * @return Contains The created expression.
     */
    public static function contains($string)
    {
        return new Contains($string);
    }

    /**
     * Check that a value key exists.
     *
     * @param string $keyName The key name.
     *
     * @return KeyExists The created expression.
     */
    public static function keyExists($keyName)
    {
        return new KeyExists($keyName);
    }

    /**
     * Check that a value key does not exist.
     *
     * @param string $keyName The key name.
     *
     * @return KeyNotExists The created expression.
     */
    public static function keyNotExists($keyName)
    {
        return new KeyNotExists($keyName);
    }

    /**
     * This class cannot be instantiated.
     */
    private function __construct()
    {
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Logic;

use Webmozart\Expression\Expr;
use Webmozart\Expression\Expression;

/**
 * A disjunction of expressions.
 *
 * A disjunction is a set of {@link Expression} instances connected by logical
 * "or" operators.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class OrX implements Expression
{
    /**
     * @var Expression[]
     */
    private $disjuncts = array();

    /**
     * Creates a disjunction of the given expressions.
     *
     * @param Expression[] $disjuncts The disjuncts.
     */
    public function __construct(array $disjuncts = array())
    {
        foreach ($disjuncts as $disjunct) {
            if ($disjunct instanceof self) {
                foreach ($disjunct->disjuncts as $expr) {
                    // $disjunct is guaranteed not to contain Disjunctions
                    $this->disjuncts[] = $expr;
                }
            } else {
                $this->disjuncts[] = $disjunct;
            }
        }
    }

    /**
     * Returns the disjuncts of the disjunction.
     *
     * @return Expression[] The disjuncts.
     */
    public function getDisjuncts()
    {
        return $this->disjuncts;
    }

    public function orX(Expression $expr)
    {
        if ($expr instanceof AlwaysFalse) {
            return $this;
        } elseif ($expr instanceof AlwaysTrue) {
            return $expr;
        }

        foreach ($this->disjuncts as $disjunct) {
            if ($disjunct->equivalentTo($expr)) {
                return $this;
            }
        }

        $disjuncts = $this->disjuncts;

        if ($expr instanceof self) {
            $disjuncts = array_merge($disjuncts, $expr->disjuncts);
        } else {
            $disjuncts[] = $expr;
        }

        return new self($disjuncts);
    }

    public function orNot(Expression $expr)
    {
        return $this->orX(Expr::not($expr));
    }

    public function orTrue()
    {
        return Expr::true();
    }

    public function orFalse()
    {
        return $this;
    }

    public function orKey($keyName, Expression $expr)
    {
        return $this->orX(Expr::key($keyName, $expr));
    }

    public function orMethod($methodName, $args)
    {
        return $this->orX(call_user_func_array(array('Webmozart\Expression\Expr', 'method'), func_get_args()));
    }

    public function orProperty($propertyName, Expression $expr)
    {
        return $this->orX(Expr::property($propertyName, $expr));
    }

    public function orAtLeast($count, Expression $expr)
    {
        return $this->orX(Expr::atLeast($count, $expr));
    }

    public function orAtMost($count, Expression $expr)
    {
        return $this->orX(Expr::atMost($count, $expr));
    }

    public function orExactly($count, Expression $expr)
    {
        return $this->orX(Expr::exactly($count, $expr));
    }

    public function orCount(Expression $expr)
    {
        return $this->orX(Expr::count($expr));
    }

    public function orAll(Expression $expr)
    {
        return $this->orX(Expr::all($expr));
    }

    public function orNull()
    {
        return $this->orX(Expr::null());
    }

    public function orNotNull()
    {
        return $this->orX(Expr::notNull());
    }

    public function orEmpty()
    {
        return $this->orX(Expr::isEmpty());
    }

    public function orNotEmpty()
    {
        return $this->orX(Expr::notEmpty());
    }

    public function orInstanceOf($className)
    {
        return $this->orX(Expr::isInstanceOf($className));
    }

    public function orEquals($value)
    {
        return $this->orX(Expr::equals($value));
    }

    public function orNotEquals($value)
    {
        return $this->orX(Expr::notEquals($value));
    }

    public function orSame($value)
    {
        return $this->orX(Expr::same($value));
    }

    public function orNotSame($value)
    {
        return $this->orX(Expr::notSame($value));
    }

    public function orGreaterThan($value)
    {
        return $this->orX(Expr::greaterThan($value));
    }

    public function orGreaterThanEqual($value)
    {
        return $this->orX(Expr::greaterThanEqual($value));
    }

    public function orLessThan($value)
    {
        return $this->orX(Expr::lessThan($value));
    }

    public function orLessThanEqual($value)
    {
        return $this->orX(Expr::lessThanEqual($value));
    }

    public function orIn(array $values)
    {
        return $this->orX(Expr::in($values));
    }

    public function orMatches($regExp)
    {
        return $this->orX(Expr::matches($regExp));
    }

    public function orStartsWith($prefix)
    {
        return $this->orX(Expr::startsWith($prefix));
    }

    public function orEndsWith($suffix)
    {
        return $this->orX(Expr::endsWith($suffix));
    }

    public function orContains($string)
    {
        return $this->orX(Expr::contains($string));
    }

    public function orKeyExists($keyName)
    {
        return $this->orX(Expr::keyExists($keyName));
    }

    public function orKeyNotExists($keyName)
    {
        return $this->orX(Expr::keyNotExists($keyName));
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($values)
    {
        foreach ($this->disjuncts as $expr) {
            if ($expr->evaluate($values)) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (get_class($this) !== get_class($other)) {
            return false;
        }

        /* @var static $other */
        $leftDisjuncts = $this->disjuncts;
        $rightDisjuncts = $other->disjuncts;

        foreach ($leftDisjuncts as $leftDisjunct) {
            foreach ($rightDisjuncts as $j => $rightDisjunct) {
                if ($leftDisjunct->equivalentTo($rightDisjunct)) {
                    unset($rightDisjuncts[$j]);
                    continue 2;
                }
            }

            // $leftDisjunct was not found in $rightDisjuncts
            return false;
        }

        // All $leftDisjuncts were found. Check if any $rightDisjuncts are left
        return 0 === count($rightDisjuncts);
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return implode(' || ', array_map(function (Expression $disjunct) {
            return $disjunct instanceof AndX ? '('.$disjunct->toString().')' : $disjunct->toString();
        }, $this->disjuncts));
    }

    public function __toString()
    {
        return $this->toString();
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Countable;

/**
 * Checks that the count of a collection matches an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Count extends Selector
{
    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_array($value) && !$value instanceof Countable) {
            return false;
        }

        return $this->expr->evaluate(count($value));
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return 'count('.$this->expr->toString().')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Util;

/**
 * Contains string utility methods.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class StringUtil
{
    /**
     * Formats a value as string.
     *
     * @param mixed $value The value.
     *
     * @return string The value as string.
     */
    public static function formatValue($value)
    {
        if (null === $value) {
            return 'null';
        }

        if (true === $value) {
            return 'true';
        }

        if (false === $value) {
            return 'false';
        }

        if (is_string($value)) {
            return '"'.$value.'"';
        }

        if (is_object($value)) {
            return 'object';
        }

        if (is_array($value)) {
            return 'array';
        }

        return (string) $value;
    }

    /**
     * Formats a list of values as strings.
     *
     * @param array $values The values.
     *
     * @return array The values as strings.
     */
    public static function formatValues(array $values)
    {
        foreach ($values as $key => $value) {
            $values[$key] = self::formatValue($value);
        }

        return $values;
    }

    /**
     * May not be instantiated.
     */
    private function __construct()
    {
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\PhpUnit;

use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;
use Webmozart\Expression\Expression;

/**
 * Compares {@link Expression} objects for equality.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ExpressionComparator extends Comparator
{
    /**
     * {@inheritdoc}
     */
    public function accepts($expected, $actual)
    {
        return $expected instanceof Expression && $actual instanceof Expression;
    }

    /**
     * {@inheritdoc}
     */
    public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false, array &$processed = array())
    {
        if (get_class($actual) !== get_class($expected)) {
            throw new ComparisonFailure(
                $expected,
                $actual,
                $this->exporter->export($expected),
                $this->exporter->export($actual),
                false,
                sprintf(
                    '%s is not instance of expected class "%s".',
                    $this->exporter->export($actual),
                    get_class($expected)
                )
            );
        }

        /** @var Expression $actual */
        if (!$actual->equivalentTo($expected)) {
            throw new ComparisonFailure(
                $expected,
                $actual,
                $expected->toString(),
                $actual->toString(),
                false,
                'Failed asserting that two expressions are equal.'
            );
        }
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Constraint;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\Literal;
use Webmozart\Expression\Util\StringUtil;

/**
 * Checks that a value is one of a list of values.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class In extends Literal
{
    /**
     * @var array
     */
    private $acceptedValues;

    /**
     * @var bool
     */
    private $strict;

    /**
     * Creates the expression.
     *
     * @param array $acceptedValues The accepted values.
     * @param bool  $strict         Whether to do strict comparison.
     */
    public function __construct(array $acceptedValues, $strict = true)
    {
        $this->acceptedValues = $acceptedValues;
        $this->strict = $strict;
    }

    /**
     * Returns the accepted values.
     *
     * @return array The accepted values.
     */
    public function getAcceptedValues()
    {
        return $this->acceptedValues;
    }

    /**
     * Returns whether the value is compared strictly.
     *
     * @return bool Returns `true` if using strict comparison (`===`) and
     *              `false` if using weak comparison (`==`).
     */
    public function isStrict()
    {
        return $this->strict;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        return in_array($value, $this->acceptedValues, $this->strict);
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (1 === count($this->acceptedValues)) {
            // In is logically equivalent to Same if strict and only one value
            if ($this->strict && $other instanceof Same) {
                return reset($this->acceptedValues) === $other->getComparedValue();
            }

            // In is logically equivalent to Equals if not strict and only one value
            if (!$this->strict && $other instanceof Equals) {
                return reset($this->acceptedValues) == $other->getComparedValue();
            }
        }

        // Since this class is final, we can check with instanceof
        if (!$other instanceof $this) {
            return false;
        }

        if ($this->strict !== $other->strict) {
            return false;
        }

        if (!$this->strict) {
            return $this->acceptedValues == $other->acceptedValues;
        }

        $acceptedValuesLeft = $this->acceptedValues;
        $acceptedValuesRight = $other->acceptedValues;

        // Ignore order
        sort($acceptedValuesLeft);
        sort($acceptedValuesRight);

        return $acceptedValuesLeft === $acceptedValuesRight;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        $values = array_map(function ($value) {
            return StringUtil::formatValue($value);
        }, $this->acceptedValues);

        return 'in('.implode(', ', $values).')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\AndX;
use Webmozart\Expression\Logic\OrX;
use Webmozart\Expression\Util\StringUtil;

/**
 * Checks whether the result of a method call matches an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Method extends Selector
{
    /**
     * @var string
     */
    private $methodName;

    /**
     * @var array
     */
    private $arguments;

    /**
     * Creates the expression.
     *
     * @param string     $methodName The name of the method to call.
     * @param array      $arguments  The arguments to pass to the method.
     * @param Expression $expr       The expression to evaluate for the result.
     */
    public function __construct($methodName, array $arguments, Expression $expr)
    {
        parent::__construct($expr);

        $this->methodName = $methodName;
        $this->arguments = $arguments;
    }

    /**
     * Returns the method name.
     *
     * @return string The method name.
     */
    public function getMethodName()
    {
        return $this->methodName;
    }

    /**
     * Returns the method arguments.
     *
     * @return array The method arguments.
     */
    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_object($value)) {
            return false;
        }

        if (!method_exists($value, $this->methodName)) {
            return false;
        }

        return $this->expr->evaluate(call_user_func_array(array($value, $this->methodName), $this->arguments));
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (!parent::equivalentTo($other)) {
            return false;
        }

        /* @var static $other */
        return $this->methodName === $other->methodName && $this->arguments === $other->arguments;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        $exprString = $this->expr->toString();
        $argsString = implode(', ', StringUtil::formatValues($this->arguments));

        if ($this->expr instanceof AndX || $this->expr instanceof OrX) {
            return $this->methodName.'('.$argsString.'){'.$exprString.'}';
        }

        // Append "functions" with "."
        if (isset($exprString[0]) && ctype_alpha($exprString[0])) {
            return $this->methodName.'('.$argsString.').'.$exprString;
        }

        return $this->methodName.'('.$argsString.')'.$exprString;
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Traversable;
use Webmozart\Expression\Expression;

/**
 * Checks that at least N iterator entries match an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class AtLeast extends Selector
{
    /**
     * @var int
     */
    private $count;

    /**
     * Creates the expression.
     *
     * @param int        $count The minimum number of entries that must match
     *                          the expression.
     * @param Expression $expr  The expression to evaluate with each entry.
     */
    public function __construct($count, Expression $expr)
    {
        parent::__construct($expr);

        $this->count = (int) $count;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_array($value) && !$value instanceof Traversable) {
            return false;
        }

        $found = 0;

        foreach ($value as $entry) {
            if ($this->expr->evaluate($entry)) {
                ++$found;

                if ($found >= $this->count) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (!parent::equivalentTo($other)) {
            return false;
        }

        /* @var static $other */
        return $this->count === $other->count;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return 'atLeast('.$this->count.', '.$this->expr->toString().')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Traversable;
use Webmozart\Expression\Expression;

/**
 * Checks that at most N iterator entries match an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class AtMost extends Selector
{
    /**
     * @var int
     */
    private $count;

    /**
     * Creates the expression.
     *
     * @param int        $count The maximum number of entries that must match
     *                          the expression.
     * @param Expression $expr  The expression to evaluate with each entry.
     */
    public function __construct($count, Expression $expr)
    {
        parent::__construct($expr);

        $this->count = (int) $count;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_array($value) && !$value instanceof Traversable) {
            return false;
        }

        $found = 0;

        foreach ($value as $entry) {
            if ($this->expr->evaluate($entry)) {
                ++$found;

                if ($found > $this->count) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (!parent::equivalentTo($other)) {
            return false;
        }

        /* @var static $other */
        return $this->count === $other->count;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return 'atMost('.$this->count.', '.$this->expr->toString().')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Selector;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\AndX;
use Webmozart\Expression\Logic\OrX;

/**
 * Checks whether an array key matches an expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Key extends Selector
{
    /**
     * @var string
     */
    private $key;

    /**
     * Creates the expression.
     *
     * @param string|int $key  The array key.
     * @param Expression $expr The expression to evaluate for the key.
     */
    public function __construct($key, Expression $expr)
    {
        parent::__construct($expr);

        $this->key = (string) $key;
    }

    /**
     * Returns the array key.
     *
     * @return string|int The array key.
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        if (!is_array($value)) {
            return false;
        }

        if (!array_key_exists($this->key, $value)) {
            return false;
        }

        return $this->expr->evaluate($value[$this->key]);
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (!parent::equivalentTo($other)) {
            return false;
        }

        /* @var static $other */
        return $this->key === $other->key;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        $exprString = $this->expr->toString();

        if ($this->expr instanceof AndX || $this->expr instanceof OrX) {
            return $this->key.'{'.$exprString.'}';
        }

        // Append "functions" with "."
        if (isset($exprString[0]) && ctype_alpha($exprString[0])) {
            return $this->key.'.'.$exprString;
        }

        return $this->key.$exprString;
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Constraint;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\Literal;

/**
 * Checks that a value is an instance of a given class name.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class IsInstanceOf extends Literal
{
    /**
     * @var array
     */
    private $className;

    /**
     * Creates the expression.
     *
     * @param string $className The accepted class name.
     */
    public function __construct($className)
    {
        $this->className = $className;
    }

    /**
     * Returns the accepted class name.
     *
     * @return array The accepted class name.
     */
    public function getClassName()
    {
        return $this->className;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        return $value instanceof $this->className;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        // Since this class is final, we can check with instanceof
        if (!$other instanceof $this) {
            return false;
        }

        return $this->className === $other->className;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return 'instanceOf('.$this->className.')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Logic;

use Webmozart\Expression\Expression;

/**
 * Negates another expression.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Not extends Literal
{
    /**
     * @var Expression
     */
    private $expr;

    /**
     * Creates the negation.
     *
     * @param Expression $expr The negated expression.
     */
    public function __construct(Expression $expr)
    {
        $this->expr = $expr;
    }

    /**
     * Returns the negated expression.
     *
     * @return Expression The negated expression.
     */
    public function getNegatedExpression()
    {
        return $this->expr;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        return !$this->expr->evaluate($value);
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        // Since this class is final, we can check with instanceof
        return $other instanceof $this && $this->expr->equivalentTo($other->expr);
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        $exprString = $this->expr->toString();

        if (isset($exprString[0]) && '(' === $exprString[0]) {
            return 'not'.$exprString;
        }

        return 'not('.$exprString.')';
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Constraint;

use Webmozart\Expression\Expression;
use Webmozart\Expression\Logic\Literal;
use Webmozart\Expression\Util\StringUtil;

/**
 * Checks that a value equals another value.
 *
 * The comparison is done using PHP's "==" equality operator.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class Equals extends Literal
{
    /**
     * @var string
     */
    private $comparedValue;

    /**
     * Creates the expression.
     *
     * @param mixed $comparedValue The compared value.
     */
    public function __construct($comparedValue)
    {
        $this->comparedValue = $comparedValue;
    }

    /**
     * Returns the compared value.
     *
     * @return mixed The compared value.
     */
    public function getComparedValue()
    {
        return $this->comparedValue;
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($value)
    {
        return $this->comparedValue == $value;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if ($other instanceof In && !$other->isStrict()) {
            return array($this->comparedValue) == $other->getAcceptedValues();
        }

        // Since this class is final, we can check with instanceof
        return $other instanceof $this && $this->comparedValue == $other->comparedValue;
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return '=='.StringUtil::formatValue($this->comparedValue);
    }
}


<?php

/*
 * This file is part of the webmozart/expression package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Webmozart\Expression\Logic;

use Webmozart\Expression\Expr;
use Webmozart\Expression\Expression;

/**
 * A disjunction of expressions.
 *
 * A disjunction is a set of {@link Expression} instances connected by logical
 * "and" operators.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class AndX implements Expression
{
    /**
     * @var Expression[]
     */
    private $conjuncts = array();

    /**
     * Creates a conjunction of the given expressions.
     *
     * @param Expression[] $conjuncts The conjuncts.
     */
    public function __construct(array $conjuncts = array())
    {
        foreach ($conjuncts as $conjunct) {
            if ($conjunct instanceof self) {
                foreach ($conjunct->conjuncts as $expr) {
                    // $conjunct is guaranteed not to contain Conjunctions
                    $this->conjuncts[] = $expr;
                }
            } else {
                $this->conjuncts[] = $conjunct;
            }
        }
    }

    /**
     * Returns the conjuncts of the conjunction.
     *
     * @return Expression[] The conjuncts.
     */
    public function getConjuncts()
    {
        return $this->conjuncts;
    }

    public function andX(Expression $expr)
    {
        if ($expr instanceof AlwaysTrue) {
            return $this;
        } elseif ($expr instanceof AlwaysFalse) {
            return $expr;
        }

        foreach ($this->conjuncts as $conjunct) {
            if ($conjunct->equivalentTo($expr)) {
                return $this;
            }
        }

        $conjuncts = $this->conjuncts;

        if ($expr instanceof self) {
            $conjuncts = array_merge($conjuncts, $expr->conjuncts);
        } else {
            $conjuncts[] = $expr;
        }

        return new self($conjuncts);
    }

    public function andNot(Expression $expr)
    {
        return $this->andX(Expr::not($expr));
    }

    public function andTrue()
    {
        return $this;
    }

    public function andFalse()
    {
        return Expr::false();
    }

    public function andKey($keyName, Expression $expr)
    {
        return $this->andX(Expr::key($keyName, $expr));
    }

    public function andMethod($methodName, $args)
    {
        return $this->andX(call_user_func_array(array('Webmozart\Expression\Expr', 'method'), func_get_args()));
    }

    public function andProperty($propertyName, Expression $expr)
    {
        return $this->andX(Expr::property($propertyName, $expr));
    }

    public function andAtLeast($count, Expression $expr)
    {
        return $this->andX(Expr::atLeast($count, $expr));
    }

    public function andAtMost($count, Expression $expr)
    {
        return $this->andX(Expr::atMost($count, $expr));
    }

    public function andExactly($count, Expression $expr)
    {
        return $this->andX(Expr::exactly($count, $expr));
    }

    public function andAll(Expression $expr)
    {
        return $this->andX(Expr::all($expr));
    }

    public function andCount(Expression $expr)
    {
        return $this->andX(Expr::count($expr));
    }

    public function andNull()
    {
        return $this->andX(Expr::null());
    }

    public function andNotNull()
    {
        return $this->andX(Expr::notNull());
    }

    public function andEmpty()
    {
        return $this->andX(Expr::isEmpty());
    }

    public function andNotEmpty()
    {
        return $this->andX(Expr::notEmpty());
    }

    public function andInstanceOf($className)
    {
        return $this->andX(Expr::isInstanceOf($className));
    }

    public function andEquals($value)
    {
        return $this->andX(Expr::equals($value));
    }

    public function andNotEquals($value)
    {
        return $this->andX(Expr::notEquals($value));
    }

    public function andSame($value)
    {
        return $this->andX(Expr::same($value));
    }

    public function andNotSame($value)
    {
        return $this->andX(Expr::notSame($value));
    }

    public function andGreaterThan($value)
    {
        return $this->andX(Expr::greaterThan($value));
    }

    public function andGreaterThanEqual($value)
    {
        return $this->andX(Expr::greaterThanEqual($value));
    }

    public function andLessThan($value)
    {
        return $this->andX(Expr::lessThan($value));
    }

    public function andLessThanEqual($value)
    {
        return $this->andX(Expr::lessThanEqual($value));
    }

    public function andIn(array $values)
    {
        return $this->andX(Expr::in($values));
    }

    public function andMatches($regExp)
    {
        return $this->andX(Expr::matches($regExp));
    }

    public function andStartsWith($prefix)
    {
        return $this->andX(Expr::startsWith($prefix));
    }

    public function andEndsWith($suffix)
    {
        return $this->andX(Expr::endsWith($suffix));
    }

    public function andContains($string)
    {
        return $this->andX(Expr::contains($string));
    }

    public function andKeyExists($keyName)
    {
        return $this->andX(Expr::keyExists($keyName));
    }

    public function andKeyNotExists($keyName)
    {
        return $this->andX(Expr::keyNotExists($keyName));
    }

    /**
     * {@inheritdoc}
     */
    public function evaluate($values)
    {
        foreach ($this->conjuncts as $expr) {
            if (!$expr->evaluate($values)) {
                return false;
            }
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function equivalentTo(Expression $other)
    {
        if (get_class($this) !== get_class($other)) {
            return false;
        }

        /* @var static $other */
        $leftConjuncts = $this->conjuncts;
        $rightConjuncts = $other->conjuncts;

        foreach ($leftConjuncts as $leftConjunct) {
            foreach ($rightConjuncts as $j => $rightConjunct) {
                if ($leftConjunct->equivalentTo($rightConjunct)) {
                    unset($rightConjuncts[$j]);
                    continue 2;
                }
            }

            // $leftConjunct was not found in $rightConjuncts
            return false;
        }

        // All $leftConjuncts were found. Check if any $rightConjuncts are left
        return 0 === count($rightConjuncts);
    }

    /**
     * {@inheritdoc}
     */
    public function toString()
    {
        return implode(' && ', array_map(function (Expression $conjunct) {
            return $conjunct instanceof OrX ? '('.$conjunct->toString().')' : $conjunct->toString();
        }, $this->conjuncts));
    }

    public function __toString()
    {
        return $this->toString();
    }
}