<?php
/**
 * Open Graph Protocol data class. Define and validate OGP values.
 *
 * @package open-graph-protocol-tools
 * @author Niall Kennedy <niall@niallkennedy.com>
 * @version 1.3
 * @copyright Public Domain
 */

/**
 * Open Graph protocol type labels are passed through gettext message interpreters for the current context.
 * Fake the interpreter function alias if not defined
 */
if ( !function_exists('_') ):
function _( $text, $domain='' ) {
	return $text;
}
endif;

/**
 * Validate inputted text against Open Graph Protocol requirements by parameter.
 *
 * @link http://ogp.me/ Open Graph Protocol
 * @version 1.3
 */
class OpenGraphProtocol {
	/**
	 * Version
	 * @var string
	 */
	const VERSION = '1.3';

	/**
	 * Should we remotely request each referenced URL to make sure it exists and returns the expected Internet media type?
	 * @var bool
	 */
	const VERIFY_URLS = false;

	/**
	 * Meta attribute name. Use 'property' if you prefer RDF or 'name' if you prefer HTML validation
	 * @var string
	 */
	const META_ATTR = 'property';

	/**
	 * Property prefix
	 * @var string
	 */
	const PREFIX = 'og';

	/**
	 * prefix namespace
	 * @var string
	 */
	const NS = 'http://ogp.me/ns#';

	/**
	 * Page classification according to a pre-defined set of base types.
	 *
	 * @var string
	 * @since 1.0
	 */
	protected $type;

	/**
	 * The title of your object as it should appear within the graph.
	 *
	 * @var string
	 * @since 1.0
	 */
	protected $title;

	/**
	 * If your object is part of a larger web site, the name which should be displayed for the overall site.
	 *
	 * @var string
	 * @since 1.0
	 */
	protected $site_name;

	/**
	 * A one to two sentence description of your object.
	 *
	 * @var string
	 * @since 1.0
	 */
	protected $description;

	/**
	 * The canonical URL of your object that will be used as its permanent ID in the graph.
	 *
	 * @var string
	 * @since 1.0
	 */
	protected $url;

	/**
	 * The word that appears before this object's title in a sentence
	 *
	 * @var string
	 * @since 1.3
	 */
	protected $determiner;

	/**
	 * Language and optional territory of page content.
	 * @var string
	 * @since 1.3
	 */
	protected $locale;

	/**
	 * An array of OpenGraphProtocolImage objects
	 *
	 * @var array
	 * @since 1.0
	 */
	protected $image;

	/**
	 * An array of OpenGraphProtocolAudio objects
	 *
	 * @var array
	 * @since 1.2
	 */
	protected $audio;

	/**
	 * An array of OpenGraphProtocolVideo objects
	 *
	 * @var array
	 * @since 1.2
	 */
	protected $video;

	/**
	 * Build Open Graph protocol HTML markup based on an array
	 *
	 * @param array $og associative array of OGP properties and values
	 * @param string $prefix optional prefix to prepend to all properties
	 */
	public static function buildHTML( array $og, $prefix=self::PREFIX ) {
		if ( empty($og) )
			return;

		$s = '';
		foreach ( $og as $property => $content ) {
			if ( is_object( $content ) || is_array( $content ) ) {
				if ( is_object( $content ) )
					$content = $content->toArray();
				if ( empty($property) || !is_string($property) )
					$s .= static::buildHTML( $content, $prefix );
				else
					$s .= static::buildHTML( $content, $prefix . ':' . $property );
			} elseif ( !empty($content) ) {
				$s .= '<meta ' . self::META_ATTR . '="' . $prefix;
				if ( is_string($property) && !empty($property) )
					$s .= ':' . htmlspecialchars( $property );
				$s .= '" content="' . htmlspecialchars($content) . '">' . PHP_EOL;
			}
		}
		return $s;
	}

