viewgit/inc/functions.php:22 Function utf8_encode() is deprecated [8192]
<?php /** * SeekQuarry/Yioop -- * Open Source Pure PHP Search Engine, Crawler, and Indexer * * Copyright (C) 2009 - 2014 Chris Pollett chris@pollett.org * * LICENSE: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * END LICENSE * * @author Chris Pollett chris@pollett.org * @package seek_quarry * @subpackage model * @license http://www.gnu.org/licenses/ GPL3 * @link http://www.seekquarry.com/ * @copyright 2009 - 2014 * @filesource */ if(!defined('BASE_DIR')) {echo "BAD REQUEST"; exit();} /** * For getPath */ require_once(BASE_DIR.'/lib/url_parser.php'); /** * This is class is used to handle * getting and saving the profile.php of the current search engine instance * * @author Chris Pollett * * @package seek_quarry * @subpackage model */ class ProfileModel extends Model { /** * These are fields whose values might be set in a Yioop instance * profile.php file * @var array */ var $profile_fields = array('API_ACCESS', 'AUTH_KEY', 'AUTHENTICATION_MODE', 'CACHE_LINK', 'CAPTCHA_MODE','DEBUG_LEVEL', 'DESCRIPTION_WEIGHT', 'DB_HOST', 'DBMS', 'DB_NAME','DB_PASSWORD', 'DB_USER', 'DEFAULT_LOCALE', 'FIAT_SHAMIR_MODULUS', 'GROUP_ITEM', 'IN_LINK','IP_LINK', 'LINK_WEIGHT', 'MAIL_PASSWORD', 'MAIL_SECURITY', 'MAIL_SENDER', 'MAIL_SERVER', 'MAIL_SERVERPORT', 'MAIL_USERNAME', 'MEMCACHE_SERVERS', 'MIN_RESULTS_TO_GROUP', 'NAME_SERVER', 'NEWS_MODE', 'PROXY_SERVERS', 'REGISTRATION_TYPE', 'ROBOT_INSTANCE', 'RSS_ACCESS', 'SERVER_ALPHA', 'SIGNIN_LINK', 'SIMILAR_LINK', 'SUBSEARCH_LINK', 'TITLE_WEIGHT', 'TOR_PROXY', 'USE_FILECACHE', 'USE_MAIL_PHP', 'USE_MEMCACHE', 'USE_PROXY', 'USER_AGENT_SHORT', 'WEB_URI', 'WEB_ACCESS', 'WORD_SUGGEST' ); /** * Associative array (table_name => SQL statement to create that table) * List is alphabetical and contains all Yioop tables. List is only * initialized after an @see initializeSql call. * @var array */ var $create_statements; /** * {@inheritDoc} */ function __construct($db_name = DB_NAME, $connect = true) { parent::__construct($db_name, $connect); $this->create_statements = array(); } /** * Used to construct $this->create_statements, the list of all SQL * CREATE statements needed to build a Yioop database * * @param object $dbm a datasource_manager object used to get strings * for autoincrement and serial types for a given db * @param array $dbinfo connect info for the database, also used in * getting autoincrement and serial types */ function initializeSql($dbm, $dbinfo) { $auto_increment = $dbm->autoIncrement($dbinfo); $serial = $dbm->serialType($dbinfo); $this->create_statements = array("ACTIVE_FETCHER" => "CREATE TABLE ACTIVE_FETCHER (NAME VARCHAR(16),FETCHER_ID INTEGER)", "AF_FETCHER_ID_INDEX" => "CREATE INDEX AF_FETCHER_ID_INDEX ON ACTIVE_FETCHER (FETCHER_ID)", "ACTIVITY" => "CREATE TABLE ACTIVITY (ACTIVITY_ID $serial PRIMARY KEY $auto_increment, TRANSLATION_ID INTEGER, METHOD_NAME VARCHAR(256))", "ACTIVITY_TRANSLATION_ID_INDEX" => "CREATE INDEX ACTIVITY_TRANSLATION_ID_INDEX ON ACTIVITY (TRANSLATION_ID)", "CRAWL_MIXES" => "CREATE TABLE CRAWL_MIXES (TIMESTAMP NUMERIC(11) PRIMARY KEY, NAME VARCHAR(16), OWNER_ID INTEGER, PARENT NUMERIC(11))", "CM_OWNER_ID_INDEX" => "CREATE INDEX CM_OWNER_ID_INDEX ON CRAWL_MIXES (OWNER_ID)", "CM_PARENT_INDEX" => "CREATE INDEX CM_PARENT_INDEX ON CRAWL_MIXES (PARENT)", "CURRENT_WEB_INDEX" => "CREATE TABLE CURRENT_WEB_INDEX (CRAWL_TIME NUMERIC(11) PRIMARY KEY)", "FEED_ITEM" => "CREATE TABLE FEED_ITEM (GUID CHAR(11) PRIMARY KEY, TITLE VARCHAR(512), LINK VARCHAR(256), DESCRIPTION VARCHAR(4096), PUBDATE INT, SOURCE_NAME VARCHAR(16))", "GROUP_ITEM" => "CREATE TABLE GROUP_ITEM (ID $serial PRIMARY KEY $auto_increment, PARENT_ID INTEGER, GROUP_ID INTEGER, USER_ID INTEGER, TITLE VARCHAR(512), DESCRIPTION VARCHAR(". MAX_GROUP_POST_LEN."), PUBDATE NUMERIC(11), UPS INTEGER DEFAULT 0, DOWNS INTEGER DEFAULT 0, TYPE INTEGER DEFAULT ". STANDARD_GROUP_ITEM.")", "GI_GROUP_ID_INDEX" => "CREATE INDEX GI_GROUP_ID_INDEX ON GROUP_ITEM (GROUP_ID)", "GI_USER_ID_INDEX" => "CREATE INDEX GI_USER_ID_INDEX ON GROUP_ITEM (USER_ID)", "GI_PARENT_ID_INDEX" => "CREATE INDEX GI_PARENT_ID_INDEX ON GROUP_ITEM (PARENT_ID)", "GROUP_ITEM_VOTE" => "CREATE TABLE GROUP_ITEM_VOTE( USER_ID INTEGER, ITEM_ID INTEGER)", "GROUP_PAGE" => "CREATE TABLE GROUP_PAGE ( ID $serial PRIMARY KEY $auto_increment, GROUP_ID INTEGER, DISCUSS_THREAD INTEGER, TITLE VARCHAR(512), PAGE VARCHAR(".MAX_GROUP_PAGE_LEN."), LOCALE_TAG VARCHAR(16), CONSTRAINT GID_TITLE_LOC UNIQUE(GROUP_ID, TITLE, LOCALE_TAG))", "GP_ID_INDEX" => "CREATE INDEX GP_ID_INDEX ON GROUP_PAGE (GROUP_ID, TITLE, LOCALE_TAG)", "GROUP_PAGE_HISTORY" => "CREATE TABLE GROUP_PAGE_HISTORY( PAGE_ID INTEGER, GROUP_ID INTEGER, EDITOR_ID INTEGER, TITLE VARCHAR(512), PAGE VARCHAR(".MAX_GROUP_PAGE_LEN."), EDIT_COMMENT VARCHAR(80), LOCALE_TAG VARCHAR(16), PUBDATE NUMERIC(11), PRIMARY KEY(PAGE_ID, PUBDATE))", "GROUPS" => "CREATE TABLE GROUPS ( GROUP_ID $serial PRIMARY KEY $auto_increment, GROUP_NAME VARCHAR(128), CREATED_TIME VARCHAR(20), OWNER_ID INTEGER, REGISTER_TYPE INTEGER, MEMBER_ACCESS INTEGER, VOTE_ACCESS INTEGER DEFAULT ". NON_VOTING_GROUP.")", /* NOTE: We are not using singular name GROUP for GROUPS as GROUP is a reserved SQL keyword */ "GRP_OWNER_ID_INDEX" => "CREATE INDEX GRP_OWNER_ID_INDEX ON GROUPS (OWNER_ID)", "GRP_MEMBER_ACCESS_INDEX" => "CREATE INDEX GRP_MEMBER_ACCESS_INDEX ON GROUPS(MEMBER_ACCESS)", "LOCALE" => "CREATE TABLE LOCALE(LOCALE_ID $serial PRIMARY KEY $auto_increment, LOCALE_TAG VARCHAR(16), LOCALE_NAME VARCHAR(256), WRITING_MODE CHAR(5))", "LCL_LOCALE_TAG_INDEX" => "CREATE INDEX LCL_LOCALE_TAG_INDEX ON LOCALE(LOCALE_TAG)", "MACHINE" => "CREATE TABLE MACHINE (NAME VARCHAR(16) PRIMARY KEY, URL VARCHAR(256) UNIQUE, HAS_QUEUE_SERVER INT, NUM_FETCHERS INTEGER, PARENT VARCHAR(16) )", "MEDIA_SOURCE" => "CREATE TABLE MEDIA_SOURCE ( TIMESTAMP NUMERIC(11) PRIMARY KEY, NAME VARCHAR(64) UNIQUE, TYPE VARCHAR(16), SOURCE_URL VARCHAR(256), THUMB_URL VARCHAR(256), LANGUAGE VARCHAR(7))", "MS_TYPE_INDEX" => "CREATE INDEX MS_TYPE_INDEX ON MEDIA_SOURCE(TYPE)", "MIX_COMPONENTS" => "CREATE TABLE MIX_COMPONENTS ( TIMESTAMP NUMERIC(11), FRAGMENT_ID INTEGER, CRAWL_TIMESTAMP NUMERIC(11), WEIGHT REAL, KEYWORDS VARCHAR(256), PRIMARY KEY(TIMESTAMP, FRAGMENT_ID, CRAWL_TIMESTAMP) )", "MIX_FRAGMENTS" => "CREATE TABLE MIX_FRAGMENTS ( TIMESTAMP NUMERIC(11),FRAGMENT_ID INTEGER, RESULT_BOUND INTEGER, PRIMARY KEY(TIMESTAMP, FRAGMENT_ID))", "ROLE" => "CREATE TABLE ROLE ( ROLE_ID $serial PRIMARY KEY $auto_increment,NAME VARCHAR(512))", "ROLE_ACTIVITY" => "CREATE TABLE ROLE_ACTIVITY (ROLE_ID INTEGER, ACTIVITY_ID INTEGER, PRIMARY KEY(ROLE_ID, ACTIVITY_ID))", "SUBSEARCH" => "CREATE TABLE SUBSEARCH ( LOCALE_STRING VARCHAR(32) PRIMARY KEY, FOLDER_NAME VARCHAR(16), INDEX_IDENTIFIER CHAR(13), PER_PAGE INT)", "TRANSLATION" => "CREATE TABLE TRANSLATION ( TRANSLATION_ID $serial PRIMARY KEY $auto_increment, IDENTIFIER_STRING VARCHAR(512) UNIQUE)", "TRANS_IDENTIFIER_STRING_INDEX" => "CREATE INDEX TRANS_IDENTIFIER_STRING_INDEX ON TRANSLATION(IDENTIFIER_STRING)", "TRANSLATION_LOCALE" => "CREATE TABLE TRANSLATION_LOCALE (TRANSLATION_ID INTEGER, LOCALE_ID INTEGER, TRANSLATION VARCHAR(4096), PRIMARY KEY(TRANSLATION_ID, LOCALE_ID))", "USERS" => "CREATE TABLE USERS(USER_ID $serial PRIMARY KEY $auto_increment, FIRST_NAME VARCHAR(16), LAST_NAME VARCHAR(16), USER_NAME VARCHAR(16) UNIQUE, EMAIL VARCHAR(60), PASSWORD CHAR(60), STATUS INTEGER, HASH CHAR(60), CREATION_TIME VARCHAR(20), UPS INTEGER DEFAULT 0, DOWNS INTEGER DEFAULT 0, ZKP_PASSWORD CHAR(200))", "USRS_USER_NAME_INDEX" => "CREATE INDEX USRS_USER_NAME_INDEX ON USERS(USER_NAME)", "USER_GROUP" => "CREATE TABLE USER_GROUP (USER_ID INTEGER, GROUP_ID INTEGER, STATUS INTEGER, JOIN_DATE NUMERIC(11), PRIMARY KEY (GROUP_ID, USER_ID) )", "USER_ROLE" => "CREATE TABLE USER_ROLE (USER_ID INTEGER, ROLE_ID INTEGER, PRIMARY KEY (ROLE_ID, USER_ID))", "USER_SESSION" => "CREATE TABLE USER_SESSION( USER_ID INTEGER PRIMARY KEY, SESSION VARCHAR(4096))", "VISITOR" => "CREATE TABLE VISITOR(ADDRESS VARCHAR(39), PAGE_NAME VARCHAR(16), END_TIME INTEGER, DELAY INTEGER, FORGET_AGE INTEGER, ACCESS_COUNT INTEGER, PRIMARY KEY(ADDRESS, PAGE_NAME))", "VERSION" => "CREATE TABLE VERSION(ID INTEGER PRIMARY KEY)", ); } /** * Creates a folder to be used to maintain local information about this * instance of the Yioop/SeekQuarry engine * * Creates the directory provides as well as subdirectories for crawls, * locales, logging, and sqlite DBs. * * @param string $directory parth and name of directory to create */ function makeWorkDirectory($directory) { $to_make_dirs = array($directory, "$directory/app", "$directory/archives", "$directory/cache", "$directory/classifiers", "$directory/data", "$directory/feeds", "$directory/locale", "$directory/log", "$directory/prepare", "$directory/schedules", "$directory/search_filters", "$directory/temp"); $dir_status = array(); foreach($to_make_dirs as $dir) { $dir_status[$dir] = $this->createIfNecessaryDirectory($dir); if( $dir_status[$dir] < 0) { return false; } } if($dir_status["$directory/locale"] == 1) { $this->db->copyRecursive(BASE_DIR."/locale", "$directory/locale"); } if($dir_status["$directory/data"] == 1) { $this->db->copyRecursive(BASE_DIR."/data", "$directory/data"); } return true; } /** * Outputs a profile.php file in the given directory containing profile * data based on new and old data sources * * This function creates a profile.php file if it doesn't exist. A given * field is output in the profile * according to the precedence that a new value is preferred to an old * value is prefered to the value that comes from a currently defined * constant. It might be the case that a new value for a given field * doesn't exist, etc. * * @param string $directory the work directory to output the profile.php * file * @param array $new_profile_data fields and values containing at least * some profile information (only $this->profile_fields * fields of $new_profile_data will be considered). * @param array $old_profile_data fields and values that come from * presumably a previously existing profile */ function updateProfile($directory, $new_profile_data, $old_profile_data) { $n = array(); $n[] = <<<EOT <?php /** * SeekQuarry/Yioop -- * Open Source Pure PHP Search Engine, Crawler, and Indexer * * Copyright (C) 2009-2012 Chris Pollett chris@pollett.org * * LICENSE: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * END LICENSE * * Computer generated file giving the key defines of directory locations * as well as database settings used to run the SeekQuarry/Yioop search engine * * @author Chris Pollett chris@pollett.org * @package seek_quarry * @subpackage config * @license http://www.gnu.org/licenses/ GPL3 * @link http://www.seekquarry.com/ * @copyright 2009-2012 * @filesource */ if(!defined('BASE_DIR')) {echo "BAD REQUEST"; exit();} EOT; foreach($this->profile_fields as $field) { if(isset($new_profile_data[$field])) { $profile[$field] = $new_profile_data[$field]; } else if(isset($old_profile_data[$field])) { $profile[$field] = $old_profile_data[$field]; } else if(defined($field)) { $profile[$field] = constant($field); } else { $profile[$field] = ""; } if($field == "NEWS_MODE" && $profile[$field] == "") { $profile[$field] = "news_off"; } if($field == "WEB_URI") { if(isset($_SERVER['REQUEST_URI'])) { $profile[$field] = UrlParser::getPath($_SERVER['REQUEST_URI']); } else { $profile[$field] = UrlParser::getPath(NAME_SERVER); } } if($field == "ROBOT_DESCRIPTION") continue; if($field != "DEBUG_LEVEL") { $profile[$field] = "\"{$profile[$field]}\""; } $n[] = "define('$field', {$profile[$field]});"; } $out = implode("\n", $n); if(file_put_contents($directory.PROFILE_FILE_NAME, $out) !== false) { @chmod($directory.PROFILE_FILE_NAME, 0777); if(isset($new_profile_data['ROBOT_DESCRIPTION'])) { $robot_path = LOCALE_DIR."/".DEFAULT_LOCALE."/pages/bot.thtml"; file_put_contents($robot_path, $new_profile_data['ROBOT_DESCRIPTION']); @chmod($robot_path, 0777); } return true; } return false; } /** * Creates a directory and sets it to world permission if it doesn't * aleady exist * * @param string $directory name of directory to create * @return int -1 on failure, 0 if already existed, 1 if created */ function createIfNecessaryDirectory($directory) { if(file_exists($directory)) return 0; else { @mkdir($directory); @chmod($directory, 0777); } if(file_exists($directory)) { return 1; } return -1; } /** * Check if $dbinfo provided the connection details for a Yioop/SeekQuarry * database. If it does provide a valid db connection but no data then try * to recreate the database from the default copy stored in /data dir. * * @param array $dbinfo has fields for DBMS, DB_USER, DB_PASSWORD, DB_HOST * and DB_NAME * @param array $skip_list an array of table or index names not to bother * creating or copying * @return bool returns true if can connect to/create a valid database; * returns false otherwise */ function migrateDatabaseIfNecessary($dbinfo, $skip_list = array()) { $test_dbm = $this->testDatabaseManager($dbinfo); if($test_dbm === false || $test_dbm === true) {return $test_dbm; } $this->initializeSql($test_dbm, $dbinfo); $copy_tables = array_diff(array_keys($this->create_statements), $skip_list); if(!($create_ok = $this->createDatabaseTables($test_dbm, $dbinfo))) { return false; } require_once(BASE_DIR."/models/datasources/sqlite3_manager.php"); $default_dbm = new Sqlite3Manager(); $default_dbm->connect("", "", "", BASE_DIR."/data/default.db"); if(!$default_dbm) {return false;} foreach($copy_tables as $table_or_index) { if($table_or_index != "CURRENT_WEB_INDEX" && stristr($table_or_index, "_INDEX")) { continue; } if(!$this->copyTable($table_or_index, $default_dbm, $test_dbm)) {return false;} } return true; } /** * On a blank database this method create all the tables necessary for * Yioop less those on a skip list * * @param object $dbm a DatabaseManager open to some DBMS and with a * blank database selected * @param array $skip_list an array of table or index names not to bother * creating * @return bool whether all of the creates were successful or not */ function createDatabaseTables($dbm, $dbinfo, $skip_list = array()) { $this->initializeSQL($dbm, $dbinfo); $create_statements = $this->create_statements; foreach($create_statements as $table_or_index => $statement) { if(in_array($table_or_index, $skip_list)) { continue; } if(!$result = $dbm->execute($statement)) { echo $statement." ERROR!"; return false; } } return true; } /** * Checks if $dbinfo provides info to connect to an working instance of * app db. * * @param array $dbinfo has field for DBMS, DB_USER, DB_PASSWORD, DB_HOST * and DB_NAME * @return mixed returns true if can connect to DBMS with username and * password, can select the given database name and that database * seems to be of Yioop/SeekQuarry type. If the connection works * but database isn't there it attempts to create it. If the * database is there but no data, then it returns a resource for * the database. Otherwise, it returns false. */ function testDatabaseManager($dbinfo) { if(!isset($dbinfo['DBMS'])) {return false;} $dbms_manager = ucfirst($dbinfo['DBMS'])."Manager"; // check if can establish a connect to dbms if(!class_exists($dbms_manager)) { require_once( BASE_DIR."/models/datasources/".$dbinfo['DBMS']."_manager.php"); } $test_dbm = new $dbms_manager(); $fields = array('DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'); foreach($fields as $field) { if(!isset($dbinfo[$field])) { $dbinfo[$field] = constant($field); } } $host = $dbinfo['DB_HOST']; // for postgress database needs to already exists $host = str_ireplace("database=".$dbinfo['DB_NAME'],"", $host); // informix, ibm (use connection string DSN) $host = str_replace(";;",";", $host); $conn = @$test_dbm->connect($host, $dbinfo['DB_USER'], $dbinfo['DB_PASSWORD'], ""); if($conn === false) {return false;} //check if can select db or if not create it $q = ""; if(isset($test_dbm->special_quote)) { $q = $test_dbm->special_quote; } @$test_dbm->execute("CREATE DATABASE $q".$dbinfo['DB_NAME']."$q"); $test_dbm->disconnect(); if(!$test_dbm->connect( $dbinfo['DB_HOST'], $dbinfo['DB_USER'], $dbinfo['DB_PASSWORD'], $dbinfo['DB_NAME'])) { return false; } /* check if need to create db contents. We check if any locale exists as proxy for contents being okay. Temporarily disable more aggressive yioop error handler while do this */ if((DEBUG_LEVEL & ERROR_INFO) == ERROR_INFO) { restore_error_handler(); } $sql = "SELECT LOCALE_ID FROM LOCALE"; $result = $test_dbm->execute($sql); if((DEBUG_LEVEL & ERROR_INFO) == ERROR_INFO) { set_error_handler("yioop_error_handler"); } if($result !== false && $test_dbm->fetchArray($result) !== false) { return true; } return $test_dbm; } /** * Copies the contents of table in the first database into the same named * table in a second database. It assumes the table exists in both databases * * @param string $table name of the table to be copied * @param resource $from_dbm database resource for the from table * @param resource $to_dbm database resource for the to table */ function copyTable($table, $from_dbm, $to_dbm) { $sql = "SELECT * FROM $table"; if(($result = $from_dbm->execute($sql)) === false) {return false;} while($row = $from_dbm->fetchArray($result)) { $statement = "INSERT INTO $table VALUES ("; $comma =""; foreach($row as $col=> $value) { $statement .= $comma." '".$to_dbm->escapeString($value)."'"; $comma = ","; } $statement .= ")"; if(($to_dbm->execute($statement)) === false) {return false;} } return true; } /** * Modifies the config.php file so the WORK_DIRECTORY define points at * $directory * * @param string $directory folder that WORK_DIRECTORY should be defined to */ function setWorkDirectoryConfigFile($directory) { $config = file_get_contents(BASE_DIR."/configs/config.php"); $start_machine_section = strpos($config,'/*+++ The next block of code'); if($start_machine_section === false) return false; $end_machine_section = strpos($config, '/*++++++*/'); if($end_machine_section === false) return false; $out = substr($config, 0, $start_machine_section); $out .= "/*+++ The next block of code is machine edited, change at \n". "your own risk, please use configure web page instead +++*/\n"; $out .= "define('WORK_DIRECTORY', '$directory');\n"; $out .= substr($config, $end_machine_section); if(file_put_contents(BASE_DIR."/configs/config.php", $out)) return true; return false; } /** * Reads a profile from a profile.php file in the provided directory * * @param string $work_directory directory to look for profile in * @return array associate array of the profile fields and their values */ function getProfile($work_directory) { $profile = array(); $profile_string = @file_get_contents($work_directory.PROFILE_FILE_NAME); foreach($this->profile_fields as $field) { $profile[$field] = $this->matchDefine($field, $profile_string); } return $profile; } /** * Finds the first occurrence of define('$defined', something) in $string * and returns something * * @param string $defined the constant being defined * @param string $string the haystack string to search in * @return string matched value of define if exists; empty string otherwise */ function matchDefine($defined, $string) { preg_match("/define\((?:\"$defined\"|\'$defined\')\,([^\)]*)\)/", $string, $match); $match = (isset($match[1])) ? trim($match[1]) : ""; $len = strlen($match); if( $len >=2 && ($match[0] == '"' || $match[0] == "'")) { $match = substr($match, 1, strlen($match) - 2); } return $match; } } ?>