Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 10 Nov 2016 */ namespace IPS\core; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Profanity Model */ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable { /** * @brief Database Table */ public static $databaseTable = 'core_profanity_filters'; /** * @brief Database Prefix */ public static $databasePrefix = ''; /** * @brief Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'wid'; /** * @brief [ActiveRecord] Database ID Fields */ protected static $databaseIdFields = array( 'type' ); /** * @brief [ActiveRecord] Multiton Map */ protected static $multitonMap = array(); /** * @brief Action Type */ public static $actionTypes = array( 'swap', 'moderate' ); /** * Table * * @return \IPS\Helpers\Table */ public static function table() { $table = new \IPS\Helpers\Table\Db( 'core_profanity_filters', \IPS\Http\Url::internal( 'app=core&module=settings&controller=posting&tab=profanityFilters' ) ); $table->langPrefix = 'profanity_'; $table->mainColumn = 'type'; /* Columns we need */ $table->include = array( 'type', 'action', 'm_exact' ); /* Default sort options */ $table->sortBy = $table->sortBy ?: 'type'; $table->sortDirection = $table->sortDirection ?: 'asc'; /* Filters */ $table->filters = array( 'profanity_require_approval' => "action='moderate'", 'profanity_replace_text' => "action='swap'", ); /* Search */ $table->quickSearch = 'type'; /* Custom parsers */ $table->parsers = array( 'action' => function( $val, $row ) { if ( $val == 'swap' ) { return \IPS\Member::loggedIn()->language()->addToStack( 'profanity_replace_with_x', FALSE, array( 'sprintf' => array( $row['swop'] ) ) ); } else { return \IPS\Member::loggedIn()->language()->addToStack('profanity_filter_action_moderate'); } }, 'm_exact' => function( $val, $row ) { return ( $val ) ? \IPS\Member::loggedIn()->language()->addToStack('profanity_filter_exact') : \IPS\Member::loggedIn()->language()->addToStack('profanity_filter_loose'); } ); /* Specify the root buttons */ $table->rootButtons['add'] = array( 'icon' => 'plus', 'title' => 'profanity_add', 'link' => \IPS\Http\Url::internal( 'app=core&module=settings&controller=posting&do=profanity' ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('profanity_add') ) ); $table->rootButtons['download'] = array( 'icon' => 'download', 'title' => 'download', 'link' => \IPS\Http\Url::internal( 'app=core&module=settings&controller=posting&do=downloadProfanity' ), 'data' => array( 'confirm' => '', 'confirmMessage' => \IPS\Member::loggedIn()->language()->addToStack('profanity_download'), 'confirmIcon' => 'info', 'confirmButtons' => json_encode( array( 'ok' => \IPS\Member::loggedIn()->language()->addToStack('download'), 'cancel' => \IPS\Member::loggedIn()->language()->addToStack('cancel') ) ) ) ); /* And the row buttons */ $table->rowButtons = function( $row ) { $return = array(); $return['edit'] = array( 'icon' => 'pencil', 'title' => 'edit', 'link' => \IPS\Http\Url::internal( 'app=core&module=settings&controller=posting&do=profanity&id=' ) . $row['wid'], 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') ) ); $return['delete'] = array( 'icon' => 'times', 'title' => 'delete', 'link' => \IPS\Http\Url::internal( 'app=core&module=settings&controller=posting&do=deleteProfanityFilters&id=' ) . $row['wid'], 'data' => array( 'delete' => '' ), ); return $return; }; return $table; } /** * Form * * @param \IPS\core\Profanity|NULL $current If we are editing, an \IPS\core\Profanity instance of the record * @return \IPS\Helpers\Form */ public static function form( \IPS\core\Profanity $current=NULL ) { $form = new \IPS\Helpers\Form; if ( !$current ) { $form->addTab('add'); } $form->add( new \IPS\Helpers\Form\Text( 'profanity_type', ( $current ) ? $current->type : NULL, NULL, array() ) ); $form->add( new \IPS\Helpers\Form\Radio( 'profanity_action', ( $current ) ? $current->action : 'swap', FALSE, array( 'options' => array( 'swap' => 'profanity_filter_action_swap', 'moderate' => 'profanity_filter_action_moderate' ), 'toggles' => array( 'swap' => array( 'profanity_swop' ), 'moderate' => array() ) ) ) ); $form->add( new \IPS\Helpers\Form\Text( 'profanity_swop', ( $current ) ? $current->swop : NULL, NULL, array(), NULL, NULL, NULL, 'profanity_swop' ) ); $form->add( new \IPS\Helpers\Form\Radio( 'profanity_m_exact', ( $current ) ? $current->m_exact : NULL, FALSE, array( 'options' => array( '1' => 'profanity_filter_exact', '0' => 'profanity_filter_loose' ) ), NULL, NULL, NULL, 'profanity_m_exact' ) ); if ( !$current ) { $form->addTab('upload'); $form->add( new \IPS\Helpers\Form\Upload( 'profanity_upload', NULL, NULL, array( 'allowedFileTypes' => array( 'xml' ), 'temporary' => TRUE ) ) ); } return $form; } /** * Create From Form * * @param array $values Array of values * @param \IPS\core\Profanity|NULL $current If we are editing, an \IPS\core\Profanity instance of the record * @return \IPS\core\Profanity */ public static function createFromForm( array $values, \IPS\core\Profanity $current=NULL ) { if ( $current ) { $obj = $current; } else { $obj = new static; } if ( array_key_exists( 'type', $values ) ) { $obj->type = $values['type']; } if ( array_key_exists( 'swop', $values ) ) { $obj->swop = $values['swop']; } if ( array_key_exists( 'm_exact', $values ) ) { $obj->m_exact = $values['m_exact']; } if ( array_key_exists( 'action', $values ) ) { if ( in_array( $values['action'], static::$actionTypes ) ) { $obj->action = $values['action']; } } $obj->save(); unset( \IPS\Data\Store::i()->profanityFilters ); return $obj; } /** * Get Profanity * * @return array */ public static function getProfanity() { $return = array(); if ( isset( \IPS\Data\Store::i()->profanityFilters ) ) { foreach( \IPS\Data\Store::i()->profanityFilters AS $id => $row ) { $return[ $id ] = static::constructFromData( $row ); } return $return; } $toCache = array(); foreach( \IPS\Db::i()->select( '*', 'core_profanity_filters' ) AS $row ) { $toCache[ $row['wid'] ] = $row; $return[ $row['wid'] ] = static::constructFromData( $row ); } \IPS\Data\Store::i()->profanityFilters = $toCache; return $return; } /** * Check if the content should be hidden by profanity or url filters * * @param string $content The content to check * @return bool */ public static function hiddenByFilters( $content ) { $return = static::_checkProfanityFilters( $content ); if ( $return == FALSE ) { $return = static::_checkUrlFilters( $content ); } return $return; } /** * Check Profanity Filters * * @param string $content The content to check * @return bool */ protected static function _checkProfanityFilters( $content ) { $looseProfanity = array(); $exactProfanity = array(); foreach( static::getProfanity() AS $profanity ) { if ( $profanity->action == 'moderate' ) { if ( $profanity->m_exact ) { $exactProfanity[] = $profanity->type; } else { $looseProfanity[] = $profanity->type; } } } /* Loose is easy - if any of the words are present, then mod queue */ if ( count( $looseProfanity ) ) { foreach( $looseProfanity AS $word ) { if ( mb_stristr( $content, $word ) ) { return TRUE; break; } } } /* Still here? Check exact - this gets a bit more complicated. */ if ( count( $exactProfanity ) ) { $words = array(); foreach( $exactProfanity AS $word ) { $words[] = preg_quote( $word, '/' ); } $split = preg_split( '/((?=<^|\b)(?:' . implode( '|', $words ) . ')(?=\b|$))/iu', $content, null, PREG_SPLIT_DELIM_CAPTURE ); if ( is_array( $split ) ) { foreach( $split AS $section ) { if ( in_array( $section, $exactProfanity ) ) { return TRUE; break; } } } } /* Still here? All good */ return FALSE; } /** * Check URL Filters * * @param string $content The content to check * @return bool */ protected static function _checkUrlFilters( $content ) { /* If we are allowing ANY URL's and not doing anything, then do that */ if ( \IPS\Settings::i()->ipb_url_filter_option == 'none' AND \IPS\Settings::i()->url_filter_any_action == 'allow' ) { return FALSE; } /* If we are using a black or white list, but not moderating, do that. */ if ( in_array( \IPS\Settings::i()->ipb_url_filter_option, array( 'black', 'white' ) ) AND \IPS\Settings::i()->url_filter_action == 'block' ) { return FALSE; } $urls = ( \IPS\Settings::i()->ipb_url_filter_option == 'black' ) ? explode( ',', \IPS\Settings::i()->ipb_url_blacklist ) : explode( ',', \IPS\Settings::i()->ipb_url_whitelist ); if ( \IPS\Settings::i()->ipb_url_filter_option == 'white' ) { $urls[] = "http://" . parse_url( \IPS\Settings::i()->base_url, PHP_URL_HOST ) . "/*"; $urls[] = "https://" . parse_url( \IPS\Settings::i()->base_url, PHP_URL_HOST ) . "/*"; } if ( $urls ) { try { /* Load the content so we can look for URL's */ $dom = new \IPS\Xml\DOMDocument; $dom->loadHTML( $content ); /* Gather up all URL's */ $selector = new \DOMXPath($dom); $tags = $selector->query('//img | //a | //iframe'); $good = NULL; foreach( $tags AS $tag ) { if ( ( $tag->hasAttribute( 'href' ) and !$tag->hasAttribute( 'data-mentionid' ) ) OR ( $tag->hasAttribute( 'src' ) AND !$tag->hasAttribute( 'data-emoticon' ) ) ) { if ( \IPS\Settings::i()->ipb_url_filter_option == 'none' ) { return TRUE; } $urlToCheck = $tag->hasAttribute( 'href' ) ? $tag->getAttribute( 'href' ) : $tag->getAttribute( 'src' ); foreach( $urls AS $url ) { /* Make sure we're not doing something weird like storing a blank URL */ if ( $url ) { $url = preg_quote( $url, '/' ); $url = str_replace( '\*', "(.*?)", $url ); if ( \IPS\Settings::i()->ipb_url_filter_option == 'black' ) { if ( preg_match( '/' . $url . '/i', $urlToCheck ) ) { return TRUE; } } else if ( \IPS\Settings::i()->ipb_url_filter_option == 'white' ) { /* @note http:// is hard-coded here as we're simply validating that the URL is on the same domain, so the protocol doesn't matter for the base_url replacement */ if ( !preg_match( '/' . $url . '/i', str_replace( '<___base_url___>', 'http://' . parse_url( \IPS\Settings::i()->base_url, PHP_URL_HOST ), $urlToCheck ), $matches ) ) { $good = FALSE; } else { $good = TRUE; break; } } } } } } if ( $good === FALSE AND \IPS\Settings::i()->ipb_url_filter_option == 'white' ) { return TRUE; } } catch( \Exception $e ) { } } return FALSE; } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { parent::delete(); unset( \IPS\Data\Store::i()->profanityFilters ); } /** * JSON Serialize * * @return array */ public function jsonSerialize() { return array( 'wid' => $this->wid, 'type' => $this->type, 'swop' => $this->swop, 'm_exact' => $this->m_exact, 'action' => $this->action ); } }