	/**
	 * A list of allowed page types in the Open Graph Protocol
	 *
	 * @param Bool $flatten true for grouped types one level deep
	 * @link http://ogp.me/#types Open Graph Protocol object types
	 * @return array Array of Open Graph Protocol object types
	 */
	public static function supported_types( $flatten=false ) {
		$types = array(
			_('Activities') => array(
				'activity' => _('Activity'),
				'sport' => _('Sport')
			),
			_('Businesses') => array(
				'company' => _('Company'),
				'bar' => _('Bar'),
				'cafe' => _('Cafe'),
				'hotel' => _('Hotel'),
				'restaurant' => _('Restaurant')
			),
			_('Groups') => array(
				'cause' => _('Cause'),
				'sports_league' => _('Sports league'),
				'sports_team' => _('Sports team')
			),
			_('Organizations') => array(
				'band' => _('Band'),
				'government' => _('Government'),
				'non_profit' => _('Non-profit'),
				'school' => _('School'),
				'university' => _('University')
			),
			_('People') => array(
				'actor' => _('Actor or actress'),
				'athlete' => _('Athlete'), 
				'author' => _('Author'),
				'director' => _('Director'),
				'musician' => _('Musician'),
				'politician' => _('Politician'),
				'profile' => _('Profile'),
				'public_figure' => _('Public Figure')
			),
			_('Places') => array(
				'city' => _('City or locality'),
				'country' => _('Country'),
				'landmark' => _('Landmark'),
				'state_province' => _('State or province')
			),
			_('Products and Entertainment') => array(
				'music.album' => _('Music Album'),
				'book' => _('Book'),
				'drink' => _('Drink'),
				'video.episode' => _('Video episode'),
				'food' => _('Food'),
				'game' => _('Game'),
				'video.movie' => _('Movie'),
				'music.playlist' => _('Music playlist'),
				'product' => _('Product'),
				'music.radio_station' => _('Radio station'),
				'music.song' => _('Song'),
				'video.tv_show' => _('Television show'),
				'video.other' => _('Video')
			),
			_('Websites') => array(
				'article' => _('Article'),
				'blog' => _('Blog'),
				'website' => _('Website')
			)
		);
		if ( $flatten === true ) {
			$types_values = array();
			foreach ( $types as $category=>$values ) {
				$types_values = array_merge( $types_values, array_keys($values) );
			}
			return $types_values;
		} else {
			return $types;
		}
	}

	/**
	 * Facebook maps languages to a default territory and only accepts locales in this list. A few popular languages such as English and French support multiple territories.
	 * Map the Facebook list to avoid throwing errors in Facebook parsers that prevent further content indexing
	 *
	 * @link https://www.facebook.com/translations/FacebookLocales.xml Facebook locales
	 * @param bool $keys_only return only keys
	 * @return array associative array of locale code and locale name. locale code is in the format language_TERRITORY where language is an ISO 639-1 alpha-2 code and territory is an ISO 3166-1 alpha-2 code with special regions 'AR' and 'LA' for Arab region and Latin America respectively.
	 */
	public static function supported_locales( $keys_only=false ) {
		$locales = array(
			'af_ZA' => _('Afrikaans'),
			'ar_AR' => _('Arabic'),
			'az_AZ' => _('Azeri'),
			'be_BY' => _('Belarusian'),
			'bg_BG' => _('Bulgarian'),
			'bn_IN' => _('Bengali'),
			'bs_BA' => _('Bosnian'),
			'ca_ES' => _('Catalan'),
			'cs_CZ' => _('Czech'),
			'cy_GB' => _('Welsh'),
			'da_DK' => _('Danish'),
			'de_DE' => _('German'),
			'el_GR' => _('Greek'),
			'en_GB' => _('English (UK)'),
			'en_US' => _('English (US)'),
			'eo_EO' => _('Esperanto'),
			'es_ES' => _('Spanish (Spain)'),
			'es_LA' => _('Spanish (Latin America)'),
			'et_EE' => _('Estonian'),
			'eu_ES' => _('Basque'),
			'fa_IR' => _('Persian'),
			'fi_FI' => _('Finnish'),
			'fo_FO' => _('Faroese'),
			'fr_CA' => _('French (Canada)'),
			'fr_FR' => _('French (France)'),
			'fy_NL' => _('Frisian'),
			'ga_IE' => _('Irish'),
			'gl_ES' => _('Galician'),
			'he_IL' => _('Hebrew'),
			'hi_IN' => _('Hindi'),
			'hr_HR' => _('Croatian'),
			'hu_HU' => _('Hungarian'),
			'hy_AM' => _('Armenian'),
			'id_ID' => _('Indonesian'),
			'is_IS' => _('Icelandic'),
			'it_IT' => _('Italian'),
			'ja_JP' => _('Japanese'),
			'ka_GE' => _('Georgian'),
			'ko_KR' => _('Korean'),
			'ku_TR' => _('Kurdish'),
			'la_VA' => _('Latin'),
			'lt_LT' => _('Lithuanian'),
			'lv_LV' => _('Latvian'),
			'mk_MK' => _('Macedonian'),
			'ml_IN' => _('Malayalam'),
			'ms_MY' => _('Malay'),
			'nb_NO' => _('Norwegian (bokmal)'),
			'ne_NP' => _('Nepali'),
			'nl_NL' => _('Dutch'),
			'nn_NO' => _('Norwegian (nynorsk)'),
			'pa_IN' => _('Punjabi'),
			'pl_PL' => _('Polish'),
			'ps_AF' => _('Pashto'),
			'pt_PT' => _('Portuguese (Brazil)'),
			'ro_RO' => _('Romanian'),
			'ru_RU' => _('Russian'),
			'sk_SK' => _('Slovak'),
			'sl_SI' => _('Slovenian'),
			'sq_AL' => _('Albanian'),
			'sr_RS' => _('Serbian'),
			'sv_SE' => _('Swedish'),
			'sw_KE' => _('Swahili'),
			'ta_IN' => _('Tamil'),
			'te_IN' => _('Telugu'),
			'th_TH' => _('Thai'),
			'tl_PH' => _('Filipino'),
			'tr_TR' => _('Turkish'),
			'uk_UA' => _('Ukrainian'),
			'vi_VN' => _('Vietnamese'),
			'zh_CN' => _('Simplified Chinese (China)'),
			'zh_HK' => _('Traditional Chinese (Hong Kong)'),
			'zh_TW' => _('Traditional Chinese (Taiwan)')
		);
		if ( $keys_only === true ) {
			return array_keys($locales);
		} else {
			return $locales;
		}
	}

