Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

@@ -11,100 +11,126 @@
namespace IPS\core\modules\front\discover;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use DateTimeZone;
use Exception;
use IPS\Application;
use IPS\Content\Reaction;
use IPS\Content\Search\ContentFilter;
use IPS\Content\Search\Index;
use IPS\Content\Search\Query;
use IPS\Content\Search\Results;
use IPS\core\DataLayer;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Dispatcher\Controller;
use IPS\Helpers\Form;
use IPS\Helpers\Form\DateRange;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use function array_slice;
use function count;
use function defined;
use function in_array;
use function intval;
use function is_numeric;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Most popular things
*/
class _popular extends \IPS\Dispatcher\Controller
class popular extends Controller
{
/**
* Execute
*
* @return void
*/
public function execute()
public function execute() : void
{
/* Ensure that the rep system is enabled and the leaderboard is enabled */
if ( ! \IPS\Settings::i()->reputation_enabled or ! \IPS\Settings::i()->reputation_leaderboard_on )
if ( ! Settings::i()->reputation_enabled or ! Settings::i()->reputation_leaderboard_on )
{
\IPS\Output::i()->error( 'module_no_permission', '2C343/1', 403, '' );
Output::i()->error( 'module_no_permission', '2C343/1', 403, '' );
}
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/search.css' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/leaderboard.css' ) );
if ( \IPS\Theme::i()->settings['responsive'] )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/search_responsive.css' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/leaderboard_responsive.css' ) );
}
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/search.css' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/leaderboard.css' ) );
/* Make sure default tab is first */
$tabs = array( \IPS\Settings::i()->reputation_leaderboard_default_tab );
$tabs = array( Settings::i()->reputation_leaderboard_default_tab );
foreach( array( 'leaderboard', 'history', 'members' ) as $addTab )
{
if ( $addTab != \IPS\Settings::i()->reputation_leaderboard_default_tab )
if ( $addTab != Settings::i()->reputation_leaderboard_default_tab )
{
$tabs[] = $addTab;
}
}
if ( \IPS\Application::appIsEnabled('cloud') and \IPS\cloud\Application::trendingIsEnabled( alwaysOn: FALSE ) )
if ( Application::appIsEnabled('cloud') and \IPS\cloud\Application::featureIsEnabled('trending') )
{
$tabs[] = 'trending';
}
$activeTab = ( isset( \IPS\Request::i()->tab ) and \in_array(\IPS\Request::i()->tab, $tabs ) ) ? \IPS\Request::i()->tab : \IPS\Settings::i()->reputation_leaderboard_default_tab;
$activeTab = ( isset( Request::i()->tab ) and in_array(Request::i()->tab, $tabs ) ) ? Request::i()->tab : Settings::i()->reputation_leaderboard_default_tab;
/* Initiate the breadcrumb */
\IPS\Output::i()->breadcrumb = array( array( \IPS\Http\Url::internal( "app=core&module=discover&controller=popular&tab=" . $activeTab, 'front', 'leaderboard_' . $activeTab ), \IPS\Member::loggedIn()->language()->addToStack('leaderboard_title') ) );
Output::i()->breadcrumb = array( array( Url::internal( "app=core&module=discover&controller=popular&tab=" . $activeTab, 'front', 'leaderboard_' . $activeTab ), Member::loggedIn()->language()->addToStack('leaderboard_title') ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_tabs_' . $activeTab );
Output::i()->title = Member::loggedIn()->language()->addToStack( 'leaderboard_tabs_' . $activeTab );
$content = $this->$activeTab();
/* Data Layer Context Property */
if ( \IPS\Settings::i()->core_datalayer_enabled AND ! \IPS\Request::i()->isAjax() )
if ( Settings::i()->core_datalayer_enabled AND ! Request::i()->isAjax() )
{
\IPS\core\DataLayer::i()->addContextProperty( 'community_area', 'leaderboard', true );
DataLayer::i()->addContextProperty( 'community_area', 'leaderboard' );
}
if ( \IPS\Request::i()->isAjax() )
if ( Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( $content ), 200, 'text/html' );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( $content ) );
}
else
{
/* Display */
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('popular')->tabs( $tabs, $activeTab, $content );
Output::i()->output = Theme::i()->getTemplate('popular')->tabs( $tabs, $activeTab, $content );
}
}
/**
* View top members
*
* @return void
* @return string
*/
protected function members()
protected function members() : string
{
/* Get filters */
$filters = array_merge( array( 'overview' => \IPS\Member::loggedIn()->language()->addToStack('overview') ), \IPS\Member::topMembersOptions( \IPS\Member::TOP_MEMBERS_FILTERS ) );
$filters = array_merge( array( 'overview' => Member::loggedIn()->language()->addToStack('overview') ), Member::topMembersOptions( Member::TOP_MEMBERS_FILTERS ) );
$activeFilter = 'overview';
if ( isset( \IPS\Request::i()->filter ) )
if ( isset( Request::i()->filter ) )
{
if ( array_key_exists( \IPS\Request::i()->filter, $filters ) )
if ( array_key_exists( Request::i()->filter, $filters ) )
{
$activeFilter = \IPS\Request::i()->filter;
$activeFilter = Request::i()->filter;
}
else
{
$possibleFilter = 'IPS\\' . str_replace( '_', '\\', \IPS\Request::i()->filter );
$possibleFilter = 'IPS\\' . str_replace( '_', '\\', Request::i()->filter );
if ( array_key_exists( $possibleFilter, $filters ) )
{
$activeFilter = $possibleFilter;
@@ -115,80 +141,80 @@ class _popular extends \IPS\Dispatcher\Controller
/* Get results */
if ( $activeFilter == 'overview' )
{
$output = \IPS\Theme::i()->getTemplate('popular')->topMembersOverview( \IPS\Member::topMembersOptions( \IPS\Member::TOP_MEMBERS_OVERVIEW ) );
$output = Theme::i()->getTemplate('popular')->topMembersOverview( Member::topMembersOptions( Member::TOP_MEMBERS_OVERVIEW ) );
}
else
{
$output = \IPS\Theme::i()->getTemplate('popular')->topMembersResults( $activeFilter, NULL, \IPS\Member::topMembers( $activeFilter, \IPS\Settings::i()->reputation_max_members ) );
$output = Theme::i()->getTemplate('popular')->topMembersResults( $activeFilter, NULL, Member::topMembers( $activeFilter, Settings::i()->reputation_max_members ) );
}
/* Output */
\IPS\Output::i()->linkTags['canonical'] = (string) \IPS\Http\Url::internal( "app=core&module=discover&controller=popular&tab=members", 'front', 'leaderboard_members' );
if ( \IPS\Request::i()->isAjax() and \IPS\Request::i()->topMembers )
Output::i()->linkTags['canonical'] = (string) Url::internal( "app=core&module=discover&controller=popular&tab=members", 'front', 'leaderboard_members' );
if ( Request::i()->isAjax() and Request::i()->topMembers )
{
\IPS\Output::i()->json( array( 'rows' => $output, 'extraHtml' => $filters[ $activeFilter ] ) );
Output::i()->json( array( 'rows' => $output, 'extraHtml' => $filters[ $activeFilter ] ) );
}
else
{
return \IPS\Theme::i()->getTemplate('popular')->topMembers( \IPS\Http\Url::internal( 'app=core&module=discover&controller=popular&tab=members', 'front', 'leaderboard_members' ), $filters, $activeFilter, $output );
return Theme::i()->getTemplate('popular')->topMembers( Url::internal( 'app=core&module=discover&controller=popular&tab=members', 'front', 'leaderboard_members' ), $filters, $activeFilter, $output );
}
}
/**
* View past leaders
*
* @return void
* @return string
*/
protected function history()
protected function history() : string
{
$table = new \IPS\Helpers\Table\Db( 'core_reputation_leaderboard_history', \IPS\Http\Url::internal( "app=core&module=discover&controller=popular&tab=history", 'front', 'leaderboard_history' ) );
$table = new \IPS\Helpers\Table\Db( 'core_reputation_leaderboard_history', Url::internal( "app=core&module=discover&controller=popular&tab=history", 'front', 'leaderboard_history' ) );
$table->where = array( 'leader_position <= 3' );
$table->limit = 21;
$table->limit = 60;
$table->selects = array( 'leader_member_id, leader_position, leader_rep_total', 'ABS(leader_date - leader_position) as leader_date' );
$table->joins = array( array( 'select' => 'core_members.*', 'from' => 'core_members', 'where' => 'core_reputation_leaderboard_history.leader_member_id=core_members.member_id' ) );
$table->include = array( 'leader_member_id', 'leader_date', 'leader_position', 'leader_rep_total' );
$table->noSort = array( 'leader_member_id', 'leader_position', 'leader_rep_total' );
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'popular', 'core', 'front' ), 'popularTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'popular', 'core', 'front' ), 'popularRows' );
$table->tableTemplate = array( Theme::i()->getTemplate( 'popular', 'core', 'front' ), 'popularTable' );
$table->rowsTemplate = array( Theme::i()->getTemplate( 'popular', 'core', 'front' ), 'popularRows' );
$table->title = 'leaderboard_history';
$table->mainColumn = 'leader_date';
$table->sortBy = $table->sortBy ?: 'leader_date';
$table->sortDirection = $table->sortDirection ?: 'DESC';
/* Like or what? */
if ( \IPS\Content\Reaction::isLikeMode() )
if ( Reaction::isLikeMode() )
{
\IPS\Member::loggedIn()->language()->words['leader_rep_total'] = \IPS\Member::loggedIn()->language()->addToStack( 'leader_rep_total_likes' );
\IPS\Member::loggedIn()->language()->words['leaderboard_history__desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_history_desc', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_history_likes') ) ) );
Member::loggedIn()->language()->words['leader_rep_total'] = Member::loggedIn()->language()->addToStack( 'leader_rep_total_likes' );
Member::loggedIn()->language()->words['leaderboard_history__desc'] = Member::loggedIn()->language()->addToStack( 'leaderboard_history_desc', FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack( 'leaderboard_history_likes') ) ) );
}
else
{
\IPS\Member::loggedIn()->language()->words['leader_rep_total'] = \IPS\Member::loggedIn()->language()->addToStack( 'leader_rep_total_rep' );
\IPS\Member::loggedIn()->language()->words['leaderboard_history__desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_history_desc', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_history_rep') ) ) );
Member::loggedIn()->language()->words['leader_rep_total'] = Member::loggedIn()->language()->addToStack( 'leader_rep_total_rep' );
Member::loggedIn()->language()->words['leaderboard_history__desc'] = Member::loggedIn()->language()->addToStack( 'leaderboard_history_desc', FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack( 'leaderboard_history_rep') ) ) );
}
/* Parsers */
$table->parsers = array(
'leader_member_id' => function( $val, $row )
{
return \IPS\Member::constructFromData( $row );
return Member::constructFromData( $row );
},
'leader_date' => function( $val )
{
return \IPS\DateTime::ts( $val )->setTimezone( new \DateTimeZone( \IPS\Settings::i()->reputation_timezone ) );
return DateTime::ts( $val )->setTimezone( new DateTimeZone( Settings::i()->reputation_timezone ) );
}
);
\IPS\Output::i()->linkTags['canonical'] = (string) \IPS\Http\Url::internal( "app=core&module=discover&controller=popular&tab=history", 'front', 'leaderboard_history' );
if ( \IPS\Request::i()->isAjax() )
Output::i()->linkTags['canonical'] = (string) Url::internal( "app=core&module=discover&controller=popular&tab=history", 'front', 'leaderboard_history' );
if ( Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( $table ), 200, 'text/html' );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( $table ) );
}
else
{
if ( $table->page > 1 )
{
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'title_with_page_number', FALSE, array( 'sprintf' => array( \IPS\Output::i()->title, $table->page ) ) );
Output::i()->title = Member::loggedIn()->language()->addToStack( 'title_with_page_number', FALSE, array( 'sprintf' => array( Output::i()->title, $table->page ) ) );
}
return (string) $table;
@@ -204,28 +230,28 @@ class _popular extends \IPS\Dispatcher\Controller
* 3) Fetch the search engine data and then...
* 4) Manually sort in PHP
*
* @return void
* @return string
*/
protected function leaderboard()
protected function leaderboard() : string
{
/* Figure out dates */
$dates = array();
$timezone = new \DateTimeZone( \IPS\Settings::i()->reputation_timezone );
$endDate = \IPS\DateTime::ts( time() )->setTimezone( $timezone );
$timezone = new DateTimeZone( Settings::i()->reputation_timezone );
$endDate = DateTime::ts( time() )->setTimezone( $timezone );
$firstRepDate = \IPS\Db::i()->select( 'MIN(rep_date)', 'core_reputation_index' )->first();
$firstIndexDate = \IPS\Content\Search\Index::i()->firstIndexDate();
$firstRepDate = intval( Db::i()->select( 'MIN(rep_date)', 'core_reputation_index' )->first() );
$firstIndexDate = intval( Index::i()->firstIndexDate() );
$appWhere = null;
$descApp = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_in_all_apps' );
$descApp = Member::loggedIn()->language()->addToStack( 'leaderboard_in_all_apps' );
$dates[ 'oldest' ] = \IPS\DateTime::ts( ( $firstRepDate > $firstIndexDate ) ? $firstRepDate : $firstIndexDate );
$dates[ 'oldest' ] = DateTime::ts( ( $firstRepDate > $firstIndexDate ) ? $firstRepDate : $firstIndexDate );
$oldestStamp = $dates[ 'oldest' ]->getTimeStamp();
$date = $dates[ 'oldest' ];
$aYearAgo = \IPS\DateTime::create()->setTimezone( $timezone )->sub( new \DateInterval( 'P1Y' ) );
$month = \IPS\DateTime::create()->setTimezone( $timezone )->sub( new \DateInterval( 'P1M' ) )->setTime( 0, 0 );
$week = \IPS\DateTime::create()->setTimezone( $timezone )->sub( new \DateInterval( 'P7D' ) )->setTime( 0, 0 );
$today = \IPS\DateTime::create()->setTimezone( $timezone )->setTime( 0, 0 );
$aYearAgo = DateTime::create()->setTimezone( $timezone )->sub( new DateInterval( 'P1Y' ) );
$month = DateTime::create()->setTimezone( $timezone )->sub( new DateInterval( 'P1M' ) )->setTime( 0, 0 );
$week = DateTime::create()->setTimezone( $timezone )->sub( new DateInterval( 'P7D' ) )->setTime( 0, 0 );
$today = DateTime::create()->setTimezone( $timezone )->setTime( 0, 0 );
if ( $aYearAgo->getTimeStamp() > $oldestStamp )
{
@@ -248,20 +274,20 @@ class _popular extends \IPS\Dispatcher\Controller
}
/* Got a date? */
if ( isset( \IPS\Request::i()->time ) and isset( $dates[ \IPS\Request::i()->time ] ) )
if ( isset( Request::i()->time ) and isset( $dates[ Request::i()->time ] ) )
{
$date = $dates[ \IPS\Request::i()->time ];
$date = $dates[ Request::i()->time ];
}
else if ( isset( $dates[ 'month' ] ) )
{
/* Set the default to month */
\IPS\Request::i()->time = 'month';
Request::i()->time = 'month';
$date = $dates[ 'month' ];
}
/* Applications */
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', TRUE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter' ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
@@ -272,70 +298,69 @@ class _popular extends \IPS\Dispatcher\Controller
$commentClass = NULL;
$reviewClass = NULL;
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Reactable' ) )
if ( IPS::classUsesTrait( $item, 'IPS\Content\Reactable' ) )
{
$areas[ $item::$application . '-' . $item::reactionType() ] = array( $item, \IPS\Member::loggedIn()->language()->addToStack( "{$item::$title}_pl" ) );
$areas[ $item::$application . '-' . $item::reactionType() ] = array( $item, Member::loggedIn()->language()->addToStack( "{$item::$title}_pl" ) );
}
if ( $item::supportsComments( \IPS\Member::loggedIn() ) and $commentClass = $item::$commentClass and \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
if ( $item::supportsComments( Member::loggedIn() ) and $commentClass = $item::$commentClass and IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
{
$commentClass = $item::$commentClass;
$supportsComments = \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $item::supportsComments( \IPS\Member::loggedIn() );
$supportsComments = IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $item::supportsComments( Member::loggedIn() );
if ( $supportsComments )
{
$areas[ $item::$application . '-' . $commentClass::reactionType() ] = array( $commentClass, \IPS\Member::loggedIn()->language()->addToStack( "{$commentClass::$title}_pl" ) );
$areas[ $item::$application . '-' . $commentClass::reactionType() ] = array( $commentClass, Member::loggedIn()->language()->addToStack( "{$commentClass::$title}_pl" ) );
}
}
if ( $item::supportsReviews( \IPS\Member::loggedIn() ) and $reviewClass = $item::$reviewClass and \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) )
if ( $item::supportsReviews( Member::loggedIn() ) and $reviewClass = $item::$reviewClass and IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) )
{
$areas[ $item::$application . '-' . $reviewClass::reactionType() ] = array( $reviewClass, \IPS\Member::loggedIn()->language()->addToStack( "{$reviewClass::$title}_pl" ) );
$areas[ $item::$application . '-' . $reviewClass::reactionType() ] = array( $reviewClass, Member::loggedIn()->language()->addToStack( "{$reviewClass::$title}_pl" ) );
}
}
$form = new \IPS\Helpers\Form( 'popular_date', 'continue' );
$form->class = 'ipsForm_vertical';
$customStart = ( isset( \IPS\Request::i()->custom_date_start ) and \is_numeric( \IPS\Request::i()->custom_date_start ) ) ? (int) \IPS\Request::i()->custom_date_start : NULL;
$customEnd = ( isset( \IPS\Request::i()->custom_date_end ) and \is_numeric( \IPS\Request::i()->custom_date_end ) ) ? (int) \IPS\Request::i()->custom_date_end : NULL;
$form->add( new \IPS\Helpers\Form\DateRange( 'custom_date', array( 'start' => $customStart, 'end' => $customEnd ), FALSE, array( 'start' => array( 'min' => $dates[ 'oldest' ], 'time' => false ) ) ) );
$form = new Form( 'popular_date', 'continue' );
$form->class = 'ipsForm--vertical ipsForm--leaderboard';
$customStart = ( isset( Request::i()->custom_date_start ) and is_numeric( Request::i()->custom_date_start ) ) ? (int) Request::i()->custom_date_start : NULL;
$customEnd = ( isset( Request::i()->custom_date_end ) and is_numeric( Request::i()->custom_date_end ) ) ? (int) Request::i()->custom_date_end : NULL;
$form->add( new DateRange( 'custom_date', array( 'start' => $customStart, 'end' => $customEnd ), FALSE, array( 'start' => array( 'min' => $dates[ 'oldest' ], 'time' => false ) ) ) );
if ( $values = $form->values() )
{
$url = \IPS\Request::i()->url()->stripQueryString( 'time' );
$url = Request::i()->url()->stripQueryString( 'time' );
if ( isset( $values[ 'custom_date' ][ 'start' ] ) and $values[ 'custom_date' ][ 'start' ] instanceof \IPS\DateTime )
if ( isset( $values[ 'custom_date' ][ 'start' ] ) and $values[ 'custom_date' ][ 'start' ] instanceof DateTime )
{
$url = $url->setQueryString( 'custom_date_start', $values[ 'custom_date' ][ 'start' ]->getTimeStamp() );
}
if ( isset( $values[ 'custom_date' ][ 'end' ] ) and $values[ 'custom_date' ][ 'end' ] instanceof \IPS\DateTime )
if ( isset( $values[ 'custom_date' ][ 'end' ] ) and $values[ 'custom_date' ][ 'end' ] instanceof DateTime )
{
$url = $url->setQueryString( 'custom_date_end', $values[ 'custom_date' ][ 'end' ]->getTimeStamp() );
}
\IPS\Output::i()->redirect( $url );
Output::i()->redirect( $url );
}
else
{
if ( $customStart )
{
$date = \IPS\DateTime::ts( $customStart )->setTimezone( $timezone )->setTime( 0, 0, 1 );
$date = DateTime::ts( $customStart )->setTimezone( $timezone )->setTime( 0, 0, 1 );
}
if ( $customEnd )
{
$endDate = \IPS\DateTime::ts( $customEnd )->setTimezone( $timezone )->setTime( 23, 59, 59 );
$endDate = DateTime::ts( $customEnd )->setTimezone( $timezone )->setTime( 23, 59, 59 );
}
}
/* Do we want results for a specific app */
$customApp = FALSE;
if ( isset( \IPS\Request::i()->in ) and isset( $areas[ \IPS\Request::i()->in ] ) )
if ( isset( Request::i()->in ) and isset( $areas[ Request::i()->in ] ) )
{
$appWhere = " AND rep_class='" . \IPS\Db::i()->escape_string( $areas[ \IPS\Request::i()->in ][ 0 ] ) . "'";
$descApp = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_in_app', FALSE, array( 'sprintf' => array( $areas[ \IPS\Request::i()->in ][ 1 ] ) ) );
$appWhere = " AND rep_class='" . Db::i()->escape_string( $areas[ Request::i()->in ][ 0 ] ) . "'";
$descApp = Member::loggedIn()->language()->addToStack( 'leaderboard_in_app', FALSE, array( 'sprintf' => array( $areas[ Request::i()->in ][ 1 ] ) ) );
$customApp = TRUE;
}
else
@@ -346,18 +371,18 @@ class _popular extends \IPS\Dispatcher\Controller
$repAreas[] = $area[0] ;
}
$appWhere = " AND " . \IPS\Db::i()->in( 'rep_class', $repAreas );
$appWhere = " AND " . Db::i()->in( 'rep_class', $repAreas );
}
$storeKey = NULL;
$hashes = NULL;
if ( ! $customStart and ! $customEnd AND ! $customApp )
{
$storeKey = 'leaderHashes_' . \IPS\Request::i()->time . '-' . md5( implode( ',', \IPS\Member::loggedIn()->groups ) );
$storeKey = 'leaderHashes_' . Request::i()->time . '-' . md5( implode( ',', Member::loggedIn()->groups ) );
if ( isset( \IPS\Data\Store::i()->$storeKey ) )
if ( isset( Store::i()->$storeKey ) )
{
$stored = \IPS\Data\Store::i()->$storeKey;
$stored = Store::i()->$storeKey;
if ( isset( $stored['hashes'] ) and isset( $stored['time'] ) and $stored['time'] > ( time() - 900 ) )
{
@@ -372,22 +397,22 @@ class _popular extends \IPS\Dispatcher\Controller
/* Prevent race condition */
if ( $storeKey )
{
\IPS\Data\Store::i()->$storeKey = array( 'time' => time(), 'hashes' => array() );
Store::i()->$storeKey = array( 'time' => time(), 'hashes' => array() );
}
/* Get rep hashes */
$inner = \IPS\Db::i()->select( 'class_type_id_hash, SUM(rep_rating) as total_rep', array( 'core_reputation_index', 'x' ), array( 'rep_date BETWEEN ' . $date->getTimeStamp() . ' AND ' . $endDate->getTimeStamp() . $appWhere ), NULL, NULL, 'class_type_id_hash' );
$repHashes = iterator_to_array( \IPS\Db::i()->select( 'class_type_id_hash, total_rep', $inner, NULL, 'x.total_rep desc', array( 0, 500 ) )->setKeyField('class_type_id_hash') );
$inner = Db::i()->select( 'class_type_id_hash, SUM(rep_rating) as total_rep', array( 'core_reputation_index', 'x' ), array( 'rep_date BETWEEN ' . $date->getTimeStamp() . ' AND ' . $endDate->getTimeStamp() . $appWhere ), NULL, NULL, 'class_type_id_hash' );
$repHashes = iterator_to_array( Db::i()->select( 'class_type_id_hash, total_rep', $inner, NULL, 'x.total_rep desc', array( 0, 500 ) )->setKeyField('class_type_id_hash') );
/* Now filter through permissions */
$searchHashes = \IPS\Content\Search\Index::i()->hashesWithPermission( array_keys( $repHashes ), \IPS\Member::loggedIn(), 500 );
$searchHashes = Index::i()->hashesWithPermission( array_keys( $repHashes ), Member::loggedIn(), 500 );
/* Filter out rep hashes not in search results */
$hashes = \array_slice( array_intersect_key( $repHashes, $searchHashes ), 0, 50 );
$hashes = array_slice( array_intersect_key( $repHashes, $searchHashes ), 0, 50 );
if ( $storeKey )
{
\IPS\Data\Store::i()->$storeKey = array( 'time' => time(), 'hashes' => $hashes );
Store::i()->$storeKey = array( 'time' => time(), 'hashes' => $hashes );
}
}
@@ -397,10 +422,10 @@ class _popular extends \IPS\Dispatcher\Controller
$results = array();
$preLoadMembers = array();
if ( \count( $hashes ) )
if ( count( $hashes ) )
{
/* Now get the reputation data */
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', array( \IPS\Db::i()->in( 'class_type_id_hash', array_keys( $hashes ) ) ) ) as $data )
foreach( Db::i()->select( '*', 'core_reputation_index', array( Db::i()->in( 'class_type_id_hash', array_keys( $hashes ) ) ) ) as $data )
{
$data['total_rep'] = $hashes[ $data['class_type_id_hash'] ]['total_rep'];
$repData[ $data['rep_class'] . '-' . $data['type_id'] ] = $data;
@@ -409,12 +434,12 @@ class _popular extends \IPS\Dispatcher\Controller
foreach( $classes as $class => $ids )
{
$or[] = \IPS\Content\Search\ContentFilter::initWithSpecificClass( $class )->onlyInIds( $ids );
$or[] = ContentFilter::initWithSpecificClass( $class )->onlyInIds( $ids );
}
/* Query and manually sort */
$sorted = array();
$search = \IPS\Content\Search\Query::init();
$search = Query::init();
/* Set the result to get as 50, as we pass in 50 unique hashes and the default is only 25, which means some may be missed as they are not sorted by rep count until after fetching */
$search->resultsToGet = 50;
@@ -432,7 +457,7 @@ class _popular extends \IPS\Dispatcher\Controller
unset( $array );
krsort( $sorted, SORT_NUMERIC );
$results = new \IPS\Content\Search\Results( $sorted, \count( $sorted ) );
$results = new Results( $sorted, count( $sorted ) );
/* Load data we need like the authors, etc */
$results->init();
@@ -442,40 +467,40 @@ class _popular extends \IPS\Dispatcher\Controller
$topContributors = array();
$innerQueryWhere = array();
$innerQueryWhere[] = array( 'member_received>0 ' . $appWhere . ' and rep_date BETWEEN ' . \intval( $date->getTimeStamp() ) . ' AND ' . \intval( $endDate->getTimeStamp() ) );
$innerQueryWhere[] = array( 'member_received>0 ' . $appWhere . ' and rep_date BETWEEN ' . $date->getTimeStamp() . ' AND ' . $endDate->getTimeStamp() );
if( \IPS\Settings::i()->leaderboard_excluded_groups )
if( Settings::i()->leaderboard_excluded_groups )
{
$innerQueryWhere[] = \IPS\Db::i()->in( 'member_group_id', explode( ',', \IPS\Settings::i()->leaderboard_excluded_groups ), TRUE );
$innerQueryWhere[] = Db::i()->in( 'member_group_id', explode( ',', Settings::i()->leaderboard_excluded_groups ), TRUE );
$innerQuery = \IPS\Db::i()->select( 'core_reputation_index.member_received as themember, SUM(rep_rating) as rep', 'core_reputation_index', $innerQueryWhere, NULL, NULL, 'themember' )->join( 'core_members', array( 'core_reputation_index.member_received = core_members.member_id' ) );
$innerQuery = Db::i()->select( 'core_reputation_index.member_received as themember, SUM(rep_rating) as rep', 'core_reputation_index', $innerQueryWhere, NULL, NULL, 'themember' )->join( 'core_members', array( 'core_reputation_index.member_received = core_members.member_id' ) );
}
else
{
$innerQuery = \IPS\Db::i()->select( 'core_reputation_index.member_received as themember, SUM(rep_rating) as rep', 'core_reputation_index', $innerQueryWhere, NULL, NULL, 'themember' );
$innerQuery = Db::i()->select( 'core_reputation_index.member_received as themember, SUM(rep_rating) as rep', 'core_reputation_index', $innerQueryWhere, NULL, NULL, 'themember' );
}
foreach( \IPS\Db::i()->select( 'themember, rep', array( $innerQuery, 'in' ), NULL, 'rep DESC', 4 )->setKeyField('themember')->setValueField('rep') as $member => $rep )
foreach( Db::i()->select( 'themember, rep', array( $innerQuery, 'in' ), NULL, 'rep DESC', 4 )->setKeyField('themember')->setValueField('rep') as $member => $rep )
{
$topContributors[ $member ] = $rep;
}
if ( \count( $topContributors ) )
if ( count( $topContributors ) )
{
$preLoadMembers = array_merge( $preLoadMembers, array_keys( $topContributors ) );
}
/* Load their data */
if ( \count( $preLoadMembers ) )
if ( count( $preLoadMembers ) )
{
foreach ( \IPS\Db::i()->select( '*', 'core_members', \IPS\Db::i()->in( 'member_id', array_unique( $preLoadMembers ) ) ) as $member )
foreach ( Db::i()->select( '*', 'core_members', Db::i()->in( 'member_id', array_unique( $preLoadMembers ) ) ) as $member )
{
\IPS\Member::constructFromData( $member );
Member::constructFromData( $member );
}
}
/* Work out the description for popular content */
if ( \IPS\Content\Reaction::isLikeMode() )
if ( Reaction::isLikeMode() )
{
$popularResultsSingle = 'popular_results_single_desc';
$popularResultsMany = 'popular_results_desc';
@@ -486,28 +511,33 @@ class _popular extends \IPS\Dispatcher\Controller
$popularResultsMany = 'popular_results_desc_rep';
}
$description = \IPS\Member::loggedIn()->language()->addToStack( ( $date->localeDate() == $endDate->localeDate() ) ? $popularResultsSingle : $popularResultsMany, NULL, array( 'sprintf' => array( $date->localeDate(), $descApp ) ) );
$description = Member::loggedIn()->language()->addToStack( ( $date->localeDate() == $endDate->localeDate() ) ? $popularResultsSingle : $popularResultsMany, NULL, array( 'sprintf' => array( $date->localeDate(), $descApp ) ) );
/* Are our offsets different? */
$tzOffsetDifference = NULL;
try
{
if ( \IPS\DateTime::ts( time() )->getOffset() != \IPS\DateTime::ts( time() )->setTimezone( $timezone )->getOffset() )
if ( DateTime::ts( time() )->getOffset() != DateTime::ts( time() )->setTimezone( $timezone )->getOffset() )
{
$tzOffsetDifference = \IPS\DateTime::ts( time() )->setTimezone( $timezone )->format('P');
$tzOffsetDifference = DateTime::ts( time() )->setTimezone( $timezone )->format('P');
}
}
catch( \Exception $ex ) { }
catch( Exception $ex ) { }
\IPS\Output::i()->linkTags['canonical'] = (string) \IPS\Http\Url::internal( "app=core&module=discover&controller=popular&tab=leaderboard", 'front', 'leaderboard_leaderboard' );
Output::i()->linkTags['canonical'] = (string) Url::internal( "app=core&module=discover&controller=popular&tab=leaderboard", 'front', 'leaderboard_leaderboard' );
/* If there are no results tell search engines not to index the page */
if( !\count( $results ) )
if( !count( $results ) )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex';
Output::i()->metaTags['robots'] = 'noindex';
}
/* Display */
return \IPS\Theme::i()->getTemplate('popular')->popularWrapper( $results, $areas, $topContributors, $dates, $description, $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) ), $tzOffsetDifference );
return Theme::i()->getTemplate('popular')->popularWrapper( $results, $areas, $topContributors, $dates, $description, $form->customTemplate( array( Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) ), $tzOffsetDifference );
}
protected function trending(): string
{
return Bridge::i()->popularTrending();
}
}
@@ -11,68 +11,80 @@
namespace IPS\core\modules\front\discover;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\core\Rss as RssClass;
use IPS\Dispatcher\Controller;
use IPS\Login;
use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Settings;
use OutOfRangeException;
use function defined;
use function in_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* rss
*/
class _rss extends \IPS\Dispatcher\Controller
class rss extends Controller
{
/**
* Display Feed
*
* @return void
*/
protected function manage()
protected function manage() : void
{
try
{
$feed = \IPS\core\Rss::load( \IPS\Request::i()->id );
$feed = RssClass::load( Request::i()->id );
}
catch( \OutOfRangeException $e )
catch( OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2C340/1', 404, '' );
Output::i()->error( 'node_error', '2C340/1', 404, '' );
}
if ( !$feed->_enabled )
{
\IPS\Output::i()->error( 'node_error_no_perm', '2C340/2', 403, '' );
Output::i()->error( 'node_error_no_perm', '2C340/2', 403, '' );
}
/* Specific Member? */
if ( isset( \IPS\Request::i()->member_id ) AND isset( \IPS\Request::i()->key ) )
if ( isset( Request::i()->member_id ) AND isset( Request::i()->key ) )
{
/* Load Member */
$member = \IPS\Member::load( \IPS\Request::i()->member_id );
$member = Member::load( Request::i()->member_id );
/* Make sure we have an actual member, and that the key matches. If it doesn't, we can bubble up and see if the feed works for guests, and just use that */
if ( $member->member_id AND \IPS\Login::compareHashes( $member->getUniqueMemberHash(), (string) \IPS\Request::i()->key ) )
if ( $member->member_id AND Login::compareHashes( $member->getUniqueMemberHash(), (string) Request::i()->key ) )
{
/* Make sure we have access to this feed. */
if ( $feed->groups == '*' OR $member->inGroup( $feed->groups ) )
{
/* Send It */
\IPS\Output::i()->sendOutput( $feed->generate( $member ), 200, 'text/xml' );
Output::i()->sendOutput( $feed->generate( $member ), 200, 'text/xml' );
}
else
{
\IPS\Output::i()->error( 'node_error_no_perm', '2C340/3', 403, '' );
Output::i()->error( 'node_error_no_perm', '2C340/3', 403, '' );
}
}
}
/* We're working with a guest. */
if ( $feed->groups == '*' OR \in_array( \IPS\Settings::i()->guest_group, $feed->groups ) )
if ( $feed->groups == '*' OR in_array( Settings::i()->guest_group, $feed->groups ) )
{
\IPS\Output::i()->sendOutput( $feed->generate(), 200, 'text/xml' );
Output::i()->sendOutput( $feed->generate(), 200, 'text/xml' );
}
else
{
\IPS\Output::i()->error( 'node_error_no_perm', '2C340/4', 403, '' );
Output::i()->error( 'node_error_no_perm', '2C340/4', 403, '' );
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,350 @@
<?php
/**
* @brief tag
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 02 Apr 2024
*/
namespace IPS\core\modules\front\discover;
use IPS\Content;
use IPS\Content\Filter;
use IPS\Content\Item;
use IPS\Content\Tag as SystemTag;
use IPS\Content\Taggable;
use IPS\DateTime;
use IPS\Db;
use IPS\Helpers\CoverPhoto;
use IPS\Helpers\CoverPhoto\Controller;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Date;
use IPS\Helpers\Form\Upload;
use IPS\Helpers\Table\Content as TableContent;
use IPS\IPS;
use IPS\Member;
use IPS\Output;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use function count;
use function defined;
use function sprintf;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* tag
*/
class tag extends Controller
{
/**
* @var SystemTag|null
*/
protected ?SystemTag $tag = null;
/**
* Execute
*
* @return void
*/
public function execute() : void
{
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/tags_page.css' ) );
try
{
$this->tag = SystemTag::load( Request::i()->tag, 'tag_text_seo' );
}
catch( OutOfRangeException )
{
Output::i()->error( 'node_error', '2C341/1', 404 );
}
parent::execute();
}
/**
* ...
*
* @return void
*/
protected function manage() : void
{
$contentTypes = SystemTag::getTaggableContentTypes();
$tabs = [];
foreach( $contentTypes as $type => $class )
{
$totalItems = $class::getItemsWithPermission( $this->_getWhereClause( $class ), null, null, 'read', Filter::FILTER_AUTOMATIC, 0, null, false, false, false, true );
if( $totalItems )
{
$tabs[] = $type;
}
}
if( count( $tabs ) == 1 )
{
$content = $this->_getTaggedContent( $tabs[0] );
}
elseif( isset( Request::i()->tab ) and Request::i()->tab and array_key_exists( Request::i()->tab, $contentTypes ) )
{
$content = $this->_getTaggedContent( Request::i()->tab );
}
else
{
$content = $this->_getOverview();
}
if( Request::i()->isAjax() )
{
Output::i()->sendOutput( $content );
}
Output::i()->title = $this->tag->text;
Output::i()->breadcrumb[] = [ $this->tag->url(), $this->tag->text ];
Output::i()->linkTags['canonical'] = (string) ( Request::i()->page > 1 ) ? $this->tag->url()->setPage( 'page', Request::i()->page ) : $this->tag->url() ;
Output::i()->metaTags['og:title'] = $this->tag->text;
Output::i()->metaTags['og:type'] = 'website';
Output::i()->metaTags['og:url'] = (string) $this->tag->url();
if( $image = $this->tag->coverPhotoFile() )
{
Output::i()->metaTags['og:image'] = (string) $image;
}
/* Do not set description tags for page 2+ as you end up with duplicate tags */
if ( Request::i()->page < 2 and $metaDescription = $this->tag->metaDescription() )
{
Output::i()->metaTags['description'] = $metaDescription;
Output::i()->metaTags['og:description'] = Output::i()->metaTags['description'];
}
Output::i()->output = Theme::i()->getTemplate( 'tags' )->view( $this->tag, $tabs, ( count( $tabs ) == 1 ? $content : '' ) );
}
/**
* Build the where clause to filter by content type and tag
*
* @param string $class
* @return array[]
*/
protected function _getWhereClause( string $class ) : array
{
/* @var Item $class */
$where = [
[ $class::$databasePrefix . $class::$databaseColumnId . ' IN (?)', Db::i()->select( 'tag_meta_id', 'core_tags', [ 'tag_meta_app=? and tag_meta_area=? and tag_text=?', $class::$application, $class::$module, $this->tag->text ] ) ]
];
/* Are any of these items pinned? */
if( $pinnedItems = $this->tag->getPinnedItems() )
{
$pinned = [];
$idColumn = $class::$databaseColumnId;
foreach( $pinnedItems as $pinnedItem )
{
if( $pinnedItem instanceof $class )
{
$pinned[] = $pinnedItem->$idColumn;
}
}
if( count( $pinned ) )
{
$where[] = [ Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, $pinned, true ) ];
}
}
return $where;
}
/**
* Build an overview with all available content types
*
* @return string
*/
protected function _getOverview() : string
{
$contentTypes = SystemTag::getTaggableContentTypes();
$pinnedItems = $this->tag->getPinnedItems();
$items = [];
foreach( Db::i()->select( '*', [ 'core_tags', 't' ], [
[ 't.tag_text=?', $this->tag->text ],
[ 'p.tag_perm_visible=?', 1 ],
[ '(p.tag_perm_text=? or ' . Db::i()->findInSet( 'p.tag_perm_text', Member::loggedIn()->groups ) . ')', '*' ]
], 't.tag_added desc', 50 )
->join( [ 'core_tags_perms', 'p' ], 't.tag_aai_lookup=p.tag_perm_aai_lookup and t.tag_aap_lookup=p.tag_perm_aap_lookup' ) as $row )
{
if( count( $items ) >= Settings::i()->tags_landing_limit )
{
break;
}
foreach( $contentTypes as $k => $itemClass )
{
if( $itemClass::$application == $row['tag_meta_app'] and $itemClass::$module == $row['tag_meta_area'] )
{
try
{
$item = $itemClass::load( $row['tag_meta_id'] );
$idColumn = $itemClass::$databaseColumnId;
$pinned = false;
if( is_array( $pinnedItems ) )
{
foreach( $pinnedItems as $pinnedItem )
{
if( get_class( $pinnedItem ) == $itemClass and $pinnedItem->$idColumn == $item->$idColumn )
{
$pinned = true;
break;
}
}
}
if( !$pinned )
{
$items[] = $item;
}
}
catch( OutOfRangeException ){}
break;
}
}
}
return Theme::i()->getTemplate( 'tags', 'core', 'front' )->overview( $items );
}
/**
* Show a table for the content type
*
* @param string $type
* @return string
*/
protected function _getTaggedContent( string $type ) : string
{
$allTypes = SystemTag::getTaggableContentTypes();
$class = $allTypes[ $type ];
$table = new TableContent( $class, $this->tag->url()->setQueryString( 'tab', $type ), $this->_getWhereClause( $class ) );
$table->classes[] = 'ipsData--grid ipsData--tags-data ipsData--tags-' . $type;
$table->limit = 20;
$table->title = sprintf( Member::loggedIn()->language()->get( 'tag_content_tagged' ), Member::loggedIn()->language()->get( $type . '_pl' ), $this->tag->text );
$table->rowsTemplate = [ Theme::i()->getTemplate( 'tags', 'core' ), 'contentTableRows' ];
$table->noModerate = true;
return (string) $table;
}
/**
* Pin an item to the top of the tag page
*
* @return void
*/
protected function pin() : void
{
if( !Member::loggedIn()->modPermission( 'can_pin_tagged' ) )
{
Output::i()->error( 'no_permission', '2C341/3', 403 );
}
$form = new Form;
$form->addMessage( 'tag_pin_info', 'ipsMessage ipsMessage--form' );
$form->add( new Date( 'tag_pin_end_date', -1, false, [ 'unlimited' => -1, 'unlimitedLang' => 'tag_pin_end_date_none' ] ) );
$form->add( new Upload( 'tag_pin_image', null, false, [
'storageExtension' => 'core_Tags',
'image' => true,
'multiple' => false
] ) );
if( $values = $form->values() )
{
$itemClass = Request::i()->itemClass;
/* what tab are we on? */
$activeTab = null;
foreach( SystemTag::getTaggableContentTypes() as $tab => $class )
{
if( $class == $itemClass )
{
$activeTab = $tab;
break;
}
}
try
{
$item = $itemClass::load( Request::i()->itemId );
$this->tag->pinItem( $item, ( $values['tag_pin_end_date'] instanceof DateTime ? $values['tag_pin_end_date'] : null ), $values['tag_pin_image'] );
}
catch( OutOfRangeException )
{
Output::i()->error( 'node_error', '2C341/2', 404 );
}
Output::i()->redirect( $this->tag->url()->setQueryString( 'tab', $activeTab ) );
}
Output::i()->output = (string) $form;
}
protected function unpin() : void
{
Request::i()->confirmedDelete();
if( !Member::loggedIn()->modPermission( 'can_pin_tagged' ) )
{
Output::i()->error( 'no_permission', '2C341/4', 403 );
}
$itemClass = Request::i()->itemClass;
try
{
$item = $itemClass::load( Request::i()->itemId );
$this->tag->removePinnedItem( $item );
}
catch( OutOfRangeException ){}
Output::i()->redirect( $this->tag->url() );
}
/**
* Get Cover Photo Storage Extension
*
* @return string
*/
protected function _coverPhotoStorageExtension(): string
{
return SystemTag::$coverPhotoStorageExtension;
}
/**
* Set Cover Photo
*
* @param CoverPhoto $photo New Photo
* @return void
*/
protected function _coverPhotoSet( CoverPhoto $photo ): void
{
$this->tag->cover_photo = (string) $photo->file;
$this->tag->cover_offset = $photo->offset;
$this->tag->save();
}
/**
* Get Cover Photo
*
* @return CoverPhoto
*/
protected function _coverPhotoGet(): CoverPhoto
{
return $this->tag->coverPhoto();
}
}