Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+194 -43
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -101,7 +101,27 @@ class _Content extends Table
* @brief Array of item IDs the current $member has posted in
*/
public $contentPostedIn = array();
/**
* @brief Callback method to adjust rows
*/
protected $callback = NULL;
/**
* @brief Get first comment
*/
protected $getFirstComment = FALSE;
/**
* @brief Get follower count
*/
protected $getFollowerCount = FALSE;
/**
* @brief Get reactions for content item if supported
*/
protected $getReactions = FALSE;
/**
* Constructor
*
@@ -113,9 +133,12 @@ class _Content extends Table
* @param string|NULL $permCheck Permission key to check
* @param bool $honorPinned Show pinned topics at the top of the table
* @param bool $showMovedLinks Show moved links in the result set.
* @param NULL|callable $callback Method to call to prepare the returned rows
* @param bool $getFirstComment Get the first comment for this item
* @param bool $getFollowerCount Get the follower counts for this item
* @return void
*/
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE )
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE, $callback=NULL, $getFirstComment=FALSE, $getFollowerCount=FALSE, $getReactions=FALSE )
{
/* Init */
$this->include = array();
@@ -126,6 +149,10 @@ class _Content extends Table
$this->honorPinned = $honorPinned;
$this->showMovedLinks = $showMovedLinks;
$this->permCheck = $permCheck;
$this->callback = $callback;
$this->getFirstComment = $getFirstComment;
$this->getFollowerCount = $getFollowerCount;
$this->getReactions = $getReactions;
/* Init */
parent::__construct( $baseUrl );
@@ -155,15 +182,7 @@ class _Content extends Table
/* Set breadcrumb */
if ( \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) and $club = $container->club() )
{
\IPS\core\FrontNavigation::$clubTabActive = TRUE;
\IPS\Output::i()->breadcrumb = array();
\IPS\Output::i()->breadcrumb[] = array( \IPS\Http\Url::internal( 'app=core&module=clubs&controller=directory', 'front', 'clubs_list' ), \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs') );
\IPS\Output::i()->breadcrumb[] = array( $club->url(), $club->name );
if ( \IPS\Settings::i()->clubs_header == 'sidebar' )
{
\IPS\Output::i()->sidebar['contextual'] = \IPS\Theme::i()->getTemplate( 'clubs', 'core' )->header( $club, $container, 'sidebar' );
}
$club->setBreadcrumbs( $container );
}
else
{
@@ -171,16 +190,25 @@ class _Content extends Table
{
\IPS\Output::i()->breadcrumb[] = array( $parent->url(), $parent->_title );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, $container->_title );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, $container->_title );
/* We do want the page in the canonical link otherwise Google won't index past page 1 */
$canonicalUrl = $baseUrl;
if ( $this->page > 1 )
{
$canonicalUrl = $canonicalUrl->setPage( $this->paginationKey, $this->page );
}
/* Meta tags */
\IPS\Output::i()->title = ( $this->page > 1 ) ? \IPS\Member::loggedIn()->language()->addToStack( 'title_with_page_number', FALSE, array( 'sprintf' => array( $container->metaTitle(), $this->page ) ) ) : $container->metaTitle();
\IPS\Output::i()->metaTags['title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['description'] = $container->metaDescription();
\IPS\Output::i()->metaTags['og:title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['og:description'] = $container->metaDescription();
\IPS\Output::i()->linkTags['canonical'] = (string) $baseUrl;
\IPS\Output::i()->linkTags['canonical'] = (string) $canonicalUrl;
}
/* Set available sort options */
@@ -188,9 +216,13 @@ class _Content extends Table
{
if ( isset( $class::$databaseColumnMap[ $k ] ) and !isset( $this->sortOptions[ $k ] ) )
{
$column = is_array( $class::$databaseColumnMap[ $k ] ) ? array_shift( $class::$databaseColumnMap[ $k ] ) : $class::$databaseColumnMap[ $k ];
$this->sortOptions[ $k ] = $class::$databasePrefix . $column;
$column = \is_array( $class::$databaseColumnMap[ $k ] ) ? $class::$databaseColumnMap[ $k ][0] : $class::$databaseColumnMap[ $k ];
/* In some circumstances `updated` and `last_comment` may be the same column, but we don't want two sort options */
if( !\in_array( $class::$databasePrefix . $column, $this->sortOptions ) )
{
$this->sortOptions[$k] = $class::$databasePrefix . $column;
}
}
}
if ( !$this->sortBy )
@@ -201,7 +233,7 @@ class _Content extends Table
}
elseif ( isset( $class::$databaseColumnMap['last_comment'] ) )
{
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . ( is_array( $class::$databaseColumnMap['last_comment'] ) ? $class::$databaseColumnMap['last_comment'][0] : $class::$databaseColumnMap['last_comment'] );
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . ( \is_array( $class::$databaseColumnMap['last_comment'] ) ? $class::$databaseColumnMap['last_comment'][0] : $class::$databaseColumnMap['last_comment'] );
}
else
{
@@ -233,7 +265,7 @@ class _Content extends Table
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
if ( \is_array( $class::$databaseColumnMap[ $k ] ) )
{
$cols = $class::$databaseColumnMap[ $k ];
$defaultSort = $class::$databaseTable . '.' . $class::$databasePrefix . array_pop( $cols );
@@ -246,6 +278,24 @@ class _Content extends Table
}
}
/* If we are limiting to a single container, check to see if this user can view them */
if ( $this->container and $this->permCheck )
{
try
{
if ( ! $this->container->can( $this->permCheck ) )
{
$this->count = 0;
return array();
}
}
catch( \OutOfBoundsException $e )
{
$this->count = 0;
return array();
}
}
$compareSortBy = $this->sortBy;
$lookFor = $class::$databaseTable . '.' . $class::$databasePrefix;
@@ -253,38 +303,134 @@ class _Content extends Table
{
$compareSortBy = mb_substr( $this->sortBy, mb_strlen( $lookFor ) );
}
$this->sortBy = in_array( $compareSortBy, $this->sortOptions ) ? $this->sortBy :
if( $this->sortBy AND \in_array( $this->sortBy, $class::$databaseColumnMap ) )
{
$len = mb_strlen( $class::$databaseTable );
$this->sortBy = ( mb_substr( $this->sortBy, 0, $len ) == $class::$databaseTable ) ? $this->sortBy : $class::$databaseTable . '.' . $this->sortBy;
}
$this->sortBy = \in_array( $compareSortBy, $this->sortOptions ) ? $this->sortBy :
( array_key_exists( $compareSortBy, $this->sortOptions ) ? $this->sortOptions[ $compareSortBy ] : $defaultSort );
/* Callback? */
if ( $this->advancedSearchCallback and !empty( $advancedSearchValues ) )
{
$obj = $this;
call_user_func( $this->advancedSearchCallback, $obj, $advancedSearchValues );
$advancedSearchCallback = $this->advancedSearchCallback;
$advancedSearchCallback( $obj, $advancedSearchValues );
}
/* What are we sorting by? */
$sortBy = $this->sortBy . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
if ( in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) and $this->honorPinned )
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) and $this->honorPinned )
{
$column = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['pinned'];
$sortBy = "{$column} DESC, {$sortBy}";
}
/* Specify filter in where clause */
$where = $this->where ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = $this->where ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Get results */
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->pages = ceil( $this->count / $this->limit );
$rows = iterator_to_array( $it );
$idField = $class::$databaseColumnId;
if ( isset( $this->getFirstComment ) and isset( $class::$databaseColumnMap['first_comment_id'] ) and $class::$firstCommentRequired )
{
$commentIds = array();
$firstCommentField = $class::$databaseColumnMap['first_comment_id'];
foreach( $rows as $item )
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
}
if ( \count( $commentIds ) )
{
$commentClass = $class::$commentClass;
$commentField = $commentClass::$databaseColumnId;
$comments = array();
foreach( $commentClass::getItemsWithPermission( array( \IPS\Db::i()->in( $commentClass::$databasePrefix . $commentField, array_values( $commentIds ) ) ), NULL, $this->limit ) as $cObj )
{
$comments[ $cObj->$commentField ] = $cObj;
}
foreach( $rows as $id => $item )
{
if ( isset( $commentIds[ $item->$idField ] ) and isset( $comments[ $commentIds[ $item->$idField ] ] ) )
{
$rows[ $id ]->firstComment = $comments[ $commentIds[ $item->$idField ] ];
}
}
}
}
if ( $this->getFollowerCount and \in_array( 'IPS\Content\Followable', class_implements( $class ) ) )
{
$followers = array();
foreach( $class::followersCounts( $rows ) as $follow )
{
$followers[ $follow['follow_rel_id'] ] = $follow['count'];
}
foreach( $rows as $id => $item )
{
if ( isset( $followers[ $item->$idField ] ) )
{
$rows[ $id ]->followerCount = $followers[ $item->$idField ];
}
else
{
$rows[ $id ]->followerCount = 0;
}
}
}
if ( $this->getReactions and \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) )
{
/* Get enabled reactions */
$reactions = array();
$itemIds = array();
foreach( $rows as $item )
{
$itemIds[] = $item->$idField;
}
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'rep_class=? AND ' . \IPS\Db::i()->in( 'item_id', $itemIds ), $class::$commentClass ) ) as $react )
{
$reactions[ $react['item_id'] ][] = $react;
}
foreach( $rows as $id => $item )
{
if ( isset( $reactions[ $item->$idField ] ) )
{
$rows[ $id ]->reputation = array();
foreach( $reactions[ $item->$idField ] as $itemRow )
{
$reputation[ $itemRow['member_id'] ] = $itemRow['reaction'];
}
$rows[ $id ]->reputation = $reputation;
}
else
{
$rows[ $id ]->reputation = array();
}
}
}
/* Pre load the contentPostedIn data to save separate DISTINCT queries later and a query for each unread item */
if ( isset( $class::$databaseColumnMap['container'] ) )
{
@@ -309,13 +455,18 @@ class _Content extends Table
catch( \OutOfRangeException $ex ) {}
}
}
/* Pull in extra data */
if ( method_exists( $class, 'tableGetRows' ) )
{
$class::tableGetRows( $rows );
}
if ( $this->callback != NULL and \is_callable( $this->callback ) )
{
$rows = \call_user_func( $this->callback, $rows );
}
/* Return */
return $rows;
}
@@ -513,7 +664,7 @@ class _Content extends Table
$params = array();
/* Permission check for the items we have specific actions for here, modActions will take care of permissions for the rest */
if( in_array( \IPS\Request::i()->modaction, array( 'hide', 'move', 'merge' ) ) )
if( \in_array( \IPS\Request::i()->modaction, array( 'hide', 'move', 'merge' ) ) )
{
$options = array();
$ids = array_keys( \IPS\Request::i()->moderate );
@@ -530,7 +681,7 @@ class _Content extends Table
}
/* The user doesn't have permission to perform the action on any of the content */
if ( !count( $options ) )
if ( !\count( $options ) )
{
throw new \OutOfRangeException;
}
@@ -543,7 +694,7 @@ class _Content extends Table
$params = $this->getMoveForm();
/* This is the form instead */
if( !is_array( $params ) )
if( !\is_array( $params ) )
{
return $params;
}
@@ -563,7 +714,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -571,7 +722,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
@@ -580,7 +731,7 @@ class _Content extends Table
/* Merge: what's the master? */
if ( \IPS\Request::i()->modaction == 'merge' )
{
if ( count( $options ) === 1 )
if ( \count( $options ) === 1 )
{
foreach ( $options as $id => $title )
{
@@ -594,7 +745,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -602,7 +753,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
}
}
@@ -613,7 +764,7 @@ class _Content extends Table
$form->class = 'ipsForm_vertical';
$form->hiddenValues['modaction'] = 'merge';
$form->hiddenValues['moderate'] = \IPS\Request::i()->moderate;
$form->add( new \IPS\Helpers\Form\Radio( 'merge_master', NULL, TRUE, array( 'options' => $options, 'descriptions' => $descriptions ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'merge_master', NULL, TRUE, array( 'options' => $options, 'descriptions' => $descriptions, 'parse' => 'normal' ) ) );
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
{
$form->add( new \IPS\Helpers\Form\Checkbox( 'move_keep_link' ) );
@@ -638,7 +789,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -646,7 +797,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
@@ -704,7 +855,7 @@ class _Content extends Table
'url' => \IPS\Request::i()->url()->setQueryString( 'modaction', 'move' ),
'permissionCheck' => function( $node ) use ( $currentContainer, $class )
{
if( $currentContainer->id != $node->id )
if( !$currentContainer or $currentContainer->id != $node->id )
{
try
{
@@ -746,7 +897,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
if ( \IPS\Request::i()->isAjax() )
{
@@ -754,7 +905,7 @@ class _Content extends Table
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
}
return;
}
+13 -8
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -51,7 +51,7 @@ class _Custom extends Table
public function __construct( $dataSource, \IPS\Http\Url $baseUrl )
{
$this->dataSource = $dataSource;
$this->count = count( $this->dataSource );
$this->count = \count( $this->dataSource );
return parent::__construct( $baseUrl );
}
@@ -74,13 +74,13 @@ class _Custom extends Table
$rows = array();
foreach ( $this->dataSource as $i => $data )
{
$row = $this->include ? array_combine( $this->include, array_fill( 0, count( $this->include ), NULL ) ) : array();
$row = $this->include ? array_combine( $this->include, array_fill( 0, \count( $this->include ), NULL ) ) : array();
/* Get the columns from the XML */
foreach ( $data as $k => $v )
{
/* Are we including this one? */
if( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) )
if( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) )
{
continue;
}
@@ -97,10 +97,13 @@ class _Custom extends Table
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
{
$quickSearchColumn = $this->quickSearch;
$rows = array_filter( $rows, is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
$rows = array_filter( $rows, \is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
{
return mb_strpos( mb_strtolower( $row[ $quickSearchColumn ] ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) !== FALSE;
} );
$this->count = \count( $rows );
$this->pages = ceil( $this->count / $this->limit );
}
/* Do we need to sort? */
@@ -124,7 +127,7 @@ class _Custom extends Table
/* Limit */
if ( $this->limit and $this->count > $this->limit )
{
$rows = array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
$rows = \array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
}
/* Apply parsers */
@@ -135,7 +138,8 @@ class _Custom extends Table
/* Parse if necessary */
if( isset( $this->parsers[ $k ] ) )
{
$v = call_user_func( $this->parsers[ $k ], $v, $this->dataSource[ $i ] );
$parserFunction = $this->parsers[ $k ];
$v = $parserFunction( $v, $this->dataSource[ $i ] );
}
else
{
@@ -148,7 +152,8 @@ class _Custom extends Table
/* Add in some buttons if necessary */
if( $this->rowButtons !== NULL )
{
$rows[ $i ]['_buttons'] = call_user_func( $this->rowButtons, $row, $i );
$rowButtons = $this->rowButtons;
$rows[ $i ]['_buttons'] = $rowButtons( $row, $i );
}
}
+69 -45
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -62,6 +62,11 @@ class _Db extends Table
*/
public $keyField = NULL;
/**
* @brief The database we will query against
*/
public $db;
/**
* Constructor
*
@@ -69,19 +74,19 @@ class _Db extends Table
* @param \IPS\Http\Url $baseUrl Base URL
* @param array|null $where WHERE clause
* @param array|null $forceIndex Index to force
* @param \IPS\Db|null $database An instance of \IPS\Db to run the queries against (defaults to current connection)
* @return void
*/
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL )
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL, $database=NULL )
{
$this->table = $table;
$this->where = $where;
$this->index = $forceIndex;
$this->db = $database ?? \IPS\Db::i();
return parent::__construct( $baseUrl );
}
protected $onlySelected = false;
/**
* Get rows
*
@@ -91,24 +96,25 @@ class _Db extends Table
public function getRows( $advancedSearchValues )
{
/* Specify filter in where clause */
$where = $this->where ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = $this->where ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Add quick search term to where clause if necessary */
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
{
if ( is_callable( $this->quickSearch ) )
if ( \is_callable( $this->quickSearch ) )
{
$where[] = call_user_func( $this->quickSearch, trim( \IPS\Request::i()->quicksearch ) );
$quickSearchFunc = $this->quickSearch;
$where[] = $quickSearchFunc( trim( \IPS\Request::i()->quicksearch ) );
}
else
{
$columns = is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = is_array( $columns ) ? $columns : array( $columns );
$columns = \is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = \is_array( $columns ) ? $columns : array( $columns );
$_where = array();
foreach ( $columns as $c )
@@ -116,7 +122,7 @@ class _Db extends Table
$_where[] = "LOWER(`{$c}`) LIKE CONCAT( '%', ?, '%' )";
}
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, \count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
}
}
@@ -125,11 +131,11 @@ class _Db extends Table
{
foreach ( $advancedSearchValues as $k => $v )
{
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' )
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' AND ( !\is_array( $v ) OR !empty( $v ) ) )
{
$type = $this->advancedSearch[ $k ];
if ( is_array( $type ) )
if ( \is_array( $type ) )
{
if ( isset( $type[2] ) )
{
@@ -146,7 +152,7 @@ class _Db extends Table
switch ( $type )
{
case SEARCH_CUSTOM:
if ( $clause = call_user_func( $lambda, $v ) )
if ( $clause = $lambda( $v ) )
{
$where[] = $clause;
}
@@ -160,17 +166,24 @@ class _Db extends Table
$where[] = array( "{$k} LIKE ?", '%' . $v . '%' );
break;
case SEARCH_QUERY_TEXT:
switch ( $v[0] )
if ( !empty( $v[1] ) )
{
case 'c':
$where[] = array( "{$k} LIKE ?", '%' . $v[1] . '%' );
break;
case 'bw':
$where[] = array( "{$k} LIKE ?", $v[1] . '%' );
break;
case 'eq':
$where[] = array( "{$k}=?", $v[1] );
break;
switch ( $v[0] )
{
case 'c':
$where[] = array( "{$k} LIKE ?", '%' . $v[1] . '%' );
break;
case 'bw':
$where[] = array( "{$k} LIKE ?", $v[1] . '%' );
break;
case 'eq':
$where[] = array( "{$k}=?", $v[1] );
break;
}
}
else
{
unset( $advancedSearchValues[ $k ] );
}
break;
case SEARCH_DATE_RANGE:
@@ -204,7 +217,7 @@ class _Db extends Table
case SEARCH_SELECT:
if ( isset( $options['multiple'] ) AND $options['multiple'] === TRUE )
{
$where[] = array( \IPS\Db::i()->in( $k, $v ) );
$where[] = array( $this->db->findInSet( $k, $v ) );
break;
}
// No break so we fall through to radio
@@ -227,7 +240,7 @@ class _Db extends Table
case SEARCH_NODE:
$nodeClass = $options['class'];
$prop = isset( $options['searchProp'] ) ? $options['searchProp'] : '_id';
if ( !is_array( $v ) )
if ( !\is_array( $v ) )
{
$v = array( $v );
}
@@ -235,7 +248,7 @@ class _Db extends Table
$values = array();
foreach ( $v as $_v )
{
if ( !is_object( $_v ) )
if ( !\is_object( $_v ) )
{
if ( mb_substr( $_v, 0, 2 ) === 's.' )
{
@@ -253,7 +266,7 @@ class _Db extends Table
}
$values[] = $_v->$prop;
}
$where[] = array( \IPS\Db::i()->in( $k, $values ) );
$where[] = array( $this->db->in( $k, $values ) );
break;
case SEARCH_NUMERIC:
@@ -283,10 +296,10 @@ class _Db extends Table
}
}
}
$selects = $this->selects;
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -298,8 +311,8 @@ class _Db extends Table
}
/* Count results (for pagination) */
$count = \IPS\Db::i()->select( 'count(*)', $this->table, $where );
if ( count( $this->joins ) )
$count = $this->db->select( 'count(*)', $this->table, $where );
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -309,9 +322,9 @@ class _Db extends Table
$count = $count->first();
/* Now get column headers */
$query = \IPS\Db::i()->select( ( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
$query = $this->db->select( ( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -363,8 +376,8 @@ class _Db extends Table
/* Run query */
$rows = array();
$select = \IPS\Db::i()->select(
( count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
$select = $this->db->select(
( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
@@ -376,7 +389,7 @@ class _Db extends Table
$select->forceIndex( $this->index );
}
if ( count( $this->joins ) )
if ( \count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -415,7 +428,8 @@ class _Db extends Table
/* Parse if necessary (NB: deliberately do this before removing the row in case we need to do some processing, but don't want the column to actually show) */
if( isset( $this->parsers[ $k ] ) )
{
$v = call_user_func( $this->parsers[ $k ], $v, $_row );
$thisParser = $this->parsers[ $k ];
$v = $thisParser( $v, $_row );
}
else
{
@@ -423,7 +437,7 @@ class _Db extends Table
}
/* Are we including this one? */
if( ( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
if( ( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
{
unset( $row[ $k ] );
continue;
@@ -436,13 +450,18 @@ class _Db extends Table
/* Add in some buttons if necessary */
if( $this->rowButtons !== NULL )
{
$row['_buttons'] = call_user_func( $this->rowButtons, $_row );
$rowButtons = $this->rowButtons;
$row['_buttons'] = $rowButtons( $_row );
}
/* Highlighting? */
if ( isset( $this->parsers['_highlight'] ) and $class = call_user_func( $this->parsers['_highlight'], $_row ) )
if ( isset( $this->parsers['_highlight'] ) )
{
$this->highlightRows[ $rowId ] = $class;
$class = $this->parsers['_highlight'];
if( $class = $class( $_row ) )
{
$this->highlightRows[ $rowId ] = $class;
}
}
$rows[ $rowId ] = $row;
@@ -455,6 +474,11 @@ class _Db extends Table
$sortDirection = $this->sortDirection;
uasort( $rows, function( $a, $b ) use ( $sortBy, $sortDirection )
{
if( !isset( $a[ $sortBy ] ) )
{
return 0;
}
if( $sortDirection === 'asc' )
{
return strnatcasecmp( mb_strtolower( $a[ $sortBy ] ), mb_strtolower( $b[ $sortBy ] ) );
@@ -472,7 +496,7 @@ class _Db extends Table
/**
* User set sortBy is suitable for an SQL sort operation
* @param array $count Result of COUNT(*) query with field names included
* @param array $count Result of count(*) query with field names included
* @return boolean
*/
protected function _isSqlSort( $count )
@@ -482,7 +506,7 @@ class _Db extends Table
return false;
}
if( !is_array( $count ) )
if( !\is_array( $count ) )
{
$count = array( $count );
}
+53 -29
View File
@@ -11,7 +11,7 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -29,6 +29,8 @@ const HEADER = 8;
const SEARCH_RADIO = 9;
const SEARCH_NUMERIC_TEXT = 10;
const SEARCH_QUERY_TEXT = 11;
const SEARCH_CHECKBOX = 12;
/**
* List Table Builder
*/
@@ -198,7 +200,7 @@ abstract class _Table
{
return array( ... ); // Same format as IPS\Helpers\Table::$rootButtons
}
* @code
* @endcode
*/
public $rowButtons = NULL;
@@ -255,7 +257,8 @@ abstract class _Table
if ( \IPS\Request::i()->$parameter )
{
$this->page = intval( \IPS\Request::i()->$parameter );
$this->page = \intval( \IPS\Request::i()->$parameter );
if ( !$this->page OR $this->page < 1 )
{
$this->page = 1;
@@ -279,7 +282,7 @@ abstract class _Table
}
/* Set base URL */
$this->baseUrl = $baseUrl->setQueryString( array( 'filter' => $this->filter, 'sortby' => $this->sortBy, 'sortdirection' => $this->sortDirection, $this->paginationKey => $this->page ) );
$this->baseUrl = $baseUrl->setQueryString( array( 'filter' => $this->filter, 'sortby' => $this->sortBy, 'sortdirection' => $this->sortDirection ) )->setPage( $this->paginationKey, $this->page );
/* Templates */
$this->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'table' );
@@ -315,7 +318,7 @@ abstract class _Table
if ( \IPS\Request::i()->$value )
{
$this->page = intval( \IPS\Request::i()->$value );
$this->page = \intval( \IPS\Request::i()->$value );
if ( !$this->page OR $this->page < 1 )
{
$this->page = 1;
@@ -331,9 +334,7 @@ abstract class _Table
* For example, we might allow array( 'last_post', 'author' ) but the $this->sortBy property is often set as "app_table.last_post" meaning
* that the filter drop down doesn't show the selected value correctly.
*
* @param string $property Property we are updating
* @param mixed $value Value being set
* @return void
* @return string
*/
public function getSortByColumn()
{
@@ -363,7 +364,7 @@ abstract class _Table
foreach ( $this->advancedSearch as $k => $type )
{
$options = array();
if ( is_array( $type ) )
if ( \is_array( $type ) )
{
$options = $type[1];
$type = $type[0];
@@ -383,13 +384,13 @@ abstract class _Table
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", $element->value[0], $element->required, array(
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'c' => \IPS\Member::loggedIn()->language()->addToStack('contains'),
'bw' => \IPS\Member::loggedIn()->language()->addToStack('begins_with'),
'eq' => \IPS\Member::loggedIn()->language()->addToStack('exactly'),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', $element->value[1], $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
@@ -419,7 +420,7 @@ abstract class _Table
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", $element->value[0], $element->required, array(
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'any' => \IPS\Member::loggedIn()->language()->addToStack('any'),
'gt' => \IPS\Member::loggedIn()->language()->addToStack('gt'),
'lt' => \IPS\Member::loggedIn()->language()->addToStack('lt'),
@@ -435,11 +436,14 @@ abstract class _Table
'eq' => array( $element->name . '-qty' ),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", $element->value[1], $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
case SEARCH_CHECKBOX:
$form->add( new \IPS\Helpers\Form\Checkbox( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_BOOL:
$form->add( new \IPS\Helpers\Form\YesNo( $this->langPrefix . $k, TRUE, FALSE, $options ) );
break;
@@ -448,7 +452,7 @@ abstract class _Table
break;
}
}
return $form;
}
@@ -482,36 +486,46 @@ abstract class _Table
}
/* Get rows */
$rows = $this->getRows( $advancedSearchValues );
$rows = $this->getRows( $advancedSearchValues ) ?? array();
/* Check we're on a valid page (must come after getRows() as this is where $this->pages is set) */
if ( $this->page )
{
if ( $this->pages and $this->page > $this->pages )
{
\IPS\Output::i()->redirect( $this->baseUrl->setQueryString( $this->paginationKey, 1 ), NULL, 303 );
\IPS\Output::i()->redirect( $this->baseUrl->setPage( $this->paginationKey, 1 ), NULL, 303 );
}
}
/* Add link tags */
if ( $this->page != 1 )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page );
\IPS\Output::i()->linkTags['first'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, 1 );
\IPS\Output::i()->linkTags['prev'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page - 1 );
if( !isset( \IPS\Output::i()->linkTags['canonical'] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page );
}
\IPS\Output::i()->linkTags['first'] = (string) $this->baseUrl->setPage( $this->paginationKey, 1 );
\IPS\Output::i()->linkTags['prev'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page - 1 );
}
/* If we literally requested ?page=1 add canonical tag to get rid of the page query string param */
elseif( isset( $this->baseUrl->data[ \IPS\Http\Url::COMPONENT_QUERY ][ $this->paginationKey ] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->stripQueryString( $this->paginationKey );
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage();
}
if ( $this->pages > $this->page )
{
\IPS\Output::i()->linkTags['next'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->page + 1 );
\IPS\Output::i()->linkTags['next'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page + 1 );
}
if ( $this->pages != $this->page )
{
\IPS\Output::i()->linkTags['last'] = (string) $this->baseUrl->setQueryString( $this->paginationKey, $this->pages );
\IPS\Output::i()->linkTags['last'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->pages );
}
/* No rows to show? Add a noindex but follow for crawling later to check if content exists */
if ( ! \count( $rows ) )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex, follow';
}
/* Get table headers */
@@ -521,9 +535,10 @@ abstract class _Table
$resortKey = $this->resortKey;
if( \IPS\Request::i()->isAjax() and \IPS\Request::i()->$resortKey )
{
if ( count( $rows ) )
if ( \count( $rows ) )
{
$rowHtml = call_user_func( $this->rowsTemplate, $this, $headers, $rows, $this->mainColumn, $this->rootButtons, array() );
$rowsTemplate = $this->rowsTemplate;
$rowHtml = $rowsTemplate( $this, $headers, $rows, $this->mainColumn, $this->rootButtons, array() );
}
else
{
@@ -545,7 +560,8 @@ abstract class _Table
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
/* Build table */
return call_user_func( $this->tableTemplate, $this, $headers, $rows, ( is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
$tableTemplate = $this->tableTemplate;
return $tableTemplate( $this, $headers, $rows, ( \is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
}
}
catch ( \Exception $e )
@@ -569,7 +585,7 @@ abstract class _Table
$return = array();
foreach( $values as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
$return[ $k ] = $this->_convertSearchValuesForQueryString( $v );
}
@@ -690,6 +706,15 @@ abstract class _Table
$advancedSearchValues = array();
$advancedSearchValuesQuery = array();
/* Store these to prevent URL from being rebuilt on subsequent calls */
$storeSearchValues = FALSE;
if ( $this->advancedSearchValues === NULL )
{
$storeSearchValues = TRUE;
$this->advancedSearchValues = array();
}
if ( !empty( $this->advancedSearch ) )
{
/* Are we displaying the advanced search form? */
@@ -703,9 +728,8 @@ abstract class _Table
$form = $this->advancedSearch();
/* Store these to prevent URL from being rebuilt on subsequent calls */
if ( $this->advancedSearchValues === NULL )
if ( $storeSearchValues === TRUE )
{
$this->advancedSearchValues = array();
foreach ( $values as $k => $v )
{
if( array_key_exists( $k, $form->hiddenValues ) )