Version 5.0.0 beta 1

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

No files matched your search

+76 -48
View File
@@ -12,16 +12,32 @@
namespace IPS\forums\tasks;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\Content\Search\Index;
use IPS\Db;
use IPS\forums\Topic;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Settings;
use IPS\Task;
use IPS\Task\Exception;
use UnderflowException;
use function defined;
use function intval;
use function IPS\Cicloud\getForumArchiveDb;
use const IPS\CIC2;
use const IPS\DISABLE_ARCHIVING;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* archive Task
*/
class _archive extends \IPS\Task
class archive extends Task
{
/**
* @brief Number of items to process per cycle
@@ -31,7 +47,7 @@ class _archive extends \IPS\Task
/**
* @brief Storage database
*/
protected $storage;
protected Db|null $storage = null;
/**
* Execute
@@ -42,38 +58,50 @@ class _archive extends \IPS\Task
* Tasks should execute within the time of a normal HTTP request.
*
* @return mixed Message to log or NULL
* @throws \IPS\Task\Exception
* @throws Exception
*/
public function execute()
public function execute() : mixed
{
/* If archiving is disabled, disable the task */
if ( !\IPS\Settings::i()->archive_on )
if ( !Settings::i()->archive_on )
{
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 0 ), array( '`key`=?', 'archive' ) );
Db::i()->update( 'core_tasks', array( 'enabled' => 0 ), array( '`key`=?', 'archive' ) );
}
/* Otherwise, let's do this! */
else
{
if( defined( 'DISABLE_ARCHIVING') AND DISABLE_ARCHIVING )
{
$working = Db::i()->select('count(*)', 'forums_topics', [ 'topic_archive_status=?', Topic::ARCHIVE_WORKING ] )->first();
if( !$working )
{
Db::i()->update( 'core_tasks', [ 'enabled' => 0 ], [ '`key`=?', 'archive' ] );
Db::i()->update( 'forums_topics', [ 'topic_archive_status' => Topic::ARCHIVE_RESTORE ], [ 'topic_archive_status=?', Topic::ARCHIVE_DONE ] );
return NULL;
}
}
/* Init */
$postsProcessed = 0;
/* Connect to the remote DB if needed */
if ( \IPS\CIC2 )
if ( CIC2 )
{
$this->storage = \IPS\Cicloud\getForumArchiveDb();
$this->storage = getForumArchiveDb();
}
else
{
$this->storage = !\IPS\Settings::i()->archive_remote_sql_host ? \IPS\Db::i() : \IPS\Db::i( 'archive', array(
'sql_host' => \IPS\Settings::i()->archive_remote_sql_host,
'sql_user' => \IPS\Settings::i()->archive_remote_sql_user,
'sql_pass' => \IPS\Settings::i()->archive_remote_sql_pass,
'sql_database' => \IPS\Settings::i()->archive_remote_sql_database,
'sql_port' => \IPS\Settings::i()->archive_sql_port,
'sql_socket' => \IPS\Settings::i()->archive_sql_socket,
'sql_tbl_prefix'=> \IPS\Settings::i()->archive_sql_tbl_prefix,
'sql_utf8mb4' => isset( \IPS\Settings::i()->sql_utf8mb4 ) ? \IPS\Settings::i()->sql_utf8mb4 : FALSE
$this->storage = !Settings::i()->archive_remote_sql_host ? Db::i() : Db::i( 'archive', array(
'sql_host' => Settings::i()->archive_remote_sql_host,
'sql_user' => Settings::i()->archive_remote_sql_user,
'sql_pass' => Settings::i()->archive_remote_sql_pass,
'sql_database' => Settings::i()->archive_remote_sql_database,
'sql_port' => Settings::i()->archive_sql_port,
'sql_socket' => Settings::i()->archive_sql_socket,
'sql_tbl_prefix'=> Settings::i()->archive_sql_tbl_prefix,
'sql_utf8mb4' => isset( Settings::i()->sql_utf8mb4 ) ? Settings::i()->sql_utf8mb4 : FALSE
) );
}
@@ -81,12 +109,12 @@ class _archive extends \IPS\Task
try
{
/* Get the topic */
$topic = \IPS\forums\Topic::constructFromData( \IPS\Db::i()->select( '*', 'forums_topics', array( 'topic_archive_status=?', \IPS\forums\Topic::ARCHIVE_WORKING ) )->first() );
$topic = Topic::constructFromData( Db::i()->select( '*', 'forums_topics', array( 'topic_archive_status=?', Topic::ARCHIVE_WORKING ) )->first() );
/* Do as many posts as we can */
$offset = $this->storage->select( 'MAX(archive_id)', 'forums_archive_posts', array( 'archive_topic_id=?', $topic->tid ) )->first();
$total = \IPS\Db::i()->select( 'COUNT(*)', 'forums_posts', array( 'pid>? AND topic_id=?', \intval( $offset ), $topic->tid ) )->join( 'forums_topics', 'tid=topic_id' )->first();
$posts = \IPS\Db::i()->select( 'forums_posts.*,forums_topics.forum_id', 'forums_posts', array( 'pid>? AND topic_id=?', \intval( $offset ), $topic->tid ), 'pid ASC', static::PROCESS_PER_BATCH )->join( 'forums_topics', 'tid=topic_id' );
$total = Db::i()->select( 'COUNT(*)', 'forums_posts', array( 'pid>? AND topic_id=?', intval( $offset ), $topic->tid ) )->join( 'forums_topics', 'tid=topic_id' )->first();
$posts = Db::i()->select( 'forums_posts.*,forums_topics.forum_id', 'forums_posts', array( 'pid>? AND topic_id=?', intval( $offset ), $topic->tid ), 'pid ASC', static::PROCESS_PER_BATCH )->join( 'forums_topics', 'tid=topic_id' );
foreach ( $posts as $post )
{
$this->archive( $post, $post['forum_id'] );
@@ -96,28 +124,28 @@ class _archive extends \IPS\Task
/* If we did them all, mark that this topic is done and remove from search index */
if ( $postsProcessed == $total )
{
$topic->topic_archive_status = \IPS\forums\Topic::ARCHIVE_DONE;
$topic->topic_archive_status = Topic::ARCHIVE_DONE;
$topic->save();
\IPS\Db::i()->delete( 'forums_posts', array( 'topic_id=?', $topic->tid ) );
Db::i()->delete( 'forums_posts', array( 'topic_id=?', $topic->tid ) );
\IPS\Content\Search\Index::i()->removeFromSearchIndex( $topic );
Index::i()->removeFromSearchIndex( $topic );
}
}
catch ( \UnderflowException $e ) { }
catch ( UnderflowException $e ) { }
/* Can we do even more? */
$query = array_merge( array( array( 'topic_archive_status=? and approved !=?', \IPS\forums\Topic::ARCHIVE_NOT, -2 ) ), \IPS\Application::load('forums')->archiveWhere( \IPS\Db::i()->select( '*', 'forums_archive_rules' ) ) );
$query = array_merge( array( array( 'topic_archive_status=? and approved !=?', Topic::ARCHIVE_NOT, -2 ) ), Application::load('forums')->archiveWhere( iterator_to_array( Db::i()->select( '*', 'forums_archive_rules' ) ) ) );
if ( $postsProcessed < static::PROCESS_PER_BATCH )
{
/* Loop topics */
foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'forums_topics', $query, 'tid ASC', 250 ), 'IPS\forums\Topic' ) as $topic )
foreach ( new ActiveRecordIterator( Db::i()->select( '*', 'forums_topics', $query, 'tid ASC', 250 ), 'IPS\forums\Topic' ) as $topic )
{
/* First set it as working */
\IPS\Db::i()->update( 'forums_topics', array( 'topic_archive_status' => \IPS\forums\Topic::ARCHIVE_WORKING ), array( 'tid=?', $topic->tid ) );
Db::i()->update( 'forums_topics', array( 'topic_archive_status' => Topic::ARCHIVE_WORKING ), array( 'tid=?', $topic->tid ) );
/* Do as many posts as we can */
foreach ( \IPS\Db::i()->select( '*', 'forums_posts', array( 'topic_id=?', $topic->tid ), 'pid ASC', static::PROCESS_PER_BATCH - $postsProcessed ) as $post )
foreach ( Db::i()->select( '*', 'forums_posts', array( 'topic_id=?', $topic->tid ), 'pid ASC', static::PROCESS_PER_BATCH - $postsProcessed ) as $post )
{
$this->archive( $post, $topic->forum_id );
$postsProcessed++;
@@ -128,11 +156,11 @@ class _archive extends \IPS\Task
}
/* Set that it's done */
\IPS\Db::i()->update( 'forums_topics', array( 'topic_archive_status' => \IPS\forums\Topic::ARCHIVE_DONE ), array( 'tid=?', $topic->tid ) );
\IPS\Db::i()->delete( 'forums_posts', array( 'topic_id=?', $topic->tid ) );
Db::i()->update( 'forums_topics', array( 'topic_archive_status' => Topic::ARCHIVE_DONE ), array( 'tid=?', $topic->tid ) );
Db::i()->delete( 'forums_posts', array( 'topic_id=?', $topic->tid ) );
/* Remove from search index */
\IPS\Content\Search\Index::i()->removeFromSearchIndex( $topic );
Index::i()->removeFromSearchIndex( $topic );
}
}
}
@@ -143,41 +171,41 @@ class _archive extends \IPS\Task
/**
* Archive
*
* @param array $post The post to be archived
* @param int|null $forumId The forum id, if known
* @param array $post The post to be archived
* @param int|null $forumId The forum id, if known
* @return void
*/
protected function archive( $post, $forumId=NULL )
protected function archive( array $post, int $forumId=NULL ) : void
{
/* Translate the fields */
$archive = array(
'archive_id' => \intval( $post['pid'] ),
'archive_author_id' => \intval( $post['author_id'] ),
'archive_id' => intval( $post['pid'] ),
'archive_author_id' => intval( $post['author_id'] ),
'archive_author_name' => $post['author_name'],
'archive_ip_address' => $post['ip_address'],
'archive_content_date' => \intval( $post['post_date'] ),
'archive_content_date' => intval( $post['post_date'] ),
'archive_content' => $post['post'],
'archive_queued' => $post['queued'],
'archive_topic_id' => \intval( $post['topic_id'] ),
'archive_is_first' => \intval( $post['new_topic'] ),
'archive_topic_id' => intval( $post['topic_id'] ),
'archive_is_first' => intval( $post['new_topic'] ),
'archive_bwoptions' => $post['post_bwoptions'],
'archive_added' => time(),
'archive_attach_key' => $post['post_key'],
'archive_html_mode' => $post['post_htmlstate'],
'archive_show_edited_by' => \intval( $post['append_edit'] ),
'archive_edit_time' => \intval( $post['edit_time'] ),
'archive_show_edited_by' => intval( $post['append_edit'] ),
'archive_edit_time' => intval( $post['edit_time'] ),
'archive_edit_name' => (string) $post['edit_name'],
'archive_edit_reason' => $post['post_edit_reason'],
'archive_field_int' => \intval( $post['post_field_int'] ),
'archive_forum_id' => $forumId ?: \IPS\Db::i()->select( 'forum_id', 'forums_topics', array( 'tid=?', \intval( $post['topic_id'] ) ) )->first()
'archive_field_int' => intval( $post['post_field_int'] ),
'archive_forum_id' => $forumId ?: Db::i()->select( 'forum_id', 'forums_topics', array( 'tid=?', intval( $post['topic_id'] ) ) )->first()
);
/* Insert */
$this->storage->replace( 'forums_archive_posts', $archive );
/* Update reports and promote tables */
\IPS\Db::i()->update( 'core_rc_index', array( 'class' => 'IPS\forums\Topic\ArchivedPost' ), array( 'class=? AND content_id=?', 'IPS\forums\Topic\Post', $post['pid'] ) );
\IPS\Db::i()->update( 'core_social_promote', array( 'promote_class' => 'IPS\forums\Topic\ArchivedPost' ), array( 'promote_class=? AND promote_class_id=?', 'IPS\forums\Topic\Post', $post['pid'] ) );
Db::i()->update( 'core_rc_index', array( 'class' => 'IPS\forums\Topic\ArchivedPost' ), array( 'class=? AND content_id=?', 'IPS\forums\Topic\Post', $post['pid'] ) );
Db::i()->update( 'core_content_promote', array( 'promote_class' => 'IPS\forums\Topic\ArchivedPost' ), array( 'promote_class=? AND promote_class_id=?', 'IPS\forums\Topic\Post', $post['pid'] ) );
}
/**
@@ -189,7 +217,7 @@ class _archive extends \IPS\Task
*
* @return void
*/
public function cleanup()
public function cleanup() : void
{
}
@@ -0,0 +1,157 @@
<?php
/**
* @brief expertUserNudge Task
* @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 25 Jul 2023
*/
namespace IPS\forums\tasks;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Application;
use IPS\Db;
use IPS\forums\Forum;
use IPS\forums\Topic;
use IPS\Member;
use IPS\Notification;
use IPS\Platform\Bridge;
use IPS\Settings;
use IPS\Task;
use IPS\Task\Exception;
use OutOfRangeException;
use function array_keys;
use function defined;
use function iterator_to_array;
use function time;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* expertUserNudge Task
*/
class expertUserNudge extends Task
{
/**
* Execute
*
* If ran successfully, should return anything worth logging. Only log something
* worth mentioning (don't log "task ran successfully"). Return NULL (actual NULL, not '' or 0) to not log (which will be most cases).
* If an error occurs which means the task could not finish running, throw an \IPS\Task\Exception - do not log an error as a normal log.
* Tasks should execute within the time of a normal HTTP request.
*
* @return mixed Message to log or NULL
* @throws Exception
*/
public function execute() : mixed
{
if ( ! Settings::i()->forums_solved_nudge_experts or !Bridge::i()->featureIsEnabled( 'experts' ) )
{
return NULL;
}
$experts = Db::i()->select( 'member_id, node_id', 'core_expert_users', [], "", [ 0, 50 ]);
$nodeExperts = [];
foreach( $experts as $expert )
{
$nodeExperts[$expert['node_id']][] = $expert['member_id'];
}
$where = [
[ Db::i()->in( 'approved', array( -2, -3 ), TRUE ) ],
[ 'start_date > ? and start_date < ? and topic_answered_pid=0', ( time() - ( 365 * 86400) ), ( time() - ( (int) Settings::i()->forums_solved_nudge_experts * 86400 ) ) ], //@todo limit by nudges sent/date?
[ Db::i()->in( 'forum_id', iterator_to_array( Db::i()->select( 'id', 'forums_forums', '(' . Db::i()->bitwiseWhere( Forum::$bitOptions['forums_bitoptions'], 'bw_solved_set_by_member' ) . ') AND (' . Db::i()->bitwiseWhere( Forum::$bitOptions['forums_bitoptions'], 'bw_solved_set_by_moderator' ) . ')' ) ) ) ],
[ Db::i()->in( 'forum_id', array_keys( $nodeExperts ) ) ]
];
$expertsToNotify = [];
foreach( Db::i()->select( '*', 'forums_topics', $where, 'start_date ASC', [ 0, 50 ] ) as $row )
{
// Do we have an expert for this forum?
if( isset( $nodeExperts[ $row['forum_id'] ] ) )
{
foreach( $nodeExperts[ $row['forum_id'] ] as $expert )
{
// make sure we didn't start the fire
if( $expert !== $row['starter_id'] )
{
$expertsToNotify[ $expert ][] = Topic::constructFromData( $row );
}
}
}
}
foreach ( $expertsToNotify as $expertId => $topics )
{
try
{
$expert = Member::load( $expertId );
if( !$expert->members_bitoptions['expert_user_disabled'] and !$expert->members_bitoptions['expert_user_blocked'] and ( $expert->expert_nudge < time() - ( 86400 * 7 ) ) )
{
$topicIds = [];
foreach( $topics as $topic )
{
$topicIds[] = $topic->tid;
}
/* Skip any topics that we already replied to */
$replied = iterator_to_array(
Db::i()->select( 'topic_id', 'forums_posts', array(
array( Db::i()->in( 'topic_id', $topicIds ) ),
array( 'author_id=?', $expert->member_id )
) )
);
$topicIds = array_diff( $topicIds, $replied );
/* Rebuild the topic list with only the remaining topics */
$topicsToInclude = [];
foreach( $topics as $topic )
{
if( in_array( $topic->tid, $topicIds ) )
{
$topicsToInclude[] = $topic;
}
}
if( count( $topicIds ) )
{
$notification = new Notification( Application::load( 'cloud' ), 'new_topics_to_review', $expert, [ $topicsToInclude, $expert ], [ 'topics' => $topicIds ] );
$notification->recipients->attach( $expert );
$notification->send();
}
$expert->expert_nudge = time();
$expert->save();
}
}
catch ( OutOfRangeException $e ) {}
}
return NULL;
}
/**
* Cleanup
*
* If your task takes longer than 15 minutes to run, this method
* will be called before execute(). Use it to clean up anything which
* may not have been done
*
* @return void
*/
public function cleanup()
{
}
}
@@ -1,78 +0,0 @@
<?php
/**
* @brief rebuildPopularTime Task
* @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}
* @since 14 May 2024
*/
namespace IPS\forums\tasks;
/* 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;
}
/**
* rebuildPopularTime Task
*/
class _rebuildPopularTime extends \IPS\Task
{
/**
* Execute
*
* If ran successfully, should return anything worth logging. Only log something
* worth mentioning (don't log "task ran successfully"). Return NULL (actual NULL, not '' or 0) to not log (which will be most cases).
* If an error occurs which means the task could not finish running, throw an \IPS\Task\Exception - do not log an error as a normal log.
* Tasks should execute within the time of a normal HTTP request.
*
* @return mixed Message to log or NULL
* @throws \IPS\Task\Exception
*/
public function execute()
{
/* Check if popular now is enabled */
$popularNowSettings = json_decode( \IPS\Settings::i()->forums_popular_now, TRUE );
if ( ( !\IPS\CIC AND ( !$popularNowSettings['posts'] or !$popularNowSettings['minutes'] ) ) OR ( \IPS\CIC AND !$popularNowSettings['minutes'] ) )
{
return NULL;
}
$cutoff = $this->last_run ?: ( time() - ( $popularNowSettings['minutes'] * 30 ) );
$topicIds = iterator_to_array(
\IPS\Db::i()->select( 'DISTINCT topic_id', 'forums_posts', [ 'post_date > ?', $cutoff ], null, 15 )
);
if( \count( $topicIds ) )
{
foreach( new \IPS\Patterns\ActiveRecordIterator(
\IPS\Db::i()->select( '*', 'forums_topics', \IPS\Db::i()->in( 'tid', $topicIds ) ),
'IPS\forums\Topic'
) as $topic )
{
$topic->rebuildPopularTime();
}
}
return NULL;
}
/**
* Cleanup
*
* If your task takes longer than 15 minutes to run, this method
* will be called before execute(). Use it to clean up anything which
* may not have been done
*
* @return void
*/
public function cleanup()
{
}
}
@@ -12,16 +12,28 @@
namespace IPS\forums\tasks;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Db;
use IPS\Email;
use IPS\forums\Forum;
use IPS\forums\Topic;
use IPS\Member;
use IPS\Settings;
use IPS\Task;
use function count;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* solvedNotifications Task
*/
class _solvedNotifications extends \IPS\Task
class solvedNotifications extends Task
{
/**
* Execute
@@ -32,11 +44,11 @@ class _solvedNotifications extends \IPS\Task
* Tasks should execute within the time of a normal HTTP request.
*
* @return mixed Message to log or NULL
* @throws \IPS\Task\Exception
* @throws Task\Exception
*/
public function execute()
public function execute() : mixed
{
if ( ! \IPS\Settings::i()->forums_solved_topic_reengage )
if ( ! Settings::i()->forums_solved_topic_reengage )
{
return NULL;
}
@@ -45,23 +57,23 @@ class _solvedNotifications extends \IPS\Task
super inefficient with sites with millions of members, we can consider an index on members_bitoptions2 or remove that sub-query and just check the author in the code before sending
which would suck if all 50 were for members with re-enagement emails off, but we will see */
$where = [
[ \IPS\Db::i()->in( 'approved', array( -2, -3 ), TRUE ) ],
[ 'posts > 1 and start_date > ? and start_date < ? and solved_reminder_sent=0 and topic_answered_pid=0', ( time() - ( 365 * 86400) ), ( time() - ( (int) \IPS\Settings::i()->forums_solved_topic_reengage * 86400 ) ) ],
[ \IPS\Db::i()->in( 'forum_id', iterator_to_array( \IPS\Db::i()->select( 'id', 'forums_forums', '(' . \IPS\Db::i()->bitwiseWhere( \IPS\forums\Forum::$bitOptions['forums_bitoptions'], 'bw_enable_answers_member' ) . ') AND (' . \IPS\Db::i()->bitwiseWhere( \IPS\forums\Forum::$bitOptions['forums_bitoptions'], 'bw_enable_answers_moderator' ) . ')' ) ) ) ],
[ 'starter_id NOT IN (?)', \IPS\Db::i()->select( 'member_id', 'core_members', \IPS\Db::i()->bitwiseWhere( \IPS\Member::$bitOptions['members_bitoptions'], 'no_solved_reenage' ) ) ]
[ Db::i()->in( 'approved', array( -2, -3 ), TRUE ) ],
[ 'posts > 1 and start_date > ? and start_date < ? and solved_reminder_sent=0 and topic_answered_pid=0', ( time() - ( 365 * 86400) ), ( time() - ( (int) Settings::i()->forums_solved_topic_reengage * 86400 ) ) ],
[ Db::i()->in( 'forum_id', iterator_to_array( Db::i()->select( 'id', 'forums_forums', '(' . Db::i()->bitwiseWhere( Forum::$bitOptions['forums_bitoptions'], 'bw_solved_set_by_member' ) . ') AND (' . Db::i()->bitwiseWhere( Forum::$bitOptions['forums_bitoptions'], 'bw_solved_set_by_moderator' ) . ')' ) ) ) ],
[ 'starter_id NOT IN (?)', Db::i()->select( 'member_id', 'core_members', Db::i()->bitwiseWhere( Member::$bitOptions['members_bitoptions'], 'no_solved_reenage' ) ) ]
];
$topics = [];
foreach( \IPS\Db::i()->select( '*', 'forums_topics', $where, 'start_date ASC', [ 0, 50 ] ) as $row )
foreach( Db::i()->select( '*', 'forums_topics', $where, 'start_date ASC', [ 0, 50 ] ) as $row )
{
$topics[ $row['tid'] ] = $row;
}
$starters = [];
if ( \count( $topics ) )
if ( count( $topics ) )
{
/* Get a count of how many reminders have been sent already */
foreach( \IPS\Db::i()->select( 'starter_id, COUNT(*) as count', 'forums_topics', [
foreach( Db::i()->select( 'starter_id, COUNT(*) as count', 'forums_topics', [
[ 'solved_reminder_sent > ?', time() - ( 86400 * 7 ) ]
], NULL, NULL, 'starter_id' ) as $row ) {
$starters[ $row['starter_id'] ] = $row['count'];
@@ -80,19 +92,19 @@ class _solvedNotifications extends \IPS\Task
We don't want to send this one as the starter has had too many sent, so reset time to 5 days ago so a task can check again on this one in a few days
There is hardcoded to 5 max a week. We could consider a setting but I feel that 5 max per week isn't overwhelming but is enough to prompt re-engagement
*/
\IPS\Db::i()->update( 'forums_topics', [ 'solved_reminder_sent' => time() - ( 86400 * 5 ) ], ['tid=?', $row['tid']] );
Db::i()->update( 'forums_topics', [ 'solved_reminder_sent' => time() - ( 86400 * 5 ) ], ['tid=?', $row['tid']] );
}
else
{
/* Avoid race conditions and make sure its updated even if constructFromData fails */
\IPS\Db::i()->update( 'forums_topics', [ 'solved_reminder_sent' => time() ], ['tid=?', $row['tid']] );
Db::i()->update( 'forums_topics', [ 'solved_reminder_sent' => time() ], ['tid=?', $row['tid']] );
/* Increment so the next foreach will count it correctly */
$starters[ $row['starter_id'] ]++;
try
{
$topic = \IPS\forums\Topic::constructFromData( $row );
$topic = Topic::constructFromData( $row );
/* Don't send for converted live topics */
if ( $topic->getLiveTopic() )
@@ -110,10 +122,10 @@ class _solvedNotifications extends \IPS\Task
continue;
}
\IPS\Email::buildFromTemplate( 'core', 'solved_reengagement', array($topic, $firstPost, $replies), \IPS\Email::TYPE_TRANSACTIONAL )->send( $topic->author() );
Email::buildFromTemplate( 'core', 'solved_reengagement', array($topic, $firstPost, $replies), Email::TYPE_TRANSACTIONAL )->send( $topic->author() );
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
}
}
@@ -12,16 +12,27 @@
namespace IPS\forums\tasks;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use DateTimeZone;
use IPS\DateTime;
use IPS\Db;
use IPS\forums\Forum;
use IPS\Settings;
use IPS\Task;
use IPS\Task\Exception;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* solvedStatsAggregation Task
*/
class _solvedStatsAggregation extends \IPS\Task
class solvedStatsAggregation extends Task
{
/**
* Aggregate yesterday's (server time 0:00:00 to 23:59:59) solve stats per forum. Depending when the task runs, it is possible for it to run multiple
@@ -37,31 +48,31 @@ class _solvedStatsAggregation extends \IPS\Task
* time: timestamp of the start of the day (so 0:00:00)
*
* @return mixed Message to log or NULL
* @throws \IPS\Task\Exception
* @throws Exception
*/
public function execute()
public function execute() : mixed
{
$timezone = new \DateTimeZone( \IPS\Settings::i()->reputation_timezone );
$end = \IPS\DateTime::create()->setTimezone( $timezone )->sub( new \DateInterval( 'P1D' ) )->setTime( 23, 59 );
$timezone = new DateTimeZone( Settings::i()->reputation_timezone );
$end = DateTime::create()->setTimezone( $timezone )->sub( new DateInterval( 'P1D' ) )->setTime( 23, 59 );
/* Now to iterate over all forums that have solved enabled and crunch the statistics */
foreach( \IPS\Db::i()->select( '*', 'forums_forums', [ '(' . \IPS\Db::i()->bitwiseWhere( \IPS\forums\Forum::$bitOptions['forums_bitoptions'], 'bw_enable_answers' ) . ') OR ( ' . \IPS\Db::i()->bitwiseWhere( \IPS\forums\Forum::$bitOptions['forums_bitoptions'], 'bw_enable_answers_moderator' ) . ' )' ] ) as $forum )
foreach( Db::i()->select( '*', 'forums_forums', [ '( ' . Db::i()->bitwiseWhere( Forum::$bitOptions['forums_bitoptions'], 'bw_solved_set_by_moderator' ) . ' )' ] ) as $forum )
{
$start = \IPS\DateTime::ts( \IPS\forums\Forum::constructFromData( $forum )->getFirstSolvedTime() )->setTimezone( $timezone );
$start = DateTime::ts( Forum::constructFromData( $forum )->getFirstSolvedTime() )->setTimezone( $timezone );
$where = [
[ \IPS\Db::i()->in( 'state', array( 'link', 'merged' ), TRUE ) ],
[ \IPS\Db::i()->in( 'approved', array( -2, -3 ), TRUE ) ],
[ Db::i()->in( 'state', array( 'link', 'merged' ), TRUE ) ],
[ Db::i()->in( 'approved', array( -2, -3 ), TRUE ) ],
[ 'forum_id=?', $forum['id'] ],
[ 'start_date > ? AND start_date < ?', $start->getTimestamp(), $end->getTimestamp() ]
];
$total = \IPS\Db::i()->select( 'COUNT(*)', 'forums_topics', $where )->first();
$solved = \IPS\Db::i()->select( 'COUNT(*)', 'forums_topics', array_merge( $where, [ [ 'core_solved_index.id IS NOT NULL' ] ] ) )->join( 'core_solved_index', "core_solved_index.app='forums' AND core_solved_index.item_id=forums_topics.tid")->first();
$avg = \IPS\Db::i()->select( 'AVG(CAST(core_solved_index.solved_date AS SIGNED)-forums_topics.start_date)', 'forums_topics', array_merge( $where, [ [ 'core_solved_index.id IS NOT NULL' ] ] ) )->join( 'core_solved_index', "core_solved_index.app='forums' AND core_solved_index.item_id=forums_topics.tid")->first();
$total = Db::i()->select( 'COUNT(*)', 'forums_topics', $where )->first();
$solved = Db::i()->select( 'COUNT(*)', 'forums_topics', array_merge( $where, [ [ 'core_solved_index.id IS NOT NULL AND type=?', 'solved' ] ] ) )->join( 'core_solved_index', "core_solved_index.app='forums' AND core_solved_index.item_id=forums_topics.tid")->first();
$avg = Db::i()->select( 'AVG(CAST(core_solved_index.solved_date AS SIGNED)-forums_topics.start_date)', 'forums_topics', array_merge( $where, [ [ 'core_solved_index.id IS NOT NULL AND type=?', 'solved' ] ] ) )->join( 'core_solved_index', "core_solved_index.app='forums' AND core_solved_index.item_id=forums_topics.tid")->first();
\IPS\Db::i()->delete( 'core_statistics', [ 'type=? and time=? and value_1=?', 'solved', $end->getTimestamp(), $forum['id'] ] );
\IPS\Db::i()->insert( 'core_statistics', [
Db::i()->delete( 'core_statistics', [ 'type=? and time=? and value_1=?', 'solved', $end->getTimestamp(), $forum['id'] ] );
Db::i()->insert( 'core_statistics', [
'type' => 'solved',
'value_1' => $forum['id'],
'value_2' => $total,
+54 -38
View File
@@ -12,16 +12,30 @@
namespace IPS\forums\tasks;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Content\Search\Index;
use IPS\Db;
use IPS\forums\Topic;
use IPS\forums\Topic\Post;
use IPS\Settings;
use IPS\Task;
use IPS\Task\Exception;
use UnderflowException;
use function defined;
use function intval;
use function IPS\Cicloud\getForumArchiveDb;
use const IPS\CIC2;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* unarchive Task
*/
class _unarchive extends \IPS\Task
class unarchive extends Task
{
/**
* @brief Number of items to process per cycle
@@ -31,7 +45,7 @@ class _unarchive extends \IPS\Task
/**
* @brief Storage database
*/
protected $storage;
protected mixed $storage = NULL;
/**
* Execute
@@ -42,36 +56,36 @@ class _unarchive extends \IPS\Task
* Tasks should execute within the time of a normal HTTP request.
*
* @return mixed Message to log or NULL
* @throws \IPS\Task\Exception
* @throws Exception
*/
public function execute()
public function execute() : mixed
{
/* If we're disabled, disable the task, but only if we don't have things that need restoring */
$count = \IPS\Db::i()->select( 'COUNT(*)', 'forums_topics', array( 'topic_archive_status=?', \IPS\forums\Topic::ARCHIVE_RESTORE ) )->first();
$count = Db::i()->select( 'COUNT(*)', 'forums_topics', array( 'topic_archive_status=?', Topic::ARCHIVE_RESTORE ) )->first();
if ( !\IPS\Settings::i()->archive_on AND !$count )
if ( !Settings::i()->archive_on AND !$count )
{
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 0 ), array( '`key`=?', 'unarchive' ) );
Db::i()->update( 'core_tasks', array( 'enabled' => 0 ), array( '`key`=?', 'unarchive' ) );
return NULL;
}
/* Connect to the remote DB if needed */
if ( \IPS\CIC2 )
if ( CIC2 )
{
$this->storage = \IPS\Cicloud\getForumArchiveDb();
$this->storage = getForumArchiveDb();
}
else
{
$this->storage = !\IPS\Settings::i()->archive_remote_sql_host ? \IPS\Db::i() : \IPS\Db::i( 'archive', array(
'sql_host' => \IPS\Settings::i()->archive_remote_sql_host,
'sql_user' => \IPS\Settings::i()->archive_remote_sql_user,
'sql_pass' => \IPS\Settings::i()->archive_remote_sql_pass,
'sql_database' => \IPS\Settings::i()->archive_remote_sql_database,
'sql_port' => \IPS\Settings::i()->archive_sql_port,
'sql_socket' => \IPS\Settings::i()->archive_sql_socket,
'sql_tbl_prefix'=> \IPS\Settings::i()->archive_sql_tbl_prefix,
'sql_utf8mb4' => isset( \IPS\Settings::i()->sql_utf8mb4 ) ? \IPS\Settings::i()->sql_utf8mb4 : FALSE
$this->storage = !Settings::i()->archive_remote_sql_host ? Db::i() : Db::i( 'archive', array(
'sql_host' => Settings::i()->archive_remote_sql_host,
'sql_user' => Settings::i()->archive_remote_sql_user,
'sql_pass' => Settings::i()->archive_remote_sql_pass,
'sql_database' => Settings::i()->archive_remote_sql_database,
'sql_port' => Settings::i()->archive_sql_port,
'sql_socket' => Settings::i()->archive_sql_socket,
'sql_tbl_prefix'=> Settings::i()->archive_sql_tbl_prefix,
'sql_utf8mb4' => isset( Settings::i()->sql_utf8mb4 ) ? Settings::i()->sql_utf8mb4 : FALSE
) );
}
@@ -82,16 +96,16 @@ class _unarchive extends \IPS\Task
/* Loop topics */
try
{
$topic = \IPS\forums\Topic::constructFromData( \IPS\Db::i()->select( '*', 'forums_topics', array( 'topic_archive_status=?', \IPS\forums\Topic::ARCHIVE_RESTORE ), 'tid ASC', 1 )->first() );
$topic = Topic::constructFromData( Db::i()->select( '*', 'forums_topics', array( 'topic_archive_status=?', Topic::ARCHIVE_RESTORE ), 'tid ASC', 1 )->first() );
}
catch( \UnderflowException $e )
catch( UnderflowException $e )
{
return FALSE;
}
/* Do as many posts as we can */
$offset = \IPS\Db::i()->select( 'MAX(pid)', 'forums_posts', array( 'topic_id=?', $topic->tid ) )->first();
foreach ( $this->storage->select( '*', 'forums_archive_posts', array( 'archive_id>? AND archive_topic_id=?', \intval( $offset ), $topic->tid ), 'archive_id ASC', static::PROCESS_PER_BATCH - $postsProcessed ) as $post )
$offset = Db::i()->select( 'MAX(pid)', 'forums_posts', array( 'topic_id=?', $topic->tid ) )->first();
foreach ( $this->storage->select( '*', 'forums_archive_posts', array( 'archive_id>? AND archive_topic_id=?', intval( $offset ), $topic->tid ), 'archive_id ASC', static::PROCESS_PER_BATCH - $postsProcessed ) as $post )
{
$unarchivedPost = $this->unarchive( $post );
$postsProcessed++;
@@ -102,33 +116,35 @@ class _unarchive extends \IPS\Task
}
/* Set that it's done */
\IPS\Content\Search\Index::i()->indexSingleItem( $topic );
\IPS\Db::i()->update( 'forums_topics', array( 'topic_archive_status' => \IPS\forums\Topic::ARCHIVE_EXCLUDE ), array( 'tid=?', $topic->tid ) );
Index::i()->indexSingleItem( $topic );
Db::i()->update( 'forums_topics', array( 'topic_archive_status' => Topic::ARCHIVE_EXCLUDE ), array( 'tid=?', $topic->tid ) );
$this->storage->delete( 'forums_archive_posts', array( 'archive_topic_id=?', $topic->tid ) );
return TRUE;
});
return true;
}
/**
* Unarchive
*
* @param array $post The post to be unarchived
* @return \IPS\forums\Topic\Post
* @param array $post The post to be unarchived
* @return Post
*/
protected function unarchive( $post )
protected function unarchive( array $post ): Post
{
/* Translate the fields */
$row = array(
'pid' => \intval( $post['archive_id'] ),
'author_id' => \intval( $post['archive_author_id'] ),
'pid' => intval( $post['archive_id'] ),
'author_id' => intval( $post['archive_author_id'] ),
'author_name' => $post['archive_author_name'],
'ip_address' => $post['archive_ip_address'],
'post_date' => \intval( $post['archive_content_date'] ),
'post_date' => intval( $post['archive_content_date'] ),
'post' => $post['archive_content'],
'queued' => $post['archive_queued'],
'topic_id' => \intval( $post['archive_topic_id'] ),
'new_topic' => \intval( $post['archive_is_first'] ),
'topic_id' => intval( $post['archive_topic_id'] ),
'new_topic' => intval( $post['archive_is_first'] ),
'post_bwoptions' => $post['archive_bwoptions'],
'post_key' => $post['archive_attach_key'],
'post_htmlstate' => $post['archive_html_mode'],
@@ -140,13 +156,13 @@ class _unarchive extends \IPS\Task
);
/* Insert */
\IPS\Db::i()->replace( 'forums_posts', $row );
Db::i()->replace( 'forums_posts', $row );
/* Update reports and promotes tables */
\IPS\Db::i()->update( 'core_rc_index', array( 'class' => 'IPS\forums\Topic\Post' ), array( 'class=? AND content_id=?', 'IPS\forums\Topic\ArchivedPost', $post['archive_id'] ) );
\IPS\Db::i()->update( 'core_social_promote', array( 'promote_class' => 'IPS\forums\Topic\Post' ), array( "promote_class=? AND promote_class_id=?", 'IPS\forums\Topic\ArchivedPost', $post['archive_id'] ) );
Db::i()->update( 'core_rc_index', array( 'class' => 'IPS\forums\Topic\Post' ), array( 'class=? AND content_id=?', 'IPS\forums\Topic\ArchivedPost', $post['archive_id'] ) );
Db::i()->update( 'core_content_promote', array( 'promote_class' => 'IPS\forums\Topic\Post' ), array( "promote_class=? AND promote_class_id=?", 'IPS\forums\Topic\ArchivedPost', $post['archive_id'] ) );
return \IPS\forums\Topic\Post::constructFromData( $row );
return Post::constructFromData( $row );
}
/**