How do I create an add-on that allows a user to click a link and call php code?

If I understand correctly, I should create the following folder and file:

library/Andy/ChangeDate/Model/Post.php

and queries should be contained in this file?
 
Your listener then looks like this:
PHP:
class Andy_ChangeDate_Listener
{
   public static function Post($class, array &$extend)
   {
     $extend[] = 'Andy_ChangeDate_ControllerPublic_Post';
   }

   public static function loadClassModel($class, array &$extend)
   {
     $extend[] = 'Andy_ChangeDate_Model_Post';
   }
}
 
I tried that, but I still get the same error.

PHP:
$db->fetchAllKeyed('
SELECT post_id
        FROM xf_post
        WHERE thread_id= ?
        ORDER BY post_date ASC
', 'post_id', $thread_id);

Question, what does the ? mark do? I see that in the Xenforo code but never understood it.

The variable(s) (in this case $thread_id) are securely swapped in in place of the ?

By simply using string concatenation in your code, you've introduced SQL injection vulnerabilities.
 
If I understand correctly, I should create the following folder and file:

library/Andy/ChangeDate/Model/Post.php

and queries should be contained in this file?
Yes, I also updated my post #120.

It is highly possible, that you won't even have to use any query for your addon, as you can use all build-in queries from xenForo like recalculatePostPositionsInThread($threadId);

You write the new date with the DataWriter as I wrote you in the conversation and here in the thread. Then you update the thread and post tables with the functions available from XenForo :)

If you just add your own queries, you can just add a file and start it with "class ... extends XenForo_Model". But if you want to extend a model, so you can use its functions by $this->...., then you have to create a listener to it like listen with the hint to XenForo_Model_Post, so you can use all its functions like this:
PHP:
$this->recalculatePostPositionsInThread($threadId);
 
The variable(s) (in this case $thread_id) are securely swapped in in place of the ?

By simply using string concatenation in your code, you've introduced SQL injection vulnerabilities.
I have access to Andys addon and I think Andys code is good. The post id is fetched with XenForo_Input::UINT and the $threadId is fetched from xenForos database. I think this is secure.
 
Using the methods Luke mentions reduces even the possibility and is recommended by the software.
 
Hi Marcus,

Thank you so much for the detailed instructions you provided in post #120 and #122. I have now extended the code event listener for the class model.

It's so cool to be able to actually understand what extending a class is and making listeners.
 
At this point all my queries are still in the following file:

/library/Andy/ChangeDate/ControllerPublic/Post.php

What is the first step to moving the queries to the following file:

/library/Andy/ChangeDate/Model/Post.php

Here's an image of my folder and file structure.

pic001.webp
 
Maybe you can even work without extending the Xenforo Post model.

In your ControllerPublic class you can put this function, so you have directly access to XenForo_Model_Post within your file:

PHP:
class Andy.......
{

.... here are your action classes ...

public function actionChangedatesave()
{
.....
$postModel = $this->_getPostModel();
$postModel->recalculatePostPositionsInThread($threadId);
...
}

  protected function _getPostModel()
   {
     return $this->getModelFromCache('XenForo_Model_Post');
   }
}

You can also write
PHP:
$this->_getPostModel()->recalculatePostPositionsInThread($threadId);
... but if you want to do more with the Post Model, it's more convenient to have it in $postModel
 
Last edited:
I have access to Andys addon and I think Andys code is good. The post id is fetched with XenForo_Input::UINT and the $threadId is fetched from xenForos database. I think this is secure.

For example the first or last poster in a thread could have a username containing an SQL injection string

It's certainly not an easily exploitable vulnerability, but it is still a vulnerability
 
Also you might want to consider just directly extending xenforo_model_post rather than going through the class proxy system - it's a toss up between whether you want to incorporate any extra changes other addons apply to it, or whether you want to avoid interfering with other addons.
 
  • Like
Reactions: Dan
Actually I guess the best method is this:

library/XenForo/DataWriter/Discussion/Thread.php
PHP:
/**
    * Rebuilds the discussion info.
    *
    * @return boolean True if still valid
    */
   public function rebuildDiscussion()

Not only is the thread data rebuild (the last post info ...), also the post data is rebuild, too :D

I do not know how you can have access to this function. Maybe with

PHP:
   $dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');

     $dw->setExistingData('thread_id', $threadId);
     $dw->rebuildDiscussion();

     $dw->save();

I would definitely go that route.
 
Last edited:
OK, this code is basically then your addon logic :D

PHP:
$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
$dw->setExistingData('post_id', $postId);
$dw->set('date', $newDate);
$dw->save();

// fetching thread_id from the DataWriter
$threadId = $dw->get('thread_id');

$dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$dw->setExistingData('thread_id', $threadId);
$dw->rebuildDiscussion();

// Maybe it works even without this:
$dw->save();
 
Very impressive, Marcus.