	/**
	 * Cleans a URL string, then checks to see if a given URL is addressable, returns a 200 OK response, and matches the accepted Internet media types (if provided).
	 *
	 * @param string $url Publicly addressable URL
	 * @param array $accepted_mimes Given URL correspond to an accepted Internet media (MIME) type.
	 * @return string cleaned URL string, or empty string on failure.
	 */
	public static function is_valid_url( $url, array $accepted_mimes = array() ) {
		if ( !is_string( $url ) || empty( $url ) )
			return '';

		/*
		 * Validate URI string by letting PHP break up the string and put it back together again
		 * Excludes username:password and port number URI parts
		 */
		$url_parts = parse_url( $url );
		$url = '';
		if ( isset( $url_parts['scheme'] ) && in_array( $url_parts['scheme'], array('http', 'https'), true ) ) {
			$url = "{$url_parts['scheme']}://{$url_parts['host']}{$url_parts['path']}";
			if ( empty( $url_parts['path'] ) )
				$url .= '/';
			if ( !empty( $url_parts['query'] ) )
				$url .= '?' . $url_parts['query'];
			if ( !empty( $url_parts['fragment'] ) )
				$url .= '#' . $url_parts['fragment'];
		}

		if ( !empty( $url ) ) {
			// test if URL exists
			$ch = curl_init( $url );
			curl_setopt( $ch, CURLOPT_TIMEOUT, 5 );
			curl_setopt( $ch, CURLOPT_FORBID_REUSE, true );
			curl_setopt( $ch, CURLOPT_NOBODY, true ); // HEAD
			curl_setopt( $ch, CURLOPT_USERAGENT, 'Open Graph protocol validator ' . self::VERSION . ' (+http://ogp.me/)' );
			if ( !empty($accepted_mimes) )
				curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Accept: ' . implode( ',', $accepted_mimes ) ) );
			$response = curl_exec( $ch );
			if ( curl_getinfo( $ch, CURLINFO_HTTP_CODE ) == 200 ) {
				if ( !empty($accepted_mimes) ) {
					$content_type = explode( ';', curl_getinfo( $ch, CURLINFO_CONTENT_TYPE ) );
					if ( empty( $content_type ) || !in_array( $content_type[0], $accepted_mimes ) )
						return '';
				}
			} else {
				return '';
			}
		}
		return $url;
	}

	/**
	 * Output the OpenGraphProtocol object as HTML elements string
	 *
	 * @return string meta elements
	 */
	public function toHTML() {
		return rtrim( static::buildHTML( get_object_vars($this) ), PHP_EOL );
	}

	/**
	 * @return String the type slug
	 */
	public function getType() {
		return $this->type;
	}

	/**
	 *
	 * @param String type slug
	 */
	public function setType( $type ) {
		if ( is_string($type) && in_array( $type, self::supported_types(true), true ) )
			$this->type = $type;
		return $this;
	}

	/**
	 * @return String document title
	 */
	public function getTitle() {
		return $this->title;
	}

