Version 4.2.0

This commit is contained in:
Neo committed 2025-12-19 05:44:59 -08:00
1 parent 0dc2aee5ed
commit 96997ddd8e
2074 files changed
+125454 -55780

No files matched your search

+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Base API endpoint for Content Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Dec 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Api;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Base API endpoint for Content Items
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Dec 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Api;
+279
View File
@@ -0,0 +1,279 @@
<?php
/**
* @brief Trait for Content Containers which can be used in Clubs
* @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 17 Feb 2017
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Trait for Content Containers which can be used in Clubs
*/
trait ClubContainer
{
/**
* Get the database column which stores the club ID
*
* @return string
*/
public static function clubIdColumn()
{
return 'club_id';
}
/**
* Get front-end language string
*
* @return string
*/
public static function clubFrontTitle()
{
$itemClass = static::$contentItemClass;
return $itemClass::$title . '_pl';
}
/**
* Get acp language string
*
* @return string
*/
public static function clubAcpTitle()
{
return static::$nodeTitle;
}
/**
* Get the associated club
*
* @return \IPS\Member\Club|NULL
*/
public function club()
{
if ( \IPS\Settings::i()->clubs )
{
$clubIdColumn = $this->clubIdColumn();
if ( $this->$clubIdColumn )
{
try
{
return \IPS\Member\Club::load( $this->$clubIdColumn );
}
catch ( \OutOfRangeException $e ) { }
}
}
return NULL;
}
/**
* Set form for creating a node of this type in a club
*
* @param \IPS\Helpers\Form $form Form object
* @return void
*/
public function clubForm( \IPS\Helpers\Form $form )
{
$itemClass = static::$contentItemClass;
$form->add( new \IPS\Helpers\Form\Text( 'club_node_name', $this->_id ? $this->_title : \IPS\Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
}
/**
* Save club form
*
* @param \IPS\Member\Club $club The club
* @param array $values Values
* @return void
*/
public function saveClubForm( \IPS\Member\Club $club, $values )
{
$nodeClass = get_called_class();
$itemClass = $nodeClass::$contentItemClass;
$this->_saveClubForm( $club, $values );
$needToUpdatePermissions = FALSE;
if ( !$this->_id )
{
$clubIdColumn = $this->clubIdColumn();
$this->$clubIdColumn = $club->id;
$this->save();
\IPS\Db::i()->insert( 'core_clubs_node_map', array(
'club_id' => $club->id,
'node_class' => $nodeClass,
'node_id' => $this->_id,
'name' => $values['club_node_name']
) );
$needToUpdatePermissions = TRUE;
}
else
{
$this->save();
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'] ), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
}
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id, $values['club_node_name'] );
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id . '_desc', isset( $values['club_node_description'] ) ? $values['club_node_description'] : '' );
if ( $needToUpdatePermissions )
{
$this->setPermissionsToClub( $club );
}
}
/**
* Class-specific routine when saving club form
*
* @param \IPS\Member\Club $club The club
* @param array $values Values
* @return void
*/
public function _saveClubForm( \IPS\Member\Club $club, $values )
{
// Deliberately does nothing so classes can override
}
/**
* Set the permission index permissions to a specific club
*
* @param \IPS\Member\Club $club The club
* @return void
*/
public function setPermissionsToClub( \IPS\Member\Club $club )
{
/* Delete current rows */
\IPS\Db::i()->delete( 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id=?', static::$permApp, static::$permType, $this->_id ) );
/* Build new rows */
$insert = array( 'app' => static::$permApp, 'perm_type' => static::$permType, 'perm_type_id' => $this->_id );
foreach ( static::$permissionMap as $k => $v )
{
if ( in_array( $k, array( 'view', 'read' ) ) )
{
switch ( $club->type )
{
case $club::TYPE_PUBLIC:
case $club::TYPE_OPEN:
$insert[ 'perm_' . $v ] = '*';
break;
case $club::TYPE_CLOSED:
case $club::TYPE_PRIVATE:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
break;
}
}
else
{
switch ( $club->type )
{
case $club::TYPE_PUBLIC:
$insert[ 'perm_' . $v ] = 'ca';
break;
case $club::TYPE_OPEN:
case $club::TYPE_CLOSED:
case $club::TYPE_PRIVATE:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
break;
}
}
}
/* Insert */
$permId = \IPS\Db::i()->insert( 'core_permission_index', $insert );
/* Update tags permission cache */
if ( isset( static::$permissionMap['read'] ) )
{
\IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_text' => $insert[ 'perm_' . static::$permissionMap['read'] ] ), array( 'tag_perm_aap_lookup=?', md5( static::$permApp . ';' . static::$permType . ';' . $this->_id ) ) );
}
/* Make sure this object resets the permissions internally */
$this->_permissions = array_merge( array( 'perm_id' => $permId ), $insert );
/* Update search index */
$this->updateSearchIndexPermissions();
}
/**
* Fetch All Root Nodes
*
* @param string|NULL $permissionCheck The permission key to check for or NULL to not check permissions
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
* @param mixed $where Additional WHERE clause
* @return array
*/
public static function rootsWithClubs( $permissionCheck='view', $member=NULL, $where=array() )
{
/* Will we need to check permissions? */
if ( $permissionCheck !== NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
}
if( static::$databaseColumnParent !== NULL )
{
$where[] = array( static::$databasePrefix . static::$databaseColumnParent . '=?', static::$databaseColumnParentRootValue );
}
return static::nodesWithPermission( $permissionCheck, $member, $where );
}
/**
* Check Moderator Permission
*
* @param string $type 'edit', 'hide', 'unhide', 'delete', etc.
* @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member
* @return bool
*/
public function modPermission( $type, \IPS\Member $member )
{
if ( \IPS\Settings::i()->clubs )
{
$clubIdColumn = $this->clubIdColumn();
if ( $this->$clubIdColumn and $club = $this->club() )
{
if ( in_array( $club->memberStatus( $member ), array( \IPS\Member\Club::STATUS_LEADER, \IPS\Member\Club::STATUS_MODERATOR ) ) )
{
if ( in_array( $type, explode( ',', \IPS\Settings::i()->clubs_modperms ) ) )
{
return TRUE;
}
}
}
}
return parent::modPermission( $type, $member );
}
/**
* [Node] Get parent list
*
* @return \SplStack
*/
public function parents()
{
$clubIdColumn = static::clubIdColumn();
/* Only do this if this node is actually associated with a club */
if ( $this->$clubIdColumn )
{
return new \SplStack;
}
else
{
return parent::parents();
}
}
}
+212 -45
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Content Comment Model
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
@@ -168,37 +167,13 @@ abstract class _Comment extends \IPS\Content
}
}
}
/* Check if profanity filters should mod-queue this comment */
$obj->checkProfanityFilters( $first );
/* Save the comment */
$obj->save();
/* Pass this through our profanity filters to see if it needs to be mod queued */
try
{
$hiddenByFilter = FALSE;
if ( !$obj->hidden() AND ( ! $item::$firstCommentRequired or ( $item::$firstCommentRequired and !$obj->isFirst() ) ) AND !$member->group['g_bypass_badwords'] )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $comment );
}
}
catch( \BadMethodCallException $e ) { }
if ( $hiddenByFilter === TRUE )
{
if ( isset( static::$databaseColumnMap['approved'] ) )
{
/* 'approved' is easy, clear and concise */
$column = static::$databaseColumnMap['approved'];
$obj->$column = 0;
}
else if ( isset( static::$databaseColumnMap['hidden'] ) )
{
/* 'hidden' is backwards */
$column = static::$databaseColumnMap['hidden'];
$obj->$column = 1;
}
$obj->save();
}
/* Increment post count */
try
{
@@ -271,6 +246,81 @@ abstract class _Comment extends \IPS\Content
/* Return */
return $obj;
}
/**
* Check comment against profanity filters
*
* @note We do not save here, as saving is done in user-land code after
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @return bool Whether to send unapproved notifications
*/
protected function checkProfanityFilters( $first=FALSE, $edit=TRUE )
{
/* We need our item */
$item = $this->item();
/* And the author */
$member = $this->author();
/* Pass this through our profanity filters to see if it needs to be mod queued */
$sendNotifications = FALSE;
try
{
$hiddenByFilter = FALSE;
if ( !$this->hidden() AND ( ! $item::$firstCommentRequired or ( $item::$firstCommentRequired and !$first ) ) AND !$member->group['g_bypass_badwords'] )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $this->content() );
}
/* If the comment is hidden (probably requires moderator approval) and this is the first post in the item that requires a comment (i.e. a post in a topic), then we need to set the item as requiring approval instead. */
elseif ( $item instanceof \IPS\Content\Hideable AND !$member->group['g_bypass_badwords'] AND ( $item::$firstCommentRequired and $first ) )
{
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $this->content() );
if ( isset( $item::$databaseColumnMap['approved'] ) and $itemHiddenByFilter )
{
/* 'approved' is easy, clear and concise */
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
}
elseif ( isset( $item::$databaseColumnMap['hidden'] ) and $itemHiddenByFilter )
{
/* 'hidden' is backwards */
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
}
if( $edit === TRUE )
{
$sendNotifications = TRUE;
}
}
}
catch( \BadMethodCallException $e ) { }
if ( $hiddenByFilter === TRUE )
{
if ( isset( static::$databaseColumnMap['approved'] ) )
{
/* 'approved' is easy, clear and concise */
$column = static::$databaseColumnMap['approved'];
$this->$column = 0;
}
else if ( isset( static::$databaseColumnMap['hidden'] ) )
{
/* 'hidden' is backwards */
$column = static::$databaseColumnMap['hidden'];
$this->$column = 1;
}
$sendNotifications = TRUE;
}
return $sendNotifications;
}
/**
* Join profile fields when loading comments?
@@ -378,6 +428,9 @@ abstract class _Comment extends \IPS\Content
$item->container()->save();
}
}
/* Are we tracking keywords? */
$this->checkKeywords( $this->content() );
}
/**
@@ -482,7 +535,7 @@ abstract class _Comment extends \IPS\Content
}
/* Can the member edit their own content? */
if ( $member->member_id == $this->author()->member_id and $member->group['g_edit_posts'] and ( !( $item instanceof \IPS\Content\Lockable ) or !$item->locked() ) )
if ( $member->member_id == $this->author()->member_id and ( $member->group['g_edit_posts'] == '1' or in_array( get_class( $item ), explode( ',', $member->group['g_edit_posts'] ) ) ) and ( !( $item instanceof \IPS\Content\Lockable ) or !$item->locked() ) )
{
if ( !$member->group['g_edit_cutoff'] )
{
@@ -518,7 +571,7 @@ abstract class _Comment extends \IPS\Content
}
catch ( \BadMethodCallException $e ) { }
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and $member->group['gbw_soft_delete_own'] ) ) );
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and ( $member->group['g_hide_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_hide_own_posts'] ) ) ) ) ) );
}
/**
@@ -565,7 +618,7 @@ abstract class _Comment extends \IPS\Content
}
catch ( \BadMethodCallException $e ) { }
return ( !$this->isFirst() and ( static::modPermission( 'delete', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and $member->group['g_delete_own_posts'] ) ) );
return ( !$this->isFirst() and ( static::modPermission( 'delete', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and ( $member->group['g_delete_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_delete_own_posts'] ) ) ) ) ) );
}
/**
@@ -596,6 +649,28 @@ abstract class _Comment extends \IPS\Content
return FALSE;
}
/**
* Can report?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canReport( $member=NULL )
{
if ( !( $this instanceof \IPS\Content\ReportCenter ) )
{
return FALSE;
}
if ( $this->hidden() === 1 )
{
return FALSE;
}
$member = $member ?: \IPS\Member::loggedIn();
return ( $member->group['g_can_report'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_can_report'] ) ) );
}
/**
* Search Index Permissions
*
@@ -624,6 +699,11 @@ abstract class _Comment extends \IPS\Content
*/
public function isIgnored( $member=NULL )
{
if ( !\IPS\Settings::i()->ignore_system_on )
{
return FALSE;
}
if ( $member === NULL )
{
$member = \IPS\Member::loggedIn();
@@ -661,16 +741,46 @@ abstract class _Comment extends \IPS\Content
$valueField = static::$databaseColumnMap['content'];
$oldValue = $this->$valueField;
$this->$valueField = $newContent;
$isHiddenNow = $this->hidden();
/* Check if profanity filters should mod-queue this comment */
$sendNotifications = $this->checkProfanityFilters( $this->isFirst(), TRUE );
$this->save();
/* Send any new mention/quote notifications */
$this->sendAfterEditNotifications( $oldValue );
/* Resync the item - necessary as profanity filters may have just hidden an otherwise approved post */
$item = $this->item();
if( $isHiddenNow != $this->hidden() )
{
$item->resyncCommentCounts();
$item->resyncLastComment();
$item->save();
}
if ( $container = $item->containerWrapper() )
{
$container->resetCommentCounts();
$container->save();
}
/* Reindex */
if ( $this instanceof \IPS\Content\Searchable )
{
\IPS\Content\Search\Index::i()->index( $this );
}
/* Send notifications */
if ( $sendNotifications AND !in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
{
if( $this->hidden() === 1 )
{
$this->sendUnapprovedNotification();
}
}
}
/**
@@ -852,10 +962,11 @@ abstract class _Comment extends \IPS\Content
/**
* Move Comment to another item
*
* @param \IPS\Content\Item $item The item to move this comment too.
* @param \IPS\Content\Item $item The item to move this comment to
* @param bool $skip Skip rebuilding new/old content item data (used for multimod where we can do it in one go after)
* @return void
*/
public function move( \IPS\Content\Item $item )
public function move( \IPS\Content\Item $item, $skip=FALSE )
{
$oldItem = $this->item();
@@ -872,13 +983,16 @@ abstract class _Comment extends \IPS\Content
\IPS\Db::i()->update( 'core_notifications', array( 'item_id' => $item->$idColumn ), array( 'item_class=? and item_id=? and item_sub_class=? and item_sub_id=?', (string) get_class( $item ), $oldItem->$idColumn, (string) get_called_class(), $this->$commentIdColumn ) );
/* Update reputation */
if ( $this instanceof \IPS\Content\Reputation )
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
{
\IPS\Db::i()->update( 'core_reputation_index', array( 'item_id' => $item->$idColumn ), array( 'class_type_id_hash=?', md5( get_class( $this ) . ':' . $oldItem->$idColumn ) ) );
}
$oldItem->rebuildFirstAndLastCommentData();
$item->rebuildFirstAndLastCommentData();
if( $skip === FALSE )
{
$oldItem->rebuildFirstAndLastCommentData();
$item->rebuildFirstAndLastCommentData();
}
/* Add to search index */
if ( $this instanceof \IPS\Content\Searchable )
@@ -937,13 +1051,19 @@ abstract class _Comment extends \IPS\Content
$item = NULL;
$itemId = $this->mapped('item');
}
/* Remove featured comment associations */
if( $this->isFeatured() AND $item )
{
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $item, $this );
}
/* Unclaim attachments */
\IPS\File::unclaimAttachments( $itemClass::$application . '_' . ucfirst( $itemClass::$module ), $itemId, $this->$idColumn );
/* Reduce the number of comment/reviews count on the item but only if the item is unapproved or visible
* - hidden as opposed to unapproved items do not get included in either of the unapproved_comments/num_comments columns */
if( $this->hidden() !== -1 )
if( $this->hidden() !== -1 AND $this->hidden() !== -2 )
{
$columnName = ( $this->hidden() === 1 ) ? 'unapproved_comments' : 'num_comments';
if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
@@ -1005,7 +1125,7 @@ abstract class _Comment extends \IPS\Content
{
\IPS\Member::load( $member )->recountNotifications();
}
/* Actually delete */
parent::delete();
@@ -1031,7 +1151,7 @@ abstract class _Comment extends \IPS\Content
{
if ( $item->container()->_comments !== NULL )
{
if ( !$this->hidden() )
if ( !$this->hidden() AND $this->hidden() !== -2 )
{
$item->container()->_comments = ( $item->container()->_comments > 0 ) ? ( $item->container()->_comments - 1 ) : 0;
}
@@ -1257,6 +1377,18 @@ abstract class _Comment extends \IPS\Content
}
}
/* No matter if we can or cannot view hidden items, we do not want these to show as they are queued for deletion */
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'];
$where[] = array( "{$col}!=-2" );
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
$where[] = array( "{$col}!=-2" );
}
if ( $joinContainer AND isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
@@ -1368,6 +1500,30 @@ abstract class _Comment extends \IPS\Content
return array( $this->item()->$idColumn, $this->$commentIdColumn );
}
/**
* Returns the content images
*
* @param int|null $limit Number of attachments to fetch, or NULL for all
*
* @return array|NULL
* @throws \BadMethodCallException
*/
public function contentImages( $limit = NULL )
{
$idColumn = static::$databaseColumnId;
$item = $this->item();
$attachments = array();
$itemIdColumn = $item::$databaseColumnId;
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=? and id2=?', $item::$application . '_' . ucfirst( $item::$module ), $item->$itemIdColumn, $this->$idColumn ) );
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
{
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
}
return count( $attachments ) ? $attachments : NULL;
}
/**
* @brief Existing warning
*/
@@ -1414,6 +1570,17 @@ abstract class _Comment extends \IPS\Content
return NULL;
}
/**
* Is a featured comment?
*
* @return bool
* @note This is a wrapper for the extension so content items can extend and apply their own logic
*/
public function isFeatured()
{
return \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->isFeatured( $this );
}
/**
* Get output for API
*
+406 -432
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Abstract Content Model
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 3 Oct 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS;
@@ -48,6 +47,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public static $includeInSitemap = TRUE;
/**
* @brief Reputation Store
*/
protected $reputation;
/**
* Should posting this increment the poster's post count?
*
@@ -244,6 +248,35 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return $this->mapped('content');
}
/**
* Returns the content images
*
* @param int|null $limit Number of attachments to fetch, or NULL for all
*
* @return array|NULL
* @throws \BadMethodCallException
*/
public function contentImages( $limit = NULL )
{
$idColumn = static::$databaseColumnId;
$internal = NULL;
$attachments = array();
if ( isset( static::$databaseColumnMap['content'] ) )
{
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . ucfirst( static::$module ), $this->$idColumn ) );
}
if ( $internal )
{
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
{
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
}
}
return count( $attachments ) ? $attachments : NULL; }
/**
* Text for use with data-ipsTruncate
* Returns the post with paragraphs turned into line breaks
@@ -280,12 +313,17 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public function delete()
{
if ( $this instanceof \IPS\Content\Reputation )
$idColumn = static::$databaseColumnId;
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
{
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, static::$reputationType, $this->$idColumn ) );
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) );
}
/* Remove any entries in the promotions table */
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', get_class( $this ), $this->$idColumn ) );
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
parent::delete();
$this->expireWidgetCaches();
@@ -329,14 +367,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* Content is hidden?
*
* @return int
* @li -2 is pending deletion
* @li -1 is hidden having been hidden by a moderator
* @li 0 is unhidden
* @li 1 is hidden needing approval
* @note The actual column may also contain 2 which means the item is hidden because the parent is hidden, but it is not hidden in itself. This method will return -1 in that case.
*
* @note A piece of content (item and comment) can have an alias for hidden OR approved.
* With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden
* With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator
* With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden, -2=pending deletion
* With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator, -2=pending deletion
*
* User posting has moderator approval set: When adding an unapproved ITEM (approved=0, hidden=1) you should *not* increment container()->_comments but you should update container()->_unapprovedItems
* User posting has moderator approval set: When adding an unapproved COMMENT (approved=0, hidden=1) you should *not* increment item()->num_comments in item or container()->_comments but you should update item()->unapproved_comments and container()->_unapprovedComments
@@ -361,6 +400,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
if ( $this->$column == -2 )
{
return intval( $this->$column );
}
return $this->$column == -1 ? intval( $this->$column ) : intval( !$this->$column );
}
else
@@ -430,27 +473,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/* Per-container permission */
elseif ( $container )
{
$containerClass = get_class( $container );
$title = static::$title;
if
(
isset( $containerClass::$modPerm )
and
(
$member->modPermission( $containerClass::$modPerm ) === -1
or
(
is_array( $member->modPermission( $containerClass::$modPerm ) )
and
in_array( $container->_id, $member->modPermission( $containerClass::$modPerm ) )
)
)
and
$member->modPermission( "can_{$type}_{$title}" )
)
{
return TRUE;
}
return $container->modPermission( $type, $member );
}
/* Still here? return false */
@@ -463,10 +486,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param string $action The action
* @param \IPS\Member|NULL $member The member doing the action (NULL for currently logged in member)
* @param string|NULL $reason Reason (for hides)
* @param bool $immediately Delete Immediately
* @return void
* @throws \OutOfRangeException|\InvalidArgumentException|\RuntimeException
*/
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL )
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL, $immediately = FALSE )
{
if( $action === 'approve' )
{
@@ -474,13 +498,19 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Check it's a valid action */
if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish' ) ) )
if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish', 'restore', 'restoreAsHidden' ) ) )
{
throw new \InvalidArgumentException;
}
/* And that we can do it */
if ( !call_user_func( array( $this, 'can' . ucfirst( $action ) ), $member ) )
$toCheck = $action;
if ( $action == 'restoreAsHidden' )
{
$toCheck = 'restore';
}
if ( !call_user_func( array( $this, 'can' . ucfirst( $toCheck ) ), $member ) )
{
throw new \OutOfRangeException;
}
@@ -541,9 +571,32 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/* Delete is just a method */
elseif ( $action === 'delete' )
{
/* If we are retaining content for a period of time, we need to just hide it instead for deleting later - this only works, though, with items that implement \IPS\Content\Hideable */
if ( \IPS\Settings::i()->dellog_retention_period AND ( $this instanceof \IPS\Content\Hideable ) AND $immediately === FALSE )
{
$this->logDelete();
return;
}
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
$this->delete();
return;
}
/* Restore is just a method */
elseif ( $action === 'restore' )
{
$this->restore();
return;
}
/* Restore As Hidden is just a method */
elseif ( $action === 'restoreAsHidden' )
{
$this->restore( TRUE );
return;
}
/* Publish is just a method */
elseif ( $action === 'publish' )
@@ -561,6 +614,200 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
/**
* Log for deletion later
*
* \IPS\Member|NULL $member The member or NULL for currently logged in
* @return void
*/
public function logDelete( $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
/* Log it! */
$log = new \IPS\core\DeletionLog;
$log->setContentAndMember( $this, $member );
$log->save();
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
}
try
{
if ( $this->container() )
{
if ( $this->container()->_comments !== NULL )
{
/* Hidden content already decrements */
if ( !$this->hidden() )
{
$this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') );
}
}
if ( $this->container()->_unapprovedComments !== NULL )
{
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments - $this->mapped( 'unapproved_comments' ) );
}
}
}
catch( \BadMethodCallException $e ) {}
$this->$column = -2;
$this->save();
if ( $this instanceof \IPS\Content\Comment )
{
$item = $this->item();
/* Update last comment stuff */
$item->resyncLastComment();
/* Update last review stuff */
$item->resyncLastReview();
/* Update number of comments */
$item->resyncCommentCounts();
/* Update number of reviews */
$item->resyncReviewCounts();
/* Save*/
$item->save();
}
try
{
if ( $this->container() )
{
$this->container()->setLastComment();
$this->container()->save();
}
}
catch( \BadMethodCallException $e ) {}
}
/**
* Restore Content
*
* @param bool Restore as hidden?
* @return void
* @throws \BadMethodCallException
*/
public function restore( $hidden = FALSE )
{
try
{
$idColumn = static::$databaseColumnId;
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id", get_class( $this ), $this->$idColumn ) )->first() );
}
catch( \UnderflowException $e )
{
throw new \BadMethodCallException;
}
/* Restoring as hidden? */
if ( $hidden )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
}
$this->$column = -1;
}
else
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
$this->$column = 0;
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = 1;
}
}
/* Save the changes */
$this->save();
/* Reindex the now hidden content */
if ( $this instanceof \IPS\Content\Item and static::$firstCommentRequired )
{
\IPS\Content\Search\Index::i()->index( $this->firstComment() );
}
else
{
\IPS\Content\Search\Index::i()->index( $this );
}
/* Delete the log */
$log->delete();
/* Log to moderate logs */
if ( !$hidden )
{
try
{
if ( $this->container() )
{
if ( $this->container()->_comments !== NULL )
{
if ( $this instanceof \IPS\Content\Item )
{
$this->container()->_comments = ( $this->container()->_comments + $this->mapped('num_comments') );
}
else
{
$this->container()->_comments = $this->container()->_comments + 1;
}
}
if ( $this->container()->_unapprovedComments !== NULL )
{
if ( $this instanceof \IPS\Content\Item )
{
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments + $this->mapped('unapproved_comments') );
}
}
$this->container()->setLastComment();
$this->container()->save();
if ( $this instanceof \IPS\Content\Comment )
{
$this->item()->rebuildFirstAndLastCommentData();
}
}
}
catch( \BadMethodCallException $e ) {}
}
}
/**
* Can restore?*
*
* @param \IPS\Member|NULL The member, or currently logged in member
* @return bool
*/
public function canRestore( $member=NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
return \IPS\Member::loggedIn()->modPermission('can_restore_deleted_content');
}
/**
* Hide
*
@@ -722,7 +969,43 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return \IPS\Member::loggedIn()->language()->addToStack('hidden');
}
}
/**
* Blurb for when/why/by whom this content was deleted
*
* @return string
* @throws \BadMethodCallException
*/
public function deletedBlurb()
{
if ( !( $this instanceof \IPS\Content\Hideable ) )
{
throw new \BadMethodCallException;
}
try
{
$idColumn = static::$databaseColumnId;
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() );
return \IPS\Member::loggedIn()->language()->addToStack( 'deletion_blurb', FALSE, array( 'sprintf' => array( $log->_deleted_by->name, $log->deleted_date->fullYearLocaleDate(), $log->deletion_date->fullYearLocaleDate() ) ) );
}
catch( \UnderflowException $e )
{
return \IPS\Member::loggedIn()->language()->addToStack('deleted');
}
}
/**
* Can promote this comment/item?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return boolean
*/
public function canPromoteToSocialMedia( $member=NULL )
{
return \IPS\core\Promote::canPromote( $member );
}
/**
* Can report?
*
@@ -741,7 +1024,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Can the member report content? */
if ( $member->group['gbw_no_report'] )
if ( !$member->group['g_can_report'] )
{
return 'no_module_permission';
}
@@ -866,7 +1149,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$oldAuthor = $this->author();
foreach ( array( 'author', 'author_name' ) as $k )
foreach ( array( 'author', 'author_name', 'edit_member_name' ) as $k )
{
if ( isset( static::$databaseColumnMap[ $k ] ) )
{
@@ -878,6 +1161,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
break;
case 'author_name':
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not */
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
break;
@@ -906,9 +1190,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param bool $asItem Displaying results as items?
* @param bool $canIgnoreComments Can ignore comments in the result stream? Activity stream can, but search results cannot.
* @param array $template Optional custom template
* @param array $reactions Reaction Data
* @return string
*/
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL )
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() )
{
/* Item details */
$itemClass = $indexData['index_class'];
@@ -931,12 +1216,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
{
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findReview', 'review' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showRepReview', 'review' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsReview', 'review' => $indexData['index_object_id'] ) );
}
else
{
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showRepComment', 'comment' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
}
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
@@ -945,7 +1230,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
else
{
$objectUrl = $itemUrl;
$showRepUrl = $itemUrl->setQueryString( 'do', 'showRep' );
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
}
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle );
@@ -971,7 +1256,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Return */
return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments ) );
return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments, $reactions ) );
}
/**
@@ -991,6 +1276,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
}
/**
* @brief Return a classname applied to the search result block
*/
public static $searchResultClassName = '';
/**
* Return the filters that are available for selecting table rows
*
@@ -1039,397 +1329,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* !Reputation */
/**
* Can give reputation?
*
* @note This method is also ran to check if a member can "unrep"
* @param int $type 1 for positive, -1 for negative
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canGiveReputation( $type, \IPS\Member $member = NULL )
{
return static::_canGiveReputation( $type, $this->author()->member_id, $this->author()->member_group_id, $this->repGiven( $member ), $member );
}
/**
* Can give reputation?
*
* @note This method is also ran to check if a member can "unrep"
* @param int $type 1 for positive, -1 for negative
* @param int $authorId The author ID
* @param int $authorGroup The author group
* @param int $repGiven The reputation we have already given (1 = positive, -1 = negative, 0 = no rep given)
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public static function _canGiveReputation( $type, $authorId, $authorGroup, $repGiven, \IPS\Member $member = NULL )
{
/* Reputation needs to be enabled */
if ( !\IPS\Settings::i()->reputation_enabled or !in_array( 'IPS\Content\Reputation', class_implements( get_called_class() ) ) )
{
return FALSE;
}
/* Guests cannot rep */
$member = $member ?: \IPS\Member::loggedIn();
if ( !$member->member_id )
{
return FALSE;
}
/* Author needs to not be protected */
if ( in_array( $authorGroup, explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
{
return FALSE;
}
/* Users may not be able to rep themselves */
if ( !\IPS\Settings::i()->reputation_can_self_vote and $authorId == $member->member_id )
{
return FALSE;
}
/* Rep per day */
$limit = ( $type === 1 ) ? $member->group['g_rep_max_positive'] : $member->group['g_rep_max_negative'];
if ( $limit == 0 and !$repGiven )
{
return FALSE;
}
/* Have we already given rep? */
switch ( \IPS\Settings::i()->reputation_point_types )
{
/* If we're using a positive system, we can rep positive only if we haven't repped yet, or negative only if we have */
case 'like':
case 'positive':
return ( $repGiven === 1 ) ? ( $type === -1 ) : ( $type === 1 );
/* If we're using a negative system, we can rep negative only if we haven't repped yet, or positive only if we have */
case 'negative':
return $repGiven ? ( $type === 1 ) : ( $type === -1 );
/* If we're using a positive and negative system - it's more complicated... */
case 'both':
switch ( $repGiven )
{
/* If we haven't repped yet, we can do either */
case 0:
return TRUE;
/* If we've given positive rep already, we can only do negative now */
case 1:
return ( $type === -1 );
/* If we've given negative rep already, we can only do positive now */
case -1:
return ( $type === 1 );
}
}
/* Catch all */
return FALSE;
}
/**
* Give reputation
*
* @param int $type 1 for positive, -1 for negative
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return void
* @throws \DomainException|\BadMethodCallException
*/
public function giveReputation( $type, \IPS\Member $member=NULL )
{
if ( !( $this instanceof \IPS\Content\Reputation ) )
{
throw new \BadMethodCallException;
}
$member = $member ?: \IPS\Member::loggedIn();
if ( !$this->canGiveReputation( $type, $member ) )
{
throw new \DomainException( 'cannot_rep_user' );
}
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND member_id=? AND type_id=?', static::$application, static::$reputationType, $member->member_id, $this->$idColumn ) );
if ( $this->repGiven( $member ) )
{
if ( $this->reputation !== NULL )
{
unset( $this->reputation[ $member->member_id ] );
}
}
else
{
$limit = ( $type === 1 ) ? $member->group['g_rep_max_positive'] : $member->group['g_rep_max_negative'];
if ( $limit !== 999 )
{
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_index', array( 'member_id=? AND rep_date>? AND rep_rating=?', $member->member_id, \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp(), $type ) )->first();
if ( $count >= $limit )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->reputation_point_types == 'like' ? 'rep_daily_exceeded_likes' :'rep_daily_exceeded', FALSE, array( 'sprintf' => array( $limit ) ) ) );
}
}
\IPS\Db::i()->insert( 'core_reputation_index', array(
'member_id' => $member->member_id,
'app' => static::$application,
'type' => static::$reputationType,
'type_id' => $this->$idColumn,
'rep_date' => time(),
'rep_rating' => $type,
'member_received' => (int) $this->author()->member_id,
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
'rep_class' => get_class( $this ),
'item_id' => (int) $this->mapped('item')
) );
/* Send Notification */
if ( $type === 1 AND $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $this->author() ) )
{
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, \IPS\Member::loggedIn() ) );
$notification->recipients->attach( $this->author() );
$notification->send();
}
if ( $this->reputation !== NULL )
{
$this->reputation[ $member->member_id ] = $type;
}
}
if( $this->author()->member_id )
{
$this->author()->pp_reputation_points += $type;
$this->author()->save();
}
}
/**
* @brief Reputation Cache
*/
public $reputation = NULL;
/**
* Get reputation count
*
* @return int
* @throws \BadMethodCallException
*/
public function reputation()
{
if ( !( $this instanceof \IPS\Content\Reputation ) )
{
throw new \BadMethodCallException;
}
if ( $this->reputation === NULL )
{
$this->reputation = iterator_to_array( \IPS\Db::i()->select( 'member_id, rep_rating', 'core_reputation_index', $this->getReputationWhereClause() )->setKeyField( 'member_id' )->setValueField( 'rep_rating' ) );
return array_sum( $this->reputation );
}
else
{
$repCount = 0;
foreach( $this->reputation as $member => $value )
{
if( is_int( $value ) )
{
$repCount += $value;
}
else
{
$repCount += $value['rep_rating'];
}
}
return $repCount;
}
}
/**
* Get reputation where clause
*
* @return array
* @throws \BadMethodCallException
*/
public function getReputationWhereClause()
{
if ( !( $this instanceof \IPS\Content\Reputation ) )
{
throw new \BadMethodCallException;
}
$idColumn = static::$databaseColumnId;
$where = array( array( 'app=? AND type=? AND type_id=?', static::$application, static::$reputationType, $this->$idColumn ) );
switch( \IPS\Settings::i()->reputation_point_types )
{
case 'positive':
case 'like':
$where[] = array( 'rep_rating=?', "1" );
break;
case 'negative':
$where[] = array( 'rep_rating=?', "-1" );
break;
}
return $where;
}
/**
* @brief Cached like blurb
*/
public $likeBlurb = NULL;
/**
* Get "like" blurb (You and X others like this)
*
* @return string
*/
public function likeBlurb()
{
if( $this->likeBlurb === NULL )
{
/* Did anyone like it? */
$numberOfLikes = $this->reputation(); # int
if ( $numberOfLikes )
{
/* Is it just us? */
$userLiked = ( $this->repGiven() === 1 );
if ( $userLiked and $numberOfLikes < 2 )
{
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack('like_blurb_just_you');
}
/* Nope, we need to display a number... */
else
{
$peopleToDisplayInMainView = array();
$andXOthers = $numberOfLikes;
/* If the user liked, we always show "You" first */
if ( $userLiked )
{
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack('like_blurb_you_and_others');
$andXOthers--;
}
/* If we have permission to see who the others are, we need to do that... */
if ( \IPS\Member::loggedIn()->group['gbw_view_reps'] )
{
$peopleToDisplayInSecondaryView = array();
/* Some random names */
$idColumn = static::$databaseColumnId;
$i = 0;
$peopleToDisplayInSecondaryView = array();
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=? AND rep_rating=?', static::$application, static::$reputationType, $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0, 1 ), 'RAND()', $userLiked ? 17 : 18 ) as $rep )
{
if ( $i < ( $userLiked ? 2 : 3 ) )
{
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->userLink( \IPS\Member::load( $rep['member_id'] ) );
$andXOthers--;
}
else
{
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
}
$i++;
}
/* If there's people to display in the secondary view, add that */
if ( $peopleToDisplayInSecondaryView )
{
if ( count( $peopleToDisplayInSecondaryView ) < $andXOthers )
{
$peopleToDisplayInSecondaryView[] = \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb_others_secondary', FALSE, array( 'pluralize' => array( $andXOthers - count( $peopleToDisplayInSecondaryView ) ) ) );
}
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reputationOthers( $this->url( 'showRep' ), \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb_others', FALSE, array( 'pluralize' => array( $andXOthers ) ) ), json_encode( $peopleToDisplayInSecondaryView ) );
}
}
/* Otherwise the "and X others" will just be static */
else
{
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack( empty( $peopleToDisplayInMainView ) ? 'like_blurb_generic' : 'like_blurb_others', FALSE, array( 'pluralize' => array( $andXOthers ) ) );
}
/* Put it all together */
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb', FALSE, array( 'pluralize' => array( $numberOfLikes ), 'htmlsprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $peopleToDisplayInMainView ) ) ) );
}
}
/* Nobody liked it - show nothing */
else
{
$this->likeBlurb = '';
}
}
return $this->likeBlurb;
}
/**
* Has reputation been given by a particular member?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return int 1 = Positive rep given. -1 = Negative rep given. 0 = No rep given
* @throws \BadMethodCallException
*/
public function repGiven( \IPS\Member $member = NULL )
{
if ( !( $this instanceof \IPS\Content\Reputation ) )
{
throw new \BadMethodCallException;
}
if ( $this->reputation === NULL )
{
$this->reputation();
}
$member = $member ?: \IPS\Member::loggedIn();
return array_key_exists( $member->member_id, $this->reputation ) ? intval( $this->reputation[ $member->member_id ] ) : 0;
}
/**
* Get table showing who has given reputation
*
* @return \IPS\Helpers\Table\Db
* @throws \DomainException
* @throws \BadMethodCallException
*/
public function reputationTable()
{
if ( !( $this instanceof \IPS\Content\Reputation ) )
{
throw new \BadMethodCallException;
}
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
{
throw new \DomainException;
}
$idColumn = static::$databaseColumnId;
$table = new \IPS\Helpers\Table\Db( 'core_reputation_index', $this->url('showRep'), $this->getReputationWhereClause() );
$table->sortBy = 'rep_date';
$table->sortDirection = 'desc';
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reputationLogTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reputationLog' );
return $table;
}
/* !Follow */
const FOLLOW_PUBLIC = 1;
@@ -1874,4 +1773,79 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return (string) \IPS\Theme::i()->logo_sharer;
}
/**
* Log keyword usage, if any
*
* @param string $content Content/text of submission
* @param string|NULL $title Title of submission
* @return void
*/
public function checkKeywords( $content, $title=NULL )
{
/* Do we have any keywords to track? */
if( !\IPS\Settings::i()->stats_keywords )
{
return;
}
/* We need to know the ID */
$idColumn = static::$databaseColumnId;
/* If this is a content item and first comment is required, skip checking the comment */
if ( $this instanceof \IPS\Content\Comment )
{
$itemClass = static::$itemClass;
if( $itemClass::$firstCommentRequired === TRUE )
{
/* During initial post, at this point the firstCommentIdColumn value won't be set, so we check for that or explicitly if this is the first post */
if( !$this->item()->mapped('first_comment_id') OR $this->$idColumn == $this->item()->mapped('first_comment_id') )
{
return;
}
}
}
$words = preg_split("/[\s]+/", trim( strip_tags( preg_replace( "/<br( \/)?>/", "\n", $content ) ) ), NULL, PREG_SPLIT_NO_EMPTY );
if( $title !== NULL )
{
$titleWords = explode( ' ', $title );
$words = array_merge( $words, $titleWords );
}
$words = array_unique( $words );
$keywords = json_decode( \IPS\Settings::i()->stats_keywords, true );
$extraData = json_encode( array( 'class' => get_class( $this ), 'id' => $this->$idColumn ) );
foreach( $keywords as $keyword )
{
if( in_array( $keyword, $words ) )
{
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
}
}
}
/* !Deprecated */
/* The methods below are deprecated and will be removed in Invision Community 4.3 */
/**
* Get reputation count
*
* @return int
* @throws \BadMethodCallException
*/
public function reputation()
{
if ( !\IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
{
throw new \BadMethodCallException;
}
return $this->reactionCount();
}
}
File diff suppressed because it is too large. Load diff
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Edit History Interface for Content Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 14 Oct 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Embeddable Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 22 Apr 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Content Extension Generator
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 26 Dec 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Featurable Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 12 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Followable Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 Dec 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Polls Interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 10 Jan 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Hidebale Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 6 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+670 -167
View File
File diff suppressed because it is too large. Load diff
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Lockable Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 12 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* @brief MetaData Interface for Content Models/Comments
* @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 12 Nov 2013
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* MetaData Interface for Content Models/Comments
*
* @note Content classes will gain special functionality by implementing this interface
*/
interface MetaData
{
}
+19 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Moderator Permissions Interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 1 Oct 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
@@ -74,6 +73,21 @@ class _ModeratorPermissions
$this->actions[] = 'reply_to_locked';
}
$this->actions[] = 'delete';
if ( in_array( 'IPS\Content\MetaData', class_implements( $class ) ) AND $class::supportedMetaDataTypes() )
{
if ( in_array( 'core_FeaturedComments', $class::supportedMetaDataTypes() ) )
{
$this->actions[] = 'feature_comments';
$this->actions[] = 'unfeature_comments';
}
if ( in_array( 'core_ContentMessages', $class::supportedMetaDataTypes() ) )
{
$this->actions[] = 'add_item_message';
$this->actions[] = 'edit_item_message';
$this->actions[] = 'delete_item_message';
}
}
if ( isset( $class::$commentClass ) )
{
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Permissions Interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Pinnable Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 12 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Polls Interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 10 Jan 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+335
View File
@@ -0,0 +1,335 @@
<?php
/**
* @brief Facebook Promotion
* @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 10 Feb 2017
*/
namespace IPS\Content\Promote;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Facebook Promotion
*/
class _Facebook extends PromoteAbstract
{
/**
* @brief Icon
*/
public static $icon = 'facebook';
/**
* @brief Default settings
*/
public $defaultSettings = array(
'token' => NULL,
'owner' => NULL,
'page_name' => NULL,
'permissions' => NULL,
'members' => NULL,
'page' => NULL,
'tags' => NULL,
'image' => NULL,
'last_sync' => 0
);
/**
* Is connected?
*
* @return bool
*/
public function connected()
{
return ( $this->member->fb_uid and $this->member->fb_token );
}
/**
* Get image
*
* @return string
*/
public function getPhoto()
{
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
{
/* Fetch again */
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large" )->request()->get();
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
$newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
}
return $this->settings['image'];
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->settings['page_name'];
}
/**
* Check publish permissions
*
* @return boolean
*/
public function canPostToPage()
{
try
{
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/permissions" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
if ( isset( $response['error'] ) )
{
return $response['error']['message'];
}
if ( !empty( $response['data'] ) )
{
$got = 0;
foreach( $response['data'] as $permission )
{
if ( in_array( $permission['permission'], array( 'manage_pages', 'publish_pages' ) ) and $permission['status'] == 'granted' )
{
$got++;
}
}
return $got === 2;
}
}
catch ( \IPS\Http\Request\Exception $e )
{
\IPS\Log::log( $e, 'facebook' );
}
return NULL;
}
/**
* Get pages this user manages
*
* @return array
*/
public function getPages()
{
$pages = array();
try
{
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/accounts" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
if ( !empty( $response['data'] ) )
{
foreach( $response['data'] as $page )
{
$pages[ $page['id'] ] = array( $page['name'], $page['access_token'] );
}
}
}
catch ( \IPS\Http\Request\Exception $e )
{
\IPS\Log::log( $e, 'facebook' );
}
return $pages;
}
/**
* Get form elements for this share service
*
* @param string $text Text for the text entry
* @param string $link Short or full link (short when available)
* @param string $content Additional text content (usually a comment, or the item content)
*
* @return array of form elements
*/
public function form( $text, $link=null, $content=null )
{
if ( $this->promote and $this->promote->id )
{
$_text = $text;
}
else
{
$_text = $text;
if ( $content )
{
$_text .= "\n" . $content;
}
if ( $link )
{
$_text .= "\n" . $link;
}
if ( count( $this->settings['tags'] ) )
{
$_text .= "\n" . '#' . implode( ' #', $this->settings['tags'] );
}
}
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ) );
}
/**
* Post to Facebook
*
* @param \IPS\core\Promote $promote Promote Object
* @return void
*/
public function post( $promote )
{
$photos = $promote->imageObjects();
/* Get the last 20 items to see if we have a duplicate */
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->setQueryString( array( 'limit' => 20, 'access_token' => $this->settings['token'] ) )->request()->get()->decodeJson();
if ( isset( $items['data'] ) )
{
foreach( $items['data'] as $item )
{
$time = new \IPS\DateTime( $item['created_time'] );
$now = new \IPS\DateTime();
/* Only look back past the last 30 mins */
if ( intval( $now->diff( $time )->format('%i') ) > 30 )
{
continue;
}
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
{
/* Duplicate */
return $item['id'];
}
}
}
if ( ! $photos or count( $photos ) === 1 )
{
/* Simple message */
try
{
if ( $photos )
{
$thePhoto = array_pop( $photos );
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/photos" )->request()->post( array(
'message' => $promote->text['facebook'],
'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ),
'access_token' => $this->settings['token'],
'link' => $promote->short_link
) )->decodeJson();
}
else
{
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->request()->post( array(
'message' => $promote->text['facebook'],
'access_token' => $this->settings['token'],
'link' => $promote->short_link
) )->decodeJson();
}
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'facebook' );
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
}
if ( isset( $this->response['id'] ) )
{
return $this->response['id'];
}
else
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
}
}
else
{
/* We have multiple photos so we first need to create an album, and then upload into that */
try
{
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/albums" )->request()->post( array(
'name' => $promote->objectTitle,
'access_token' => $this->settings['token'],
) )->decodeJson();
if ( ! isset( $this->response['id'] ) )
{
throw new \InvalidArgumentException('Could not create album');
}
$newAlbumId = $this->response['id'];
foreach( $photos as $photo )
{
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( array(
'message' => $promote->text['facebook'],
'url' => (string) $this->returnUrlWithProtocol( $photo->url ),
'access_token' => $this->settings['token'],
) )->decodeJson();
}
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'facebook' );
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
}
return $this->response['id'];
}
}
/**
* Return the published URL
*
* @param array $data Data returned from a successful POST
* @return \IPS\Http\Url
* @throws InvalidArgumentException
*/
public function getUrl( $data )
{
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
{
return \IPS\Http\Url::external( 'https://facebook.com/' . $data );
}
throw new \InvalidArgumentException();
}
/**
* Ensure the URL is not protocol relative
*
* @return \IPS\Http\Url
*/
protected function returnUrlWithProtocol( \IPS\Http\Url $url )
{
if ( ! $url->data['scheme'] )
{
$url = $url->setScheme( ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) ? 'https' : 'http' );
}
return $url;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* @brief Internal Promotion
* @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 10 FEB 2017
*/
namespace IPS\Content\Promote;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Internal Promotion
*/
class _Internal extends PromoteAbstract
{
/**
* @brief Icon
*/
public static $icon = 'plus';
/**
* @brief Default settings
*/
public $defaultSettings = array();
/**
* Get image
*
* @return string
*/
public function getPhoto()
{
return \IPS\Theme::i()->settings['logo_sharer'] ? \IPS\Theme::i()->settings['logo_sharer'] : \IPS\Theme::i()->settings['logo_front'];
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return \IPS\Settings::i()->board_name;
}
/**
* Get form elements for this share service
*
* @param string $text Text for the text entry
* @param string $link Short or full link (short when available)
* @param string $content Additional text content (usually a comment, or the item content)
*
* @return array of form elements
*/
public function form( $text, $link=null, $content=null )
{
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_internal', $content ?: $text, FALSE, array( 'maxLength' => 3000, 'rows' => 6 ) ) );
}
/**
* Post to internal
*
* @param \IPS\Content\Promote $promote Promote Object
* @return void
*/
public function post( $promote )
{
return time();
}
/**
* Return the published URL
*
* @param array $data Data returned from a successful POST
* @return \IPS\Http\Url
* @throws InvalidArgumentException
*/
public function getUrl( $data )
{
return NULL;
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
/**
* @brief Promote Abstract Class
* @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 13 Jun 2013
*/
namespace IPS\Content\Promote;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Promote Handler
*/
abstract class _PromoteAbstract extends \IPS\Patterns\ActiveRecord
{
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_social_promote_sharers';
/**
* @brief [ActiveRecord] Database Prefix
*/
public static $databasePrefix = 'sharer_';
/**
* @brief [ActiveRecord] ID Database Column
*/
public static $databaseColumnId = 'key';
/**
* @brief [Node] Order Database Column
*/
public static $databaseColumnOrder = 'key';
/**
* @brief [Node] Automatically set position for new nodes
*/
public static $automaticPositionDetermination = FALSE;
/**
* @brief Default settings
*/
public $defaultSettings = array();
/**
* @brief The last response sent back
*/
public $response = NULL;
/**
* @brief \IPS\core\Promote object
*/
public $promote = NULL;
/**
* Construct ActiveRecord from database row
*
* @param array $data Row from database table
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
* @return static
*/
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
{
/* Initiate an object */
$classname = '\\IPS\Content\\Promote\\' . \ucfirst( $data['sharer_key'] );
if ( !class_exists( $classname ) )
{
throw new \RuntimeException;
}
$obj = new $classname;
$obj->_new = FALSE;
/* Import data */
$databasePrefixLength = \strlen( static::$databasePrefix );
foreach ( $data as $k => $v )
{
if( static::$databasePrefix )
{
$k = \substr( $k, $databasePrefixLength );
}
$obj->$k = $v;
}
$obj->changed = array();
/* Return */
return $obj;
}
/**
* @brief Member
*/
protected $member;
/**
* Constructor
*
* \IPS\Member $member The member
* @returns $this for daisy chaining
*/
public function setMember( \IPS\Member $member )
{
$this->member = $member;
return $this;
}
/**
* Returns the lowercase key
*
* @returns string
*/
public function get__key()
{
return mb_strtolower( $this->key );
}
/**
* Get settings
*
* @return array
*/
public function get_settings()
{
return array_merge( $this->defaultSettings, json_decode( $this->_data['settings'], TRUE ) );
}
/**
* Can a member promote using this service?
*
* @return bool
*/
public function canPromote()
{
return $this->enabled and \IPS\core\Promote::canPromote( $this->member );
}
/**
* Get image
*
* @return string
*/
abstract public function getPhoto();
/**
* Get name
*
* @return string
*/
abstract public function getName();
/**
* Return the published URL
*
* @param array $data Data returned from a successful POST
* @return \IPS\Http\Url
* @throws InvalidArgumentException
*/
abstract function getUrl( $data );
/**
* Bulk save settings
*
* @param array $settings Array of (k => v) settings
* @return void
*/
public function saveSettings( $settings )
{
$this->settings = json_encode( array_merge( $this->settings, $settings ) );
$this->save();
}
/**
* [ActiveRecord] Save Changed Columns
*
* @return void
*/
function save()
{
parent::save();
unset( \IPS\Data\Store::i()->promoters );
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
/**
* @brief Twitter Promotion
* @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 10 FEB 2017
*/
namespace IPS\Content\Promote;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Twitter Promotion
*/
class _Twitter extends PromoteAbstract
{
/**
* @brief Icon
*/
public static $icon = 'twitter';
/**
* @brief Default settings
*/
public $defaultSettings = array(
'id' => NULL,
'owner' => NULL,
'secret' => NULL,
'token' => NULL,
'name' => NULL,
'permissions' => NULL,
'members' => NULL,
'tags' => NULL,
'tags_method' => 'fill',
'image' => NULL,
'last_sync' => 0
);
/**
* @brief Twitter log in object
*/
protected static $twitter = NULL;
/**
* Twitter object
*
* @return Object
*/
protected function twitter()
{
if ( static::$twitter === NULL )
{
static::$twitter = \IPS\Login::getHandler('Twitter');
}
return static::$twitter;
}
/**
* Get image
*
* @return string
*/
public function getPhoto()
{
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
{
$user = $this->twitter()->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $this->settings['token'], $this->settings['secret'] )->decodeJson();
/* Fetch again */
$response = \IPS\Http\Url::external( $user['profile_image_url'] )->request()->get();
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
$newFile = \IPS\File::create( 'core_Promote', 'twitter_' . $this->settings['id'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
}
return $this->settings['image'];
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->settings['name'];
}
/**
* Check publish permissions
*
* @return boolean
*/
public function canPostToPage()
{
try
{
$this->twitter()->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $this->settings['token'], $this->settings['secret'] )->decodeJson();
return TRUE;
}
catch ( \IPS\Http\Request\Exception $e ) { }
return FALSE;
}
/**
* Get form elements for this share service
*
* @param string $text Text for the text entry
* @param string $link Short or full link (short when available)
* @param string $content Additional text content (usually a comment, or the item content)
*
* @return array of form elements
*/
public function form( $text, $link=null, $content=null )
{
$textToActuallyUse = $text;
if ( $link )
{
$textToActuallyUse .= ' ' . $link;
}
if ( mb_strlen( $textToActuallyUse ) < 140 )
{
/* Got any tags to add? */
if ( count( $this->settings['tags'] ) )
{
if ( $this->settings['tags_method'] === 'trim' )
{
$urlLength = mb_strlen( $link ) + 2; // spaces either side
$left = 140 - $urlLength;
$tagLength = 0;
$tagString = '';
$useText = $text;
foreach( $this->settings['tags'] as $tag )
{
if ( $left > 0 )
{
$tagString .= ' #' . $tag;
$left -= mb_strlen( $tagString );
}
}
if ( $left > 20 )
{
$useText = mb_substr( $text, 0, $left );
}
$textToActuallyUse = $useText . ' ' . $link . ' ' . $tagString;
}
else
{
foreach( $this->settings['tags'] as $tag )
{
$considerAddingThis = ' #' . $tag;
if ( mb_strlen( $textToActuallyUse . $considerAddingThis ) < 140 )
{
$textToActuallyUse .= $considerAddingThis;
}
}
}
}
}
$return = array();
if ( $this->promote and $this->promote->id )
{
$textToActuallyUse = $text;
$return[] = \IPS\Theme::i()->getTemplate( 'promote' )->promoteDialogTwitterDuplicate();
}
$return[] = new \IPS\Helpers\Form\TextArea( 'promote_social_content_twitter', $textToActuallyUse, FALSE, array( 'maxLength' => 300, 'rows' => 3 ) );
return $return;
}
/**
* Post to Twitter
*
* @param \IPS\Content\Promote $promote Promote Object
* @return void
*/
public function post( $promote )
{
$photos = $promote->imageObjects();
$mediaIds = array();
if ( count( $photos ) )
{
try
{
$done = 0;
foreach( $photos as $photo )
{
/* Twitter can only have a max of 4 images */
if ( $done < 4 )
{
$this->response = $this->twitter()->sendRequest( 'post', 'https://upload.twitter.com/1.1/media/upload.json', array( 'media' => $photo->contents() ), $this->settings['token'], $this->settings['secret'] )->decodeJson();
if ( isset( $this->response['media_id_string'] ) )
{
$mediaIds[] = $this->response['media_id_string'];
}
else
{
\IPS\Log::log( $this->response, 'twitter' );
}
$done++;
}
}
}
catch( \Exception $e )
{
\IPS\Log::log( $e->getMessage(), 'twitter' );
}
}
try
{
$send = array( 'status' => $promote->text['twitter'] );
if ( count( $mediaIds ) )
{
$send['media_ids'] = implode( ',', $mediaIds );
}
$this->response = $this->twitter()->sendRequest( 'post', "https://api.twitter.com/1.1/statuses/update.json", $send, $this->settings['token'], $this->settings['secret'] )->decodeJson();
if ( isset( $this->response['id_str'] ) )
{
return $this->response['id_str'];
}
else
{
/* Check for non critical errors we don't need to flag as complete failures */
if ( isset( $this->response['errors'] ) )
{
foreach( $this->response['errors'] as $error )
{
/* Status is a duplicate */
if ( $error['code'] == 187 )
{
/* Fetch the original string if possible */
foreach( $promote->responses('twitter') as $response )
{
if ( isset( $response['id_str'] ) )
{
return $response['id_str'];
}
}
/* No? Ok */
return 'Duplicate status';
}
}
}
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
}
}
catch ( \Exception $e )
{
\IPS\Log::log( $e->getMessage(), 'twitter' );
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
}
}
/**
* Return the published URL
*
* @param array $data Data returned from a successful POST
* @return \IPS\Http\Url
* @throws InvalidArgumentException
*/
public function getUrl( $data )
{
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
{
return \IPS\Http\Url::external( 'https://twitter.com/' . $this->settings['id'] . '/status/' . $data );
}
throw new \InvalidArgumentException();
}
}
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Rating Interface for Content Models/Comments
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 15 Jan 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+575
View File
@@ -0,0 +1,575 @@
<?php
/**
* @brief Reaction Trait
* @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 10 Nov 2016
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Reaction Trait
*/
trait Reactable
{
/**
* Reaction type
*
* @return string
*/
public static function reactionType()
{
throw new \BadMethodCallException;
}
/**
* React
*
* @param \IPS\core\Reaction THe reaction
* @param \IPS\Member The member reacting, or NULL
* @return void
* @throws \DomainException
*/
public function react( \IPS\Content\Reaction $reaction, \IPS\Member $member = NULL )
{
/* Did we pass a member? */
$member = $member ?: \IPS\Member::loggedIn();
/* Figure out the owner of this - if it is content, it will be the author. If it is a node, then it will be the person who created it */
if ( $this instanceof \IPS\Content )
{
$owner = $this->author();
}
else if ( $this instanceof \IPS\Node\Model )
{
$owner = $this->owner();
}
/* Can we react? */
if ( $this->canReact( $member ) === FALSE )
{
throw new \DomainException( 'cannot_react' );
}
/* Have we hit our limit? Also, why 999 for unlimited? */
if ( $member->group['g_rep_max_positive'] !== -1 )
{
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_index', array( 'member_id=? AND rep_date>?', $member->member_id, \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp() ) )->first();
if ( $count >= $member->group['g_rep_max_positive'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'react_daily_exceeded', FALSE, array( 'sprintf' => array( $member->group['g_rep_max_positive'] ) ) ) );
}
}
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
/* If this is a comment, we need the parent items ID */
$itemId = 0;
if ( $this instanceof \IPS\Content\Comment )
{
$item = $this->item();
$itemIdColumn = $item::$databaseColumnId;
$itemId = $item->$itemIdColumn;
}
/* Have we already reacted? */
if ( $this->reacted( $member ) )
{
$this->removeReaction( $member );
}
/* Actually insert it */
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->insert( 'core_reputation_index', array(
'member_id' => $member->member_id,
'app' => $app,
'type' => static::reactionType(),
'type_id' => $this->$idColumn,
'rep_date' => \IPS\DateTime::create()->getTimestamp(),
'rep_rating' => $reaction->value,
'member_received' => $owner->member_id,
'rep_class' => get_class( $this ),
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
'item_id' => $itemId,
'reaction' => $reaction->id
) );
/* Send the notification */
if ( $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $owner ) )
{
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, $member ) );
$notification->recipients->attach( $owner );
$notification->send();
}
if ( $owner->member_id )
{
$owner->pp_reputation_points += $reaction->value;
$owner->save();
}
}
/**
* Remove Reaction
*
* @param \IPS\Member|NULL The member, or NULL
* @return void
*/
public function removeReaction( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
try
{
try
{
$idColumn = static::$databaseColumnId;
$rep = \IPS\Db::i()->select( '*', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first();
}
catch( \UnderflowException $e )
{
throw new \OutOfRangeException;
}
$memberReceived = \IPS\Member::load( $rep['member_received'] );
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
}
catch( \OutOfRangeException $e )
{
throw new \DomainException;
}
if ( $memberReceived->member_id )
{
$memberReceived->pp_reputation_points = $memberReceived->pp_reputation_points - $reaction->value;
$memberReceived->save();
}
\IPS\Db::i()->delete( 'core_reputation_index', array( "id=?", $rep['id'] ) );
}
/**
* Can React
*
* @param \IPS\Member|NULL The member, or NULL for currently logged in
* @return bool
*/
public function canReact( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $this instanceof \IPS\Content )
{
$owner = $this->author();
}
else if ( $this instanceof \IPS\Node\Model )
{
$owner = $this->owner();
}
/* Only members can react */
if ( !$member->member_id )
{
return FALSE;
}
if ( !$owner->member_id )
{
return FALSE;
}
/* Protected Groups */
if ( $owner->inGroup( explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
{
return FALSE;
}
/* Reactions per day */
if ( $member->group['g_rep_max_positive'] == 0 )
{
return FALSE;
}
/* React to own content */
if ( !\IPS\Settings::i()->reputation_can_self_vote AND $this->author()->member_id == $member->member_id )
{
return FALSE;
}
/* Still here? All good. */
return TRUE;
}
/**
* @brief Reactions Cache
*/
protected $_reactions = NULL;
/**
* Reactions
*
* @param array|NULL $mixInData If the data is already know, it can be passed here to be manually set
* @return array
*/
public function reactions()
{
if ( $this->_reactionCount === NULL )
{
$this->_reactionCount = 0;
}
if ( $this->_reactions === NULL )
{
$idColumn = static::$databaseColumnId;
$this->_reactions = array();
if ( is_array( $this->reputation ) )
{
foreach( $this->reputation AS $memberId => $reactionId )
{
try
{
$this->_reactionCount += \IPS\Content\Reaction::load( $reactionId )->value;
$this->_reactions[ $memberId ][] = $reactionId;
}
catch( \OutOfRangeException $e ) { }
}
}
else
{
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
{
$this->_reactions[ $reaction['member_id'] ][] = $reaction['reaction'];
$this->_reactionCount += $reaction['rep_rating'];
}
}
}
return $this->_reactions;
}
/**
* @brief Reaction Count
*/
protected $_reactionCount = NULL;
/**
* Reaction Count
*
* @return int
*/
public function reactionCount()
{
$this->reactions();
return $this->_reactionCount;
}
/**
* Reaction Where Clause
*
* @param \IPS\Content\Reaction|array|int|NULL This can be any one of the following: An \IPS\Content\Reaction object, an array of \IPS\Content\Reaction objects, and integer, or an array of integers, or NULL
* @return array
*/
public function getReactionWhereClause( $reactions = NULL )
{
$idColumn = static::$databaseColumnId;
$where = array( array( 'rep_class=? AND type=? AND type_id=? AND reaction_enabled=?', get_class( $this ), static::reactionType(), $this->$idColumn, 1 ) );
if ( $reactions !== NULL )
{
if ( !is_array( $reactions ) )
{
$reactions = array( $reactions );
}
$in = array();
foreach( $reactions AS $reaction )
{
if ( $reaction instanceof \IPS\Content\Reaction )
{
$in[] = $reaction->id;
}
else
{
$in[] = $reaction;
}
}
if ( count( $in ) )
{
$where[] = array( \IPS\Db::i()->in( 'reaction', $in ) );
}
}
return $where;
}
/**
* Reaction Table
*
* @return \IPS\Helpers\Table\Db
*/
public function reactionTable( $reaction=NULL )
{
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
{
throw new \DomainException;
}
$idColumn = static::$databaseColumnId;
$table = new \IPS\Helpers\Table\Db( 'core_reputation_index', $this->url('showReactions'), $this->getReactionWhereClause( $reaction ) );
$table->sortBy = 'rep_date';
$table->sortDirection = 'desc';
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLogTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLog' );
$table->joins = array( array( 'from' => 'core_reactions', 'where' => 'reaction=reaction_id' ) );
$table->rowButtons = function( $row )
{
return array(
'delete' => array(
'icon' => 'times-circle',
'title' => 'delete',
'link' => $this->url( 'unreact' )->csrf()->setQueryString( array( 'member' => $row['member_id'] ) ),
'data' => array( 'confirm' => TRUE )
)
);
};
return $table;
}
/**
* Has reacted?
*
* @param \IPS\Member|NULL The member, or NULL for currently logged in
* @return \IPS\Content\Reaction|FALSE
*/
public function reacted( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
$idColumn = static::$databaseColumnId;
try
{
if ( is_array( $this->reputation ) )
{
if ( isset( $this->reputation[ $member->member_id ] ) )
{
return \IPS\Content\Reaction::load( $this->reputation[ $member->member_id ] );
}
else
{
return FALSE;
}
}
else
{
return \IPS\Content\Reaction::load( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first() );
}
}
catch( \UnderflowException $e )
{
return FALSE;
}
}
/**
* @brief Cached React Blurb
*/
public $reactBlurb = NULL;
/**
* React Blurb
*
* @return string
*/
public function reactBlurb()
{
if ( $this->reactBlurb === NULL )
{
$this->reactBlurb = array();
if ( count( $this->reactions() ) )
{
$idColumn = static::$databaseColumnId;
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "app=? AND rep_class=? AND type=? AND type_id=?", $app, get_class( $this ), static::reactionType(), $this->$idColumn ) ) AS $rep )
{
if ( !isset( $this->reactBlurb[ $rep ] ) )
{
$this->reactBlurb[ $rep ] = 0;
}
$this->reactBlurb[ $rep ]++;
}
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
@uksort( $this->reactBlurb, function( $a, $b ) {
try
{
$a = \IPS\Content\Reaction::load( $a );
$b = \IPS\Content\Reaction::load( $b );
}
/* One of the reactions does not exist */
catch( \OutOfRangeException $e )
{
return 1;
}
if ( $a->position < $b->position )
{
return -1;
}
elseif ( $a->position == $b->position )
{
return 0;
}
else
{
return 1;
}
} );
}
else
{
$this->reactBlurb = array();
}
}
return $this->reactBlurb;
}
/**
* @brief Cached like blurb
*/
public $likeBlurb = NULL;
/**
* Who Reacted
*
* @param bool|NULL Use like text instead? NULL to automatically determine
* @return string
*/
public function whoReacted( $isLike = NULL )
{
if ( $isLike === NULL )
{
$isLike = \IPS\Content\Reaction::isLikeMode();
}
if( $this->likeBlurb === NULL )
{
$langPrefix = 'react_';
if ( $isLike )
{
$langPrefix = 'like_';
}
/* Did anyone like it? */
$numberOfLikes = $this->reactionCount(); # int
if ( $numberOfLikes )
{
/* Is it just us? */
$userLiked = ( $this->reacted() );
if ( $userLiked and $numberOfLikes < 2 )
{
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_just_you");
}
/* Nope, we need to display a number... */
else
{
$peopleToDisplayInMainView = array();
$andXOthers = $numberOfLikes;
/* If the user liked, we always show "You" first */
if ( $userLiked )
{
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_you_and_others");
$andXOthers--;
}
$peopleToDisplayInSecondaryView = array();
/* Some random names */
$idColumn = static::$databaseColumnId;
$i = 0;
$peopleToDisplayInSecondaryView = array();
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=?', $app, static::reactionType(), $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0 ), 'RAND()', $userLiked ? 17 : 18 ) as $rep )
{
if ( $i < ( $userLiked ? 2 : 3 ) )
{
$peopleToDisplayInMainView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$andXOthers--;
}
else
{
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
}
$i++;
}
/* If there's people to display in the secondary view, add that */
if ( $peopleToDisplayInSecondaryView )
{
if ( count( $peopleToDisplayInSecondaryView ) < $andXOthers )
{
$peopleToDisplayInSecondaryView[] = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others_secondary", FALSE, array( 'pluralize' => array( $andXOthers - count( $peopleToDisplayInSecondaryView ) ) ) );
}
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reputationOthers( $this->url( 'showReactions' ), \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others", FALSE, array( 'pluralize' => array( $andXOthers ) ) ), json_encode( $peopleToDisplayInSecondaryView ) );
}
/* Put it all together */
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb", FALSE, array( 'pluralize' => array( $numberOfLikes ), 'htmlsprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $peopleToDisplayInMainView ) ) ) );
}
}
/* Nobody liked it - show nothing */
else
{
$this->likeBlurb = '';
}
}
return $this->likeBlurb;
}
}
+334
View File
@@ -0,0 +1,334 @@
<?php
/**
* @brief Reaction Model
* @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 10 Nov 2016
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Reaction Model
*/
class _Reaction extends \IPS\Node\Model
{
/**
* @brief Database Table
*/
public static $databaseTable = 'core_reactions';
/**
* @brief Database Prefix
*/
public static $databasePrefix = 'reaction_';
/**
* @brief Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] ID Database Column
*/
public static $databaseColumnId = 'id';
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static $multitonMap = array();
/**
* @brief [Node] Node Title
*/
public static $nodeTitle = 'reactions';
/**
* @brief [Node] Sortable
*/
public static $nodeSortable = TRUE;
/**
* @brief [Node] Positon Column
*/
public static $databaseColumnOrder = 'position';
/**
* @brief [Node] Modal Forms because Charles loves them so
*/
public static $modalForms = TRUE;
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
*/
public static $titleLangPrefix = 'reaction_title_';
/**
* @brief [Node] Enabled/Disabled Column
*/
public static $databaseColumnEnabledDisabled = 'enabled';
/**
* Form
*
* @param \IPS\Helpers\Form The form
* @return void
*/
public function form( &$form )
{
$form->add( new \IPS\Helpers\Form\Translatable( 'reaction_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'reaction_title_' . $this->id : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'reaction_value', $this->id ? $this->value : 1, TRUE, array( 'options' => array( 1 => 'positive', 0 => 'neutral', -1 => 'negative' ) ) ) );
$form->add( new \IPS\Helpers\Form\Upload( 'reaction_icon', $this->id ? \IPS\File::get( 'core_Reaction', $this->icon ) : NULL, TRUE, array( 'image' => TRUE, 'storageExtension' => 'core_Reaction', 'storageContainer' => 'reactions', 'obscure' => FALSE ) ) );
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( $values )
{
if ( !$this->id )
{
$this->save();
}
\IPS\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()
{
if ( $this->id === 1 )
{
return FALSE;
}
return TRUE;
}
/**
* Get Icon
*
* @return \IPS\File
*/
public function get__icon()
{
return \IPS\File::get( 'core_Reaction', $this->_data['icon'] );
}
/**
* Get Description
*
* @return strong
*/
public function get__description()
{
if ( $this->value == 1 )
{
return \IPS\Member::loggedIn()->language()->addToStack('positive');
}
elseif ( $this->value == -1 )
{
return \IPS\Member::loggedIn()->language()->addToStack('negative');
}
else
{
return \IPS\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 \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
* @param mixed $where Additional WHERE clause
* @return array
*/
public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
{
if ( !count( $where ) )
{
$return = array();
foreach( static::reactionStore() AS $reaction )
{
$return[ $reaction['reaction_id'] ] = static::constructFromData( $reaction );
}
return $return;
}
else
{
return parent::roots( $permissionCheck, $member, $where );
}
}
/**
* [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()
{
if ( $this->id == 1 )
{
return NULL;
}
return parent::get__enabled();
}
/**
* Set Enabled
*
* @return void
*/
public function set__enabled( $enabled )
{
parent::set__enabled( $enabled );
unset( \IPS\Data\Store::i()->reactions );
}
/**
* Reaction Store
*
* @return array
*/
public static function reactionStore()
{
if ( !isset( \IPS\Data\Store::i()->reactions ) )
{
\IPS\Data\Store::i()->reactions = iterator_to_array( \IPS\Db::i()->select( '*', 'core_reactions', NULL, "reaction_position ASC" )->setKeyField( 'reaction_id' ) );
}
return \IPS\Data\Store::i()->reactions;
}
/**
* Is Like Mode
*
* @return bool
*/
public static function isLikeMode()
{
$i = 0;
foreach( static::roots() AS $row )
{
if ( $row->_enabled !== FALSE )
{
$i++;
}
}
return ( $i == 1 );
}
/**
* Load Record
*
* @see \IPS\Db::build
* @param int|string $id ID
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details)
* @return static
* @throws \InvalidArgumentException
* @throws \OutOfRangeException
*/
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
{
if ( ( $idField === NULL or $idField === 'reaction_id' ) and $extraWhereClause === NULL )
{
$reactions = static::reactionStore();
if ( isset( $reactions[ $id ] ) )
{
return static::constructFromData( $reactions[ $id ] );
}
else
{
throw new \OutOfRangeException;
}
}
return parent::load( $id, $idField, $extraWhereClause );
}
/**
* [ActiveRecord] Delete
*
* @return void
*/
public function delete()
{
parent::delete();
unset( \IPS\Data\Store::i()->reactions );
}
/**
* [ActiveRecord] Save Changed Columns
*
* @return void
*/
public function save()
{
parent::save();
unset( \IPS\Data\Store::i()->reactions );
}
/**
* [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 string $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( $url, $subnode=FALSE )
{
$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' => \IPS\Member::loggedIn()->language()->addToStack('delete') ),
'hotkey'=> 'd'
);
}
return $buttons;
}
}
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Read/Unread Tracking Interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Interface for Reportable Comment Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
-28
View File
@@ -1,28 +0,0 @@
<?php
/**
* @brief Reputation interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 27 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Reputation Interface for Content Models
*
* @note Content classes will gain special functionality by implementing this interface
*/
interface Reputation
{
}
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Content Review Model
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 4 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+20 -15
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Content Filter for Search Queries
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 6 Jul 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search;
@@ -32,6 +31,12 @@ class _ContentFilter
* @brief Classes to include
*/
public $classes = array();
/**
* @brief Container classes to include
* @note NULL means not to filter by container classes
*/
public $containerClasses = NULL;
/**
* @brief Container ID filter
@@ -85,7 +90,7 @@ class _ContentFilter
* @param bool $includeItems Include items in results?
* @param bool $includeComments Include comments in results?
* @param bool $includeReviews Include reviews in results?
* @return \IPS\Content\Search\ContainerFilter
* @return \IPS\Content\Search\ContentFilter
*/
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
{
@@ -114,7 +119,7 @@ class _ContentFilter
* Only include results in containers
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function onlyInContainers( array $ids )
{
@@ -128,7 +133,7 @@ class _ContentFilter
* Exclude results in containers
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function excludeInContainers( array $ids )
{
@@ -142,7 +147,7 @@ class _ContentFilter
* Only include results in items
*
* @param array $ids Acceptable item IDs
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function onlyInItems( array $ids )
{
@@ -156,7 +161,7 @@ class _ContentFilter
* Exclude results in items
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function excludeInItems( array $ids )
{
@@ -170,7 +175,7 @@ class _ContentFilter
* Set minimum number of comments
*
* @param int $minimumComments The minimum number of comments
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function minimumComments( $minimumComments )
{
@@ -183,7 +188,7 @@ class _ContentFilter
* Set minimum number of reviews
*
* @param int $minimumReviews The minimum number of reviews
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function minimumReviews( $minimumReviews )
{
@@ -196,7 +201,7 @@ class _ContentFilter
* Set minimum number of views
*
* @param int $minimumViews The minimum number of views
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function minimumViews( $minimumViews )
{
@@ -208,7 +213,7 @@ class _ContentFilter
/**
* Only include results which are the first comment on an item requiring a comment
*
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function onlyFirstComment()
{
@@ -220,7 +225,7 @@ class _ContentFilter
/**
* Only include results which are the last comment on an item requiring a comment
*
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
*/
public function onlyLastComment()
{
+27 -7
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Abstract Search Index
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search;
@@ -250,12 +249,32 @@ abstract class _Index extends \IPS\Patterns\Singleton
catch ( \Exception $e ) { }
}
}
/* Club */
$container = NULL;
$containerId = NULL;
$clubId = NULL;
if ( $object instanceof \IPS\Content\Item )
{
$containerId = (int) $object->searchIndexContainer();
$container = $object->containerWrapper();
}
else
{
$containerId = (int) $object->item()->mapped('container');
$container = $object->item()->containerWrapper();
}
if ( $container and \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) )
{
$clubId = $container->{$container::clubIdColumn()};
}
/* Return */
return array(
'index_class' => $class,
'index_object_id' => $object->$idColumn,
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? get_class( $object->searchIndexContainerClass() ) : get_class( $object->item()->searchIndexContainerClass() ),
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
'index_title' => $title,
'index_content' => $content,
@@ -268,7 +287,8 @@ abstract class _Index extends \IPS\Patterns\Singleton
'index_hidden' => $hiddenStatus,
'index_item_index_id' => $itemIndexId,
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
'index_is_last_comment' => $isLastComment
'index_is_last_comment' => $isLastComment,
'index_club_id' => $clubId
);
}
+17 -6
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief MySQL Search Index
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search\Mysql;
@@ -331,7 +330,7 @@ class _Index extends \IPS\Content\Search\Index
$where[] = array( 'index_container_id=?', $containerId );
}
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_item_id IN( ? )', \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index', $where );
}
@@ -375,6 +374,18 @@ class _Index extends \IPS\Content\Search\Index
if ( $newContainer )
{
$update['index_container_id'] = $newContainer;
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
{
try
{
$update['index_club_id'] = $containerClass::load( $newContainer )->$clubIdColumn;
}
catch ( \OutOfRangeException $e )
{
$update['index_club_id'] = NULL;
}
}
}
if ( $newItemId )
{
+67 -6
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief MySQL Search Query
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search\Mysql;
@@ -128,6 +127,12 @@ class _Query extends \IPS\Content\Search\Query
$clause[] = \IPS\Db::i()->in( 'index_container_id', $unsearchableIds, TRUE );
}
}
/* Are we excluding certain container classes? */
if( $filter->containerClasses !== NULL )
{
$clause[] = \IPS\Db::i()->in( 'index_container_class', $filter->containerClasses );
}
/* Set the item IDs */
if ( $filter->itemIdFilter !== NULL )
@@ -209,7 +214,32 @@ class _Query extends \IPS\Content\Search\Query
}
return $this;
}
}
/**
* Filter by club
*
* @param \IPS\Member\Club|int|array|null $club The club, or array of club IDs, or NULL to exclude content from clubs
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByClub( $club )
{
if ( $club === NULL )
{
$this->where[] = 'index_club_id IS NULL';
}
if ( is_array( $club ) )
{
$this->where[] = array( \IPS\Db::i()->in( 'index_club_id', $club ) );
}
else
{
$this->where[] = array( 'index_club_id=?', $club instanceof \IPS\Member\Club ? $club->id : $club );
}
return $this;
}
/**
* Filter for profile
@@ -253,6 +283,37 @@ class _Query extends \IPS\Content\Search\Query
return $this;
}
/**
* Filter by container class
*
* @param array $classes Container classes to exclude from results.
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
* such as Gallery images, comments and reviews.
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByContainerClasses( $classes=array(), $exclude=array() )
{
if( empty( $exclude ) )
{
$this->where[] = \IPS\Db::i()->in( 'index_container_class', $classes, TRUE );
}
else
{
foreach( $classes as $i => $class )
{
$classes[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
}
foreach( $exclude as $i => $class )
{
$exclude[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
}
$this->where[] = '( index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
}
return $this;
}
/**
* Filter by content the user follows
+13 -6
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Abstract Search Query
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search;
@@ -46,7 +45,7 @@ abstract class _Query
* Create new query
*
* @param \IPS\Member $member The member performing the search (NULL for currently logged in member)
* @return \IPS\Content\Search
* @return \IPS\Content\Search\Query
*/
public static function init( \IPS\Member $member = NULL )
{
@@ -108,6 +107,14 @@ abstract class _Query
*/
abstract public function filterByAuthor( $author );
/**
* Filter by club
*
* @param \IPS\Member\Club|int|array $club The club, or array of club IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByClub( $club );
/**
* Filter for profile
*
+8 -7
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Search Result from Index
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 15 Sep 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search\Result;
@@ -63,9 +62,10 @@ class _Content extends \IPS\Content\Search\Result
* @param array $reputationData Array of people who have given reputation and the reputation they gave
* @param int|NULL $reviewRating If this is a review, the rating
* @param bool $iPostedIn If the user has posted in the item
* @param array $reactions Reaction Data
* @return void
*/
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE )
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE, $reactions=array() )
{
$this->createdDate = \IPS\DateTime::ts( $indexData['index_date_created'] );
$this->lastUpdatedDate = \IPS\DateTime::ts( $indexData['index_date_updated'] );
@@ -76,6 +76,7 @@ class _Content extends \IPS\Content\Search\Result
$this->reputationData = $reputationData;
$this->reviewRating = $reviewRating;
$this->iPostedIn = $iPostedIn;
$this->reactions = $reactions;
}
/**
@@ -89,7 +90,7 @@ class _Content extends \IPS\Content\Search\Result
*/
public function html( $view = 'expanded', $asItem = FALSE, $canIgnoreComments=FALSE, $template=NULL )
{
return call_user_func( array( $this->indexData['index_class'], 'searchResult' ), $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem, $canIgnoreComments, $template );
return call_user_func( array( $this->indexData['index_class'], 'searchResult' ), $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem, $canIgnoreComments, $template, $this->reactions );
}
/**
+35 -6
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Search Result not from Index
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 15 Sep 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search\Result;
@@ -32,6 +31,11 @@ class _Custom extends \IPS\Content\Search\Result
* @brief Image
*/
protected $image;
/**
* @brief Data used for merging multiple extra items
*/
protected $mergeData;
/**
* Constructor
@@ -39,14 +43,39 @@ class _Custom extends \IPS\Content\Search\Result
* @param \IPS\DateTime $time The time for this result
* @param string $html HTML to display
* @param string|NULL $image HTML for image to display
* @param string|NULL $mergeData Data for merging
* @return void
*/
public function __construct( \IPS\DateTime $time, $html, $image = NULL )
public function __construct( \IPS\DateTime $time, $html, $image = NULL, $mergeData = NULL )
{
$this->createdDate = $time;
$this->lastUpdatedDate = $time;
$this->html = $html;
$this->image = $image;
$this->mergeData = array( $mergeData );
}
/**
* Merge in new data
*
* @param string $html New HTML to merge in as a list
* @param string|NULL $mergeData Data for merging
* @return void
*/
public function mergeInData( $html, $mergeData = NULL )
{
$this->html = $html;
$this->mergeData[] = $mergeData;
}
/**
* Get data for merging
*
* @return array
*/
public function getMergeData()
{
return $this->mergeData ?: array();
}
/**
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Search Result
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 15 Sep 2015
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search;
+157 -65
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Search Results
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\Search;
@@ -53,6 +52,11 @@ class _Results extends \ArrayIterator
*/
protected $reputationData = array();
/**
* @brief Reaction Data
*/
protected $reactions = array();
/**
* @brief Review Ratings
*/
@@ -139,7 +143,7 @@ class _Results extends \ArrayIterator
}
}
if ( in_array( 'IPS\Content\Reputation', class_implements( $result['index_class'] ) ) )
if ( \IPS\IPS::classUsesTrait( $result['index_class'], 'IPS\Content\Reactable' ) )
{
$reputationIds[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
}
@@ -153,8 +157,13 @@ class _Results extends \ArrayIterator
{
$itemIndexIds[] = $result['index_item_index_id'];
}
if ( $result['index_club_id'] )
{
$containersToLoad[ 'IPS\Member\Club' ][ $result['index_club_id'] ] = $result['index_club_id'];
}
}
/* Load item data */
foreach ( $itemsToLoad as $itemClass => $itemIds )
{
@@ -162,7 +171,7 @@ class _Results extends \ArrayIterator
{
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
{
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
$membersToLoad[ $memberId ] = $memberId;
@@ -198,26 +207,23 @@ class _Results extends \ArrayIterator
$binds = array();
foreach ( $reputationIds as $class => $ids )
{
$clause[] = "( app=? AND type=? AND " . \IPS\Db::i()->in( 'type_id', $ids ) . " )";
$clause[] = "( reaction_enabled=? AND app=? AND type=? AND " . \IPS\Db::i()->in( 'type_id', $ids ) . " )";
$binds[] = 1;
$binds[] = $class::$application;
$binds[] = $class::$reputationType;
$binds[] = $class::reactionType();
}
$where = array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) );
switch( \IPS\Settings::i()->reputation_point_types )
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating', 'reaction' ), 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
case 'positive':
case 'like':
$where[] = array( 'rep_rating=?', "1" );
break;
case 'negative':
$where[] = array( 'rep_rating=?', "-1" );
break;
}
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating' ), 'core_reputation_index', $where ) as $rep )
{
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['rep_rating'];
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['reaction'];
if ( !isset( $this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ] ) )
{
$this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ] = 0;
}
$this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ]++;
}
}
@@ -258,9 +264,15 @@ class _Results extends \ArrayIterator
$containerClass = isset( $itemClass::$containerNodeClass ) ? $itemClass::$containerNodeClass : NULL;
$reputationData = array();
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and isset( $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ] ) )
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
{
$reputationData = $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ];
$reputationData = $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
}
$reactions = array();
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
{
$reactions = $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
}
/* Ensure we have an item if results are gathered from the map table */
@@ -295,14 +307,25 @@ class _Results extends \ArrayIterator
$data['index_prefix'] = $this->prefixes[ $data['index_item_index_id'] ]['index_prefix'];
}
$containerData = NULL;
if ( $containerClass and isset( $this->containerData[ $containerClass ] ) and isset( $this->containerData[ $containerClass ][ $data['index_container_id'] ] ) )
{
$containerData = $this->containerData[ $containerClass ][ $data['index_container_id'] ];
if ( $data['index_club_id'] and isset( $this->containerData[ 'IPS\Member\Club' ][ $data['index_club_id'] ] ) )
{
$containerData['_club'] = $this->containerData[ 'IPS\Member\Club' ][ $data['index_club_id'] ];
}
}
return new Result\Content(
$data,
isset( $this->authorData[ $data['index_author'] ] ) ? $this->authorData[ $data['index_author'] ] : array( 'member_id' => 0, 'name' => \IPS\Member::loggedIn()->language()->addToStack('guest'), 'members_seo_name' => '', 'pp_main_photo' => NULL, 'pp_photo_type' => 'none' ),
$itemData,
( $containerClass and isset( $this->containerData[ $containerClass ] ) and isset( $this->containerData[ $containerClass ][ $data['index_container_id'] ] ) ) ? $this->containerData[ $containerClass ][ $data['index_container_id'] ] : NULL,
$containerData,
$reputationData,
isset( $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] ) ? $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] : NULL,
in_array( $data['index_item_index_id'], $this->iPostedIn )
in_array( $data['index_item_index_id'], $this->iPostedIn ),
$reactions
);
}
@@ -383,8 +406,15 @@ class _Results extends \ArrayIterator
}
else
{
foreach ( $results as $result )
foreach ( $results as $key => $result )
{
/* If an item has been removed incorrectly an orphan search index entry can be left. This is already logged in current() so let's not fail catastrophically */
if( !$result )
{
unset( $results[ $key ] );
continue;
}
$lastTime = $result->createdDate->getTimestamp();
}
}
@@ -481,6 +511,9 @@ class _Results extends \ArrayIterator
/* If an application was not updated or is not installed, do not try to fetch it or we can get a class does not exist fatal error */
$where[] = array( "follow_app IN('" . implode( "','", array_keys( \IPS\Application::enabledApplications() ) ) . "')" );
$lastClass = NULL;
$lastMember = NULL;
foreach ( \IPS\Db::i()->select( array( 'follow_app', 'follow_area', 'follow_rel_id', 'follow_member_id', 'follow_added' ), 'core_follow', $where, 'follow_added DESC', 10 ) as $follow )
{
if ( $follow['follow_app'] == 'core' and $follow['follow_area'] == 'member' )
@@ -493,66 +526,121 @@ class _Results extends \ArrayIterator
else
{
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
try
{
if ( class_exists( $class ) )
{
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
$urlToThing = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE );
/* Do we need to merge with the last result? */
if( $lastClass !== NULL AND $lastClass === $class AND $lastMember === $follow['follow_member_id'] AND count( $extraItems ) )
{
$extraItem = array_pop( $extraItems );
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $follow['follow_added'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE ) ) ) )
);
$mergeList = array_merge( array( $urlToThing ), $extraItem->getMergeData() );
$listToDisplay = $mergeList;
if( count( $mergeList ) > 3 )
{
$listToDisplay = array_slice( $mergeList, 0, 3 );
$listToDisplay[] = \IPS\Member::loggedIn()->language()->addToStack( 'and_x_others', FALSE, array( 'pluralize' => array( count( $mergeList ) - 3 ) ) );
}
$extraItem->mergeInData( \IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Member::loggedIn()->language()->formatList( $listToDisplay ) ) ) ), $urlToThing );
$extraItems[] = $extraItem;
}
else
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $follow['follow_added'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), $urlToThing ) ) ),
NULL,
$urlToThing
);
}
}
}
catch ( \OutOfRangeException $e ) { }
$lastClass = $class;
$lastMember = $follow['follow_member_id'];
}
}
}
/* Likes */
if ( \IPS\Settings::i()->reputation_enabled and \IPS\Member::loggedIn()->group['gbw_view_reps'] and ( in_array( 'like', $extra ) or in_array( 'rep_neg', $extra ) ) )
if ( \IPS\Settings::i()->reputation_enabled and \IPS\Member::loggedIn()->group['gbw_view_reps'] and ( in_array( 'like', $extra ) or in_array( 'react', $extra ) ) )
{
$where = array( array( 'rep_date>?', $lastTime ) );
$where[] = array( 'reaction_enabled=?', 1 );
if ( $firstTime )
{
$where[] = array( 'rep_date<?', $firstTime );
}
if ( !in_array( 'rep_neg', $extra ) or ( \IPS\Settings::i()->reputation_point_types != 'both' and \IPS\Settings::i()->reputation_point_types != 'negative' ) )
{
$where[] = array( 'rep_rating=1' );
}
elseif ( !in_array( 'like', $extra ) or ( \IPS\Settings::i()->reputation_point_types == 'negative' ) )
{
$where[] = array( 'rep_rating=-1' );
}
/* get only the reputation data for installed applications */
$where[] = \IPS\Db::i()->in( 'app', array_keys( \IPS\Application::applications() ) ) ;
if ( $author )
{
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 ) as $rep )
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 ) as $rep )
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
}
else
{
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 ) as $rep )
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
}
}
/* Clubs */
if ( \IPS\Settings::i()->clubs and in_array( 'clubs', $extra ) )
{
$where = array( array( 'created>?', $lastTime ) );
if ( $firstTime )
{
$where[] = array( 'created<?', $firstTime );
}
if ( $author )
{
$where[] = array( 'owner=?', $author->member_id );
}
foreach ( \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), NULL, 'created', FALSE, array(), $where ) as $club )
{
if ( $club->owner AND $club->owner->member_id )
{
$extraItems[] = new Result\Custom(
$club->created,
\IPS\Member::loggedIn()->language()->addToStack( 'activity_club', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $club->owner ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $club->url(), FALSE, $club->name, FALSE ) ) ) )
);
}
}
}
/* Extensions */
foreach ( \IPS\Application::allExtensions( 'core', 'StreamItems', TRUE, 'core' ) as $key => $extension )
{
$settingKey = "all_activity_" . mb_strtolower( $key );
if( method_exists( $extension, 'extraItems' ) and \IPS\Settings::i()->$settingKey )
{
$extraItems = array_merge( $extraItems, $extension->extraItems( $author, $lastTime, $firstTime ) );
}
}
}
/* Merge them in */
if ( !empty( $extraItems ) )
{
@@ -594,7 +682,7 @@ class _Results extends \ArrayIterator
{
foreach ( $ext->classes as $class )
{
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and $class::$reputationType == $rep['type'] )
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and $class::reactionType() == $rep['type'] )
{
$thingBeingRepped = $class::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -602,7 +690,7 @@ class _Results extends \ArrayIterator
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
if ( in_array( 'IPS\Content\Reputation', class_implements( $commentClass ) ) and $commentClass::$reputationType == $rep['type'] )
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $commentClass::reactionType() == $rep['type'] )
{
$thingBeingRepped = $commentClass::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -611,7 +699,7 @@ class _Results extends \ArrayIterator
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
if ( in_array( 'IPS\Content\Reputation', class_implements( $reviewClass ) ) and $reviewClass::$reputationType == $rep['type'] )
if ( \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) and $reviewClass::reactionType() == $rep['type'] )
{
$thingBeingRepped = $reviewClass::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -624,27 +712,31 @@ class _Results extends \ArrayIterator
throw new \OutOfRangeException;
}
if ( \IPS\Settings::i()->reputation_point_types == 'like' )
if ( \IPS\Content\Reaction::isLikeMode() )
{
$lang = 'activity_member_liked';
}
elseif ( $rep['rep_rating'] > 0 )
{
$lang = 'activity_rep_pos';
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $rep['rep_date'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_liked', FALSE, array( 'htmlsprintf' => array(
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
$thingBeingRepped->indefiniteArticle(),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
) ) )
);
}
else
{
$lang = 'activity_rep_neg';
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $rep['rep_date'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_reacted', FALSE, array( 'htmlsprintf' => array(
(string) $reaction->_icon,
$reaction->_title,
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
$thingBeingRepped->indefiniteArticle(),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
) ) )
);
}
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $rep['rep_date'] ),
\IPS\Member::loggedIn()->language()->addToStack( $lang, FALSE, array( 'htmlsprintf' => array(
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
$thingBeingRepped->indefiniteArticle(),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
) ) )
);
}
catch ( \OutOfRangeException $e ) { }
}
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Interface for Searchable Content/Comment Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Delicious share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='https://delicious.com/tools'>Delicious button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Digg share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='http://ftllc.com/articles/social-media-buttons.php'>Digg button</a> (This was the best I could find...Digg has removed most of their button documentation it seems)
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Email share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\ShareServices;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Facebook share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='https://developers.facebook.com/docs/reference/plugins/like/'>Facebook like button documentation</a>
*/
@@ -1,12 +1,11 @@
<?php
/**
* @brief Google +1 share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='https://developers.google.com/+/web/+1button/'>Google +1 button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Linked In share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='http://developer.linkedin.com/plugins/share-plugin-generator'>Linked In button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Pinterest share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 01 Dec 2016
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content\ShareServices;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Reddit share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='http://www.reddit.com/buttons/‎'>Reddit button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief StumbleUpon share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='http://www.stumbleupon.com/dt/badges/create'>StumbleUpon button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Twitter share link
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 11 Sept 2013
* @version SVN_VERSION_NUMBER
* @see <a href='https://dev.twitter.com/docs/tweet-button'>Tweet button documentation</a>
*/
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Interface for Shareable Comment Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jul 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Tags interface for Content Models
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 14 Nov 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+4 -5
View File
@@ -1,12 +1,11 @@
<?php
/**
* @brief Interface for Tracking Views
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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 8 Jan 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
+8 -15
View File
@@ -1,13 +1,12 @@
<?php
/**
* @brief Content Item Feed Widget
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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
* @subpackage forums
* @since 16 Oct 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;
@@ -151,15 +150,9 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
/* Date restrict */
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_restrict_days', isset( $this->configuration['widget_feed_restrict_days'] ) ? $this->configuration['widget_feed_restrict_days'] : -1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('widget_feed_restrict_days_suffix') ) );
/* Sort */
$sortOptions = array();
foreach ( array( 'updated', 'title', 'num_comments', 'date', 'views', 'rating_average' ) as $k )
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
$sortOptions[ $class::$databaseColumnMap[ $k ] ] = 'sort_' . $k;
}
}
/* Sort */
$sortOptions = $class::getWidgetSortOptions();
$form->add( new \IPS\Helpers\Form\Select( 'widget_feed_sort_on', isset( $this->configuration['widget_feed_sort_on'] ) ? $this->configuration['widget_feed_sort_on'] : $class::$databaseColumnMap['updated'], FALSE, array( 'options' => $sortOptions ) ), NULL, NULL, NULL, 'widget_feed_sort_on' );
$form->add( new \IPS\Helpers\Form\Select( 'widget_feed_sort_dir', isset( $this->configuration['widget_feed_sort_dir'] ) ? $this->configuration['widget_feed_sort_dir'] : 'desc', FALSE, array(
@@ -342,7 +335,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
/* As the block config can try and search all rows in topics/records, etc, we can end up with a tmp table and block nested buffer on members table, so we just query members after separately */
$items = iterator_to_array( $class::getItemsWithPermission(
$where, /* Where clause */
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( $class::$databasePrefix . $this->configuration['widget_feed_sort_on'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) : ( $class::$databasePrefix . $class::$databaseColumnMap['updated'] . ' DESC' ), /* Order */
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( ( $this->configuration['widget_feed_sort_on'] == '_rand' ) ? $this->configuration['widget_feed_sort_on'] : ( $class::$databasePrefix . $this->configuration['widget_feed_sort_on'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) ) : ( $class::$databasePrefix . $class::$databaseColumnMap['updated'] . ' DESC' ), /* Order */
isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5, /* Limit */
( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? 'read' : NULL, /* Permission key to check against */
$hidden, /* Whether or not to include hidden items */
+4 -5
View File
@@ -1,13 +1,12 @@
<?php
/**
* @brief Content Item Feed Widget
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @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
* @subpackage forums
* @since 16 Oct 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Content;