So if I understand correctly, the code in post #133 should be able to replace all my queries which currently reside in:

/library/Andy/ChangeDate/ControllerPublic/Post.php

At this point I have no idea where to place the code you wrote or how to modify the Listener.php file. I also assume I need to create another Code Event Listener and remove some of the existing ones.
 
You just use the old listener from this morning that worked. You do not need the other loadClassModel function for a Model Listener. This code is just replacing your functions in the library/Andy/Changedate/ControllerPublic/Post.php:

PHP:
public action Changedatesave()
{
$this->_assertPostOnly();
     
     // if you are not a super admin, you will get an error
     if (!XenForo_Visitor::getInstance()->isSuperAdmin())
     {
       return;
     }
     
     $postId= $this->_input->filterSingle('post_id', XenForo_Input::UINT);


......


$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
$dw->setExistingData('post_id', $postId);
$dw->set('date', $newDate);$dw->save();

// fetching thread_id from the DataWriter
$threadId = $dw->get('thread_id');
$dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$dw->setExistingData('thread_id', $threadId);
$dw->rebuildDiscussion();

// Maybe it works even without this:
$dw->save();

    $post['post_id'] = $postId;
        
     return $this->responseRedirect(
     XenForo_ControllerResponse_Redirect::SUCCESS,
     XenForo_Link::buildPublicLink('posts', $post),'Post Date Changed');

}
I did not want to write too much of your own code here in public, so there are .... in the code.
 
You always access the DataWriter in the Controller, here in ControllerPublic. It's easy to use the DataWriter. Usually you use it to write new stuff to the database. You can also use it to modify or add existing stuff (the setExistingData function that is used in both DataWriters here displays that). And I guess one can also use the DataWriter to use its internal functions like rebuildDiscussion(); I have never done that before, but I think it might work :)

If that really works, it may be a good first addon you created and you will see the MVC approach has its advantages :)
 
Hi Marcus,

I put back Listener.php as it was before and removed the code event listener from the code event listener editor.

I then updated Post.php it now looks like this:

PHP:
<?php

class Andy_ChangeDate_ControllerPublic_Post extends XFCP_Andy_ChangeDate_ControllerPublic_Post
{
	public function actionChangedate()
	{
		$post['post_id'] = $this->_input->filterSingle('post_id', XenForo_Input::UINT);
		
		$viewParams = array('post' => $post,
		);
		
		if (XenForo_Visitor::getInstance()->isSuperAdmin())
		{
			return $this->responseView('Andy_DateChange_ViewPublic_Post','andy_changedate',$viewParams);
		} 
	}
	
	public function actionChangedatesave()
	{
		$this->_assertPostOnly();
		
		// if you are not a super admin, you will get an error
		if (!XenForo_Visitor::getInstance()->isSuperAdmin())
		{
			return;
		}
		
		$postId= $this->_input->filterSingle('post_id', XenForo_Input::UINT); 
		
		// get input text from template
		$newPostDate = $this->_input->filterSingle('new_post_date', XenForo_Input::STRING);
		
		if ($newPostDate == '')
		{
			return $this->responseError(new XenForo_Phrase('date_missing'));
		}
		
		// get input text from template
		$newPostTime = $this->_input->filterSingle('new_post_time', XenForo_Input::STRING);
		
		if ($newPostTime == '')
		{
			return $this->responseError(new XenForo_Phrase('time_missing'));
		}		
		
		// convert to unix timestamp
		date_default_timezone_set('America/Los_Angeles');
		$date = $newPostDate . ' ' . $newPostTime;
		$newDate = strtotime($date);
		
		if ($newDate == '')
		{
			return $this->responseError(new XenForo_Phrase('date_or_time_format_incorrect'));
		}
		
		//########################################
		// start database operations
		//########################################

		$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
		$dw->setExistingData('post_id', $postId);
		$dw->set('date', $newDate);
		$dw->save();
		
		// fetching thread_id from the DataWriter
		$threadId = $dw->get('thread_id');
		$dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
		$dw->setExistingData('thread_id', $threadId);
		$dw->rebuildDiscussion();
		
		// Maybe it works even without this:
		$dw->save();
		
		$post['post_id'] = $postId;
		
		return $this->responseRedirect(
		XenForo_ControllerResponse_Redirect::SUCCESS,
		XenForo_Link::buildPublicLink('posts', $post),'Post Date Changed');		
	}
}

?>

However when I try to change a post date I get the following error message:

pic001.webp
 
Listener.php

PHP:
<?php

class Andy_ChangeDate_Listener
{
	public static function Post($class, array &$extend)
	{
		$extend[] = 'Andy_ChangeDate_ControllerPublic_Post';
	}
}

?>
 
The data field is not "date", the table field is "post_date":
PHP:
$dw->set('post_date', $newDate);

Does it work now?
 
Last edited:
Top Bottom