	/**
	 * @param String $title document title
	 */
	public function setTitle( $title ) {
		if ( is_string($title) ) {
			$title = trim( $title );
			if ( mb_strlen( $title ) > 128 )
				$this->title = mb_substr( $title, 0, 128 );
			else
				$this->title = $title;
		}
		return $this;
	}

	/**
	 * @return String Site name
	 */
	public function getSiteName() {
		return $this->site_name;
	}

	/**
	 * @param String $site_name Site name
	 */
	public function setSiteName( $site_name ) {
		if ( is_string($site_name) && !empty($site_name) ) {
			$site_name = trim( $site_name );
			if ( mb_strlen( $site_name ) > 128 )
				$this->site_name = mb_substr( $site_name, 0, 128 );
			else
				$this->site_name = $site_name;
		}
		return $this;
	}

	/**
	 * @return String Description
	 */
	public function getDescription() {
		return $this->description;
	}

	/**
	 * @param String $description Document description
	 */
	public function setDescription( $description ) {
		if ( is_string($description) && !empty($description) ) {
			$description = trim( $description );
			if ( mb_strlen( $description ) > 255 )
				$this->description = mb_substr( $description, 0, 255 );
			else
				$this->description = $description;
		} 
		return $this;
	}

	/**
	 * @return String URL
	 */
	public function getURL() {
		return $this->url;
	}

	/**
	 * @param String $url Canonical URL
	 */
	public function setURL( $url ) {
		if ( is_string( $url ) && !empty( $url ) ) {
			$url = trim($url);
			if (self::VERIFY_URLS) {
				$url = self::is_valid_url( $url, array( 'text/html', 'application/xhtml+xml' ) );
			}
			if ( !empty( $url ) )
				$this->url = $url;
		}
		return $this;
	}

	/**
	 * @return string the determiner
	 */
	public function getDeterminer() {
		return $this->determiner;
	}

	public function setDeterminer( $determiner ) {
		if ( in_array($determiner, array('a','an','auto','the'), true) )
			$this->determiner = $determiner;
		return $this;
	}

	/**
	 * @return string language_TERRITORY
	 */
	public function getLocale() {
		return $this->locale;
	}

	/**
	 * @var string $locale locale in the format language_TERRITORY
	 */
	public function setLocale( $locale ) {
		if ( is_string($locale) && in_array($locale, static::supported_locales(true)) )
			$this->locale = $locale;
		return $this;
	}

	/**
	 * @return array OpenGraphProtocolImage array
	 */
	public function getImage() {
		return $this->image;
	}

	/**
	 * Add an image.
	 * The first image added is given priority by the Open Graph Protocol spec. Implementors may choose a different image based on size requirements or preferences.
	 *
	 * @param OpenGraphProtocolImage $image image object to add
	 */
	public function addImage( OpenGraphProtocolImage $image ) {
		$image_url = $image->getURL();
		if ( empty($image_url) )
			return;
		$image->removeURL();
		$value = array( $image_url, array($image) );
		if ( ! isset( $this->image ) )
			$this->image = array( $value );
		else
			$this->image[] = $value;
		return $this;
	}

	/**
	 * @return array OpenGraphProtocolAudio objects
	 */
	public function getAudio() {
		return $this->audio;
	}

	/**
	 * Add an audio reference
	 * The first audio is given priority by the Open Graph protocol spec.
	 *
	 * @param OpenGraphProtocolAudio $audio audio object to add
	 */
	public function addAudio( OpenGraphProtocolAudio $audio ) {
		$audio_url = $audio->getURL();
		if ( empty($audio_url) )
			return;
		$audio->removeURL();
		$value = array( $audio_url, array($audio) );
		if ( ! isset($this->audio) )
			$this->audio = array($value);
		else
			$this->audio[] = $value;
		return $this;
	}

	/**
	 * @return array OpenGraphProtocolVideo objects
	 */
	public function getVideo() {
		return $this->video;
	}

	/**
	 * Add a video reference
	 * The first video is given priority by the Open Graph protocol spec. Implementors may choose a different video based on size requirements or preferences.
	 *
	 * @param OpenGraphProtocolVideo $video video object to add
	 */
	public function addVideo( OpenGraphProtocolVideo $video ) {
		$video_url = $video->getURL();
		if ( empty($video_url) )
			return;
		$video->removeURL();
		$value = array( $video_url, array($video) );
		if ( ! isset( $this->video ) )
			$this->video = array( $value );
		else
			$this->video[] = $value;
		return $this;
	}
}

include_once dirname(__FILE__) . '/media.php';  // image, video, audio
include_once dirname(__FILE__) . '/objects.php'; // global objects: profile, article
?>