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\Content; /* To prevent PHP errors (extending class does not exist) revealing path */ use Exception; use IPS\IPS; use IPS\Node\CustomBadge; use IPS\Platform\Bridge; use IPS\Data\Store; use IPS\Db; use IPS\File; use IPS\Helpers\Form; use IPS\Helpers\Form\Radio; use IPS\Helpers\Form\Translatable; use IPS\Helpers\Form\Upload; use IPS\Http\Url; use IPS\Lang; use IPS\Member; use IPS\Node\Model; use IPS\Settings; use function count; use function defined; use function get_called_class; use function intval; if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Reaction Model */ class Reaction extends Model { use CustomBadge; /** * @brief Database Table */ public static ?string $databaseTable = 'core_reactions'; /** * @brief Database Prefix */ public static string $databasePrefix = 'reaction_'; /** * @brief Multiton Store */ protected static array $multitons; /** * @brief [ActiveRecord] ID Database Column */ public static string $databaseColumnId = 'id'; /** * @brief [ActiveRecord] Multiton Map */ protected static array $multitonMap = array(); /** * @brief [Node] Node Title */ public static string $nodeTitle = 'reactions'; /** * @brief [Node] Sortable */ public static bool $nodeSortable = TRUE; /** * @brief [Node] Positon Column */ public static ?string $databaseColumnOrder = 'position'; /** * @brief [Node] Modal Forms because Charles loves them so */ public static bool $modalForms = TRUE; /** * @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key */ public static ?string $titleLangPrefix = 'reaction_title_'; /** * @brief [Node] Enabled/Disabled Column */ public static ?string $databaseColumnEnabledDisabled = 'enabled'; /** * @brief Icon Cache */ public static array $icons = array(); /** * @brief Disable the number overlay for custom badges */ public static bool $customBadgeNumberOverlay = false; /** * @brief Toggle off these fields when generating a custom badge * * @var array|string[] */ public static array $customBadgeToggles = [ 'reaction_icon' ]; /** * Form * * @param Form $form The form * @return void */ public function form( Form &$form ): void { /* Allow SVGs without the obscure hash removing the file extension */ File::$safeFileExtensions[] = 'svg'; $form->add( new Translatable( 'reaction_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'reaction_title_' . $this->id : NULL ) ) ) ); $form->add( new Radio( 'reaction_value', $this->id ? $this->value : 1, TRUE, array( 'options' => array( 1 => 'positive', 0 => 'neutral', -1 => 'negative' ) ) ) ); parent::form( $form ); $form->add( new Upload( 'reaction_icon', $this->id ? File::get( 'core_Reaction', (string) $this->icon ) : NULL, false, array( 'image' => TRUE, 'storageExtension' => 'core_Reaction', 'storageContainer' => 'reactions', 'obscure' => FALSE ), null, null, null, 'reaction_icon' ) ); } /** * [Node] Format form values from add/edit form for save * * @param array $values Values from the form * @return array */ public function formatFormValues( array $values ): array { if ( !$this->id ) { $this->save(); } $values['use_custom'] = ( isset( $values['custombadge_use_custom'] ) and $values['custombadge_use_custom'] ); Lang::saveCustom( 'core', 'reaction_title_' . $this->id, $values['reaction_title'] ); unset( $values['reaction_title'] ); return parent::formatFormValues( $values ); } /** * [Node] Does the currently logged in user have permission to delete this node? * * @return bool */ public function canDelete(): bool { if ( $this->id === 1 OR Bridge::i()->liveTopicsUnconverted() ) { return FALSE; } return TRUE; } /** * @inheritDoc * @return bool */ public static function canAddRoot() : bool { /* When live topics are in session, prevent users from modifying the content */ if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canAddRoot(); } /** * @inheritdoc */ public function canAdd() : bool { if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canAdd(); } /** * @inheritdoc */ public function canEdit() : bool { if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canEdit(); } /** * @inheritdoc */ public function canManagePermissions() : bool { if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canManagePermissions(); } /** * @inheritdoc */ public function canMassManageContent() : bool { if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canMassManageContent(); } /** * @inheritdoc */ public function canCopy() : bool { if ( Bridge::i()->liveTopicsUnconverted() ) { return false; } return parent::canCopy(); } /** * Get Icon * * @return mixed */ public function get__icon(): mixed { if( $this->use_custom and $badge = $this->getRecordBadge() ) { return $badge->file()?->url; } if( $this->_data['icon'] ) { try { return File::get( 'core_Reaction', $this->_data['icon'] ); } catch( Exception $e ){} } return NULL; } /** * Get Description * * @return string|null */ public function get__description(): ?string { if ( $this->value == 1 ) { return Member::loggedIn()->language()->addToStack('positive'); } elseif ( $this->value == -1 ) { return Member::loggedIn()->language()->addToStack('negative'); } else { return Member::loggedIn()->language()->addToStack('neutral'); } } /** * Fetch All Root Nodes * * @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions * @param Member|NULL $member The member to check permissions for or NULL for the currently logged in member * @param mixed $where Additional WHERE clause * @param array|NULL $limit Limit/offset to use, or NULL for no limit (default) * @return (static|Model)[]|static[] */ public static function roots( ?string $permissionCheck='view', Member|null $member=NULL, mixed $where=array(), array $limit=NULL ): array { if ( !count( $where ) ) { $cacheKey = md5( get_called_class() . $permissionCheck ); if( isset( static::$rootsResult[ $cacheKey ] ) ) { return static::$rootsResult[ $cacheKey ]; } static::$rootsResult[ $cacheKey ] = array(); foreach( static::getStore() AS $reaction ) { static::$rootsResult[ $cacheKey ][ $reaction['reaction_id'] ] = static::constructFromData( $reaction ); } return static::$rootsResult[ $cacheKey ]; } else { return parent::roots( $permissionCheck, $member, $where, $limit ); } } /** * [Node] Get whether or not this node is enabled * * @note Return value NULL indicates the node cannot be enabled/disabled * @return bool|null */ protected function get__enabled(): ?bool { if ( $this->id == 1 ) { return NULL; } return parent::get__enabled(); } /** * Reaction Store * * @return array */ public static function getStore(): array { if ( !isset( Store::i()->reactions ) ) { Store::i()->reactions = iterator_to_array( Db::i()->select( '*', 'core_reactions', NULL, "reaction_position ASC" )->setKeyField( 'reaction_id' ) ); } return Store::i()->reactions; } /** * Is Like Mode * * @return bool */ public static function isLikeMode(): bool { return (bool) Settings::i()->reaction_is_likemode; } /** * @brief [ActiveRecord] Attempt to load from cache * @note If this is set to TRUE you MUST define a getStore() method to return the objects from cache */ protected static bool $loadFromCache = TRUE; /** * @brief [ActiveRecord] Caches * @note Defined cache keys will be cleared automatically as needed */ protected array $caches = array( 'reactions' ); /** * Clear any defined caches * * @param bool $removeMultiton Should the multiton record also be removed? * @return void */ public function clearCaches( bool $removeMultiton=FALSE ): void { parent::clearCaches( $removeMultiton ); static::updateLikeModeSetting(); } /** * [Node] Get buttons to display in tree * Example code explains return value * * @code * array( * array( * 'icon' => 'plus-circle', // Name of FontAwesome icon to use * 'title' => 'foo', // Language key to use for button's title parameter * 'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to * 'class' => 'modalLink' // CSS Class to use on link (Optional) * ), * ... // Additional buttons * ); * @endcode * @param Url $url Base URL * @param bool $subnode Is this a subnode? * @return array */ public function getButtons( Url $url, bool $subnode=FALSE ):array { $buttons = parent::getButtons( $url, $subnode ); if ( $this->canDelete() ) { $buttons['delete'] = array( 'icon' => 'times-circle', 'title' => 'delete', 'link' => $url->setQueryString( array( 'do' => 'delete', 'id' => $this->_id ) ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('delete') ), 'hotkey'=> 'd' ); } return $buttons; } /** * Update the setting which stores if likemode is enabled ( = only one reaction is being used ) * * @return void */ public static function updateLikeModeSetting(): void { Settings::i()->changeValues( array( 'reaction_is_likemode' => intval( count( static::enabledReactions() ) == 1 ) ) ); } /** * Return enabled reactions * * @return array */ public static function enabledReactions(): array { return array_filter( static::roots(), function( $reaction ){ return ( $reaction->_enabled !== FALSE ); } ); } /** * [ActiveRecord] Duplicate * * @return void */ public function __clone(): void { if ( $this->skipCloneDuplication === TRUE ) { return; } $oldIcon = $this->icon; parent::__clone(); try { $icon = File::get( 'core_Reaction', $oldIcon ); $newIcon = File::create( 'core_Reaction', $icon->originalFilename, $icon->contents() ); $this->icon = (string) $newIcon; } catch ( Exception $e ) { $this->icon = NULL; } $this->save(); } /** * Delete Record * * @return void */ public function delete(): void { try { File::get( 'core_Reaction', $this->icon )->delete(); } catch( Exception $ex ) { } parent::delete(); } /** * Get output for API * * @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials * @return array * @apiresponse int id ID number * @apiresponse string icon URL to the icon, * @apiresponse string value Value of the reaction */ public function apiOutput( Member $authorizedMember = NULL ): array { $return = array( 'id' => $this->id, 'title' => $this->title, 'icon' => (string) $this->icon, 'value' => $this->value ); return $return; } }