I have to work with the techcrunch wp-async-task to run a synchronization task in background in my wordpress plugin.
So to test, at the bottom of the main file I have :
//top of the php file
require_once(dirname(__FILE__) . '/lib/WP_Async_Task.php');
require_once(dirname(__FILE__) . '/class/my_api_status.class.php');
define('API_URL', '...');
/* ... */
// At the bottom of the file
function my_api_status($api_url)
{
sleep(5);
$r = wp_safe_remote_get($api_url);
if (!is_wp_error($r)) {
$body = json_decode(wp_remote_retrieve_body($r));
if (isset($body->success)) {
return;
}
}
}
add_action('wp_async_api_status', 'my_api_status');
function my_init_api_status()
{
new ApiStatusTask();
do_action('api_status', constant('API_URL'));
}
add_action('plugins_loaded', 'my_init_api_status');
And api status task class
class ApiStatusTask extends WP_Async_Task {
protected $action = 'api_status';
/**
* Prepare data for the asynchronous request
* #throws Exception If for any reason the request should not happen
* #param array $data An array of data sent to the hook
* #return array
*/
protected function prepare_data( $data ) {
return array(
'api_url' => $data[0]
);
}
/**
* Run the async task action
*/
protected function run_action() {
if(isset($_POST['api_url'])){
do_action("wp_async_$this->action", $_POST['api_url']);
}
}
}
The function prepare_data is correctly called by launchand after that launch_on_shutdown is also correctly called and finally wp_remote_post is called at the end of launch_on_shutdown with admin-post.php.
But the function run_action is never called ... and so the my_api_status in the main file.
What it possibly go wrong ?
I will put a complete example of a plugin here soon. But for now, I found my problem :
// In the `launch_on_shutdown` method of `WP_Async_Task` class
public function launch_on_shutdown() {
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown');
if ( ! empty( $this->_body_data ) ) {
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
$cookies[] = "$name=" . urlencode( is_array( $value ) ? serialize( $value ) : $value );
}
$request_args = array(
'timeout' => 0.01,
'blocking' => false,
'sslverify' => false, //apply_filters( 'https_local_ssl_verify', true ),
'body' => $this->_body_data,
'headers' => array(
'cookie' => implode( '; ', $cookies ),
),
);
$url = admin_url( 'admin-post.php' );
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown wp_remote_post');
wp_remote_post( $url, $request_args );
}
}
The sslverify option failed in my local environment. I just had to put it on false if we are not in production.
With this option set, the run_action is correctly trigger.
Related
Sorry I feel like really stuck here.
I have a plugin introducing a new Rest API controller (WP_REST_Controller) with basically a single endpoint which uses a separate class as a client to fetch some data. Let's say:
#my_plugin.php
function register_items_routes() {
if ( ! class_exists( 'WP_REST_My_Controller' ) ) {
require_once __DIR__ . '/class-wp-my-controller.php';
}
$controller = new WP_REST_My_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'register_items_routes' );
_
#class-wp-my-controller.php
class WP_REST_My_Controller extends WP_REST_Controller {
/**
* Registers the routes.
*/
public function register_routes() {
$namespace = 'my/namespace';
$path = 'get-items';
register_rest_route( $namespace, '/' . $path, [
array(
'methods' => 'GET',
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' )
),
] );
}
public function get_items_permissions_check( $request ) {
return true;
}
/**
* Get items from My_Class and return them.
*
* #param WP_REST_Request $request The incoming HTTP request.
*
* #return WP_REST_Response|WP_Error The response containing the items in JSON, WP_Error in case of error.
*/
public function get_items( $request ) {
$client = new My_Class();
try {
$items = $client->fetch_some_items();
} catch ( Exception $e ) {
return new WP_Error(
'some-client-error',
$e->getMessage()
);
// Code to be tested. - Do some stuff with items and return.
return new WP_REST_Response( $items );
}
How am I supposed to stub the My_Class dependency from PhpUnit in order to return a predefined set of items which I could test with?
public function test_get_items() {
$request = new WP_REST_Request( 'GET', '/my/namespace/get-items' );
$data = rest_get_server()->dispatch( $request );
$expected_items = [
'some_key1' => 'some_value1',
'some_key2' => 'some_value2',
];
$this->assertTrue( count($data['items']) == count($expected_items) );
}
I am testing the Clickatell API to integrate SMS confirmations in my php based app, I've used their rest api to send the message to myself to test but the messages never arrive.
My Attempts
I used this https://www.clickatell.com/developers/api-docs/get-coverage-rest/ to check the coverage and this was the JSON response:
object(stdClass)[54]
public 'data' =>
object(stdClass)[57]
public 'routable' => boolean true
public 'destination' => string ' 21655609125' (length=12)
public 'minimumCharge' => float 0.8
I've also made sure the message is actually sent by checking status; and this was the JSON response:
object(stdClass)[54]
public 'data' =>
object(stdClass)[57]
public 'charge' => float 0.8
public 'messageStatus' => string '004' (length=3)
public 'description' => string 'Received by recipient' (length=21)
public 'apiMessageId' => string 'b57f4a28dece65a134b56be2010c8a78' (length=32)
public 'clientMessageId' => string '' (length=0)
I've then tried their own website for sent messages reports and that's what I see:
MESSAGE CONTENT Thanks for testing Clickatell's gateway coverage. You
will be able to change the content of your message after your initial
purchase of message credits.
Mobile Network Tunisia:Orange To 21655609125
Received by recipient (status 4)
But I never receive the message myself. What could be the issue?
Edit: here is the full class i use in my app currently
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* MC_SMS class
*/
class MC_SMS {
public $rest_uri = 'https://api.clickatell.com/rest';
public $method = 'post';
public $args = array();
/**
* Constructor
*/
public function __construct( $action, $data = null ) {
$this->data = $data;
$this->init();
switch( $action ) :
// Send message
case 'send' :
$this->endpoint = '/message';
$this->method = 'post';
break;
// Message status
case 'status' :
$this->endpoint = '/message/' . $data;
$this->method = 'get';
break;
// Network coverage
case 'coverage' :
$this->endpoint = '/coverage/' . $data;
$this->method = 'get';
break;
// Account balance
case 'balance' :
$this->endpoint = '/account/balance';
$this->method = 'get';
break;
endswitch;
$this->queried_uri = $this->rest_uri . $this->endpoint;
$this->do_request();
$this->response = ( isset( $this->response_body['body'] ) ) ? json_decode( $this->response_body['body'] ) : null;
}
/**
* Init.
*/
public function init() {
$this->headers = array(
'X-Version' => 1,
'Authorization' => 'Bearer ClHrbIEo_LwAlSVTSMemBIA5Gmvz8HNb5sio3N9GVDdAO_PPJPaZKzdi8Y8cDSmrs4A4',
'Content-Type' => 'application/json',
'Accept' => 'application/json'
);
$this->data = ( ! empty( $this->data ) && is_array( $this->data ) ) ? json_encode( $this->data ) : null;
$this->args['headers'] = $this->headers;
if ( $this->data ) {
$this->args['body'] = $this->data;
}
}
/**
* Do the API request
*/
public function do_request() {
if ( $this->method == 'get' ) {
$this->response_body = wp_remote_get( $this->queried_uri, $this->args );
}
if ( $this->method == 'post' ) {
$this->response_body = wp_remote_post( $this->queried_uri, $this->args );
}
}
}
Before paying money and whilst using the trial credits, the message reaching you is the text:
MESSAGE CONTENT Thanks for testing Clickatell's gateway coverage. You will be able to change the content of your message after your initial purchase of message credits.
This means that you have successfully sent and received a sms to your device. Your own actual messages are seen by the recipient devices only after you have paid for credits.
As an extension to #Dimitris Magdalinos' answer which appears to be correct, the following documentation has this to say (emphasis mine):
You can begin testing the gateway using the methods laid out in the chapter "Everyday tasks". However, please note that if you are using the 10 complimentary SMS credits which came with the account, until you have purchased credits Clickatell will replace the content with thank you text like the message below:
Thanks for testing Clickatell's gateway coverage. You will be able to change the content of your message after your initial purchase of message credits.
I'm trying to implement GCM server using PHP and Zend Framework on Google App Engine. So far it works fine locally, but fails with this message when uploaded to App Engine:
Here is the code:
$ids = '....my device id....';
$apiKey = '...my api key...';
$data = array( 'message' => 'Hello World!' );
$gcm_client = new Client();
$gcm_client->setApiKey($apiKey);
$gcm_message = new Message();
$gcm_message->setTimeToLive(86400);
$gcm_message->setData($data);
$gcm_message->setRegistrationIds($ids);
$response = $gcm_client->send($gcm_message);
var_dump($response);
And it fails with this error message:
PHP Fatal error: Uncaught exception 'ErrorException' with message
'stream_socket_client(): unable to connect to
android.googleapis.com:443 (Unknown error 4294967295)' in
/base/data/home/..../backend:v1.375711862873219029/vendor/zendframework/zend-http/Zend/Http/Client/Adapter/Socket.php:253
I know App Engine doesn't allow socket connections and offers urlFetch wrapper for http and https, but how do I tell Zend Framework to use this transport?
Try enabling Billing. As far as I remember sockets are enabled only for paid apps.
This won't charge you anything (unless you exceed free quota) but should get rid of the error.
Promoted this from a comment - I ended up making my own class implementing Zend\Http\Client\Adapter\AdapterInterface that uses URLFetch by opening a URL using the usual fopen with stream context to send POST request. Although this works I'm not sure it's the best way. Would prefer to use the framework capabilities, if possible.
I'm not sure if this is going to help anyone, as both ZendFramework and AppEngine have evolved since I asked the question, but here is the adapter I've implemented:
use Zend\Http\Client\Adapter\AdapterInterface;
use Zend\Http\Client\Adapter\Exception\RuntimeException;
use Zend\Http\Client\Adapter\Exception\TimeoutException;
use Zend\Stdlib\ErrorHandler;
class URLFetchHttpAdapter implements AdapterInterface
{
protected $stream;
protected $options;
/**
* Set the configuration array for the adapter
*
* #param array $options
*/
public function setOptions($options = array())
{
$this->options = $options;
}
/**
* Connect to the remote server
*
* #param string $host
* #param int $port
* #param bool $secure
*/
public function connect($host, $port = 80, $secure = false)
{
// no connection yet - it's performed in "write" method
}
/**
* Send request to the remote server
*
* #param string $method
* #param \Zend\Uri\Uri $url
* #param string $httpVer
* #param array $headers
* #param string $body
*
* #throws \Zend\Loader\Exception\RuntimeException
* #return string Request as text
*/
public function write($method, $url, $httpVer = '1.1', $headers = array(), $body = '')
{
$headers_str = '';
foreach ($headers as $k => $v) {
if (is_string($k))
$v = ucfirst($k) . ": $v";
$headers_str .= "$v\r\n";
}
if (!is_array($this->options))
$this->options = array();
$context_arr = array("http" =>
array( "method" => $method,
"content" => $body,
"header" => $headers_str,
"protocol_version" => $httpVer,
'ignore_errors' => true,
'follow_location' => false,
) + $this->options
);
$context = stream_context_create($context_arr);
ErrorHandler::start();
$this->stream = fopen((string)$url, 'r', null, $context);
$error = ErrorHandler::stop();
if (!$this->stream) {
throw new \Zend\Loader\Exception\RuntimeException('', 0, $error);
}
}
/**
* Read response from server
*
* #throws \Zend\Http\Client\Adapter\Exception\RuntimeException
* #return string
*/
public function read()
{
if ($this->stream) {
ErrorHandler::start();
$metadata = stream_get_meta_data($this->stream);
$headers = join("\r\n", $metadata['wrapper_data']);
$contents = stream_get_contents($this->stream);
$error = ErrorHandler::stop();
if ($error)
throw $error;
$this->close();
//echo $headers."\r\n\r\n".$contents;
return $headers."\r\n\r\n".$contents;
} else {
throw new RuntimeException("No connection exists");
}
}
/**
* Close the connection to the server
*
*/
public function close()
{
if (is_resource($this->stream)) {
ErrorHandler::start();
fclose($this->stream);
ErrorHandler::stop();
$this->stream = null;
}
}
/**
* Check if the socket has timed out - if so close connection and throw
* an exception
*
* #throws TimeoutException with READ_TIMEOUT code
*/
protected function _checkSocketReadTimeout()
{
if ($this->stream) {
$info = stream_get_meta_data($this->stream);
$timedout = $info['timed_out'];
if ($timedout) {
$this->close();
throw new TimeoutException(
"Read timed out after {$this->options['timeout']} seconds",
TimeoutException::READ_TIMEOUT
);
}
}
}
}
public function sendAndroidPushNotification($registration_ids, $message)
{
$registrationIds = array($registration_ids);
$msg = array(
'message' => $message,
'title' => 'notification center',
'vibrate' => 1,
'sound' => 1
);
$fields = array(
'registration_ids' => $registrationIds,
'data' => $msg
);
$fields = json_encode($fields);
$arrContextOptions=array(
"http" => array(
"method" => "POST",
"header" =>
"Authorization: key = <YOUR_APP_KEY>". "\r\n" .
"Content-Type: application/json". "\r\n",
"content" => $fields,
),
"ssl"=>array(
"allow_self_signed"=>true,
"verify_peer"=>false,
),
);
$arrContextOptions = stream_context_create($arrContextOptions);
$result = file_get_contents('https://android.googleapis.com/gcm/send', false, $arrContextOptions);
return $result;
}
Here is my Controller:
<?php
class Check_Login {
var $CI;
var $class;
var $allowed_klasses = array('user', 'testing', 'home', 'lesson_assets', 's3_handler', 'ajax', 'api', 'pages', 'invite', 'mail', 'partner', 'renew', 'store', 'news', 'breathe','popup','subscription', 'lessons');
public function __construct() {
$this->CI =& get_instance();
if(!isset($this->CI->session)) {
$this->CI->load->library('session');
}
if(!nash_logged_in()) {
$this->CI->session->sess_destroy();
redirect('/');
}
$this->_set_accessed_klass();
}
public function auth_check() {
if($this->CI->session->userdata('id')) {
$query = $CI->db->query("SELECT authentication_token FROM users WHERE id = ".$this->CI->session->userdata('id')." AND authentication_token IS NOT NULL");
if(!in_array($this->class, $this->allowed_klasses)) {
if($query->num_rows() == 0){
redirect('/user/logout');
}
}else{
return;
}
}else{
return;
}
}
private function _set_accessed_klass() {
$this->class = $this->CI->router->fetch_class();
}
}
The lines that I am referring too are:
if(!nash_logged_in()) {
$this->CI->session->sess_destroy();
redirect('/');
}
Essentially, the app uses the nash_logged_in() method to check against our OAuth system to see if the user is truly "logged in". When this happens a redirect loop happens.
The nash_logged_in method simply returns a JSON key of either TRUE or FALSE. Any reason why I would be running into this redirect loop?
nash_logged_in method:
if(!function_exists('nash_logged_in')) {
function nash_logged_in(){
$url = NASH_OAUTH_URL . '/api/v1/loggedin.json';
$json = file_get_contents($url);
$data = json_decode($json);
return $data->loggedin;
}
}
If nash_logged_in() does not return a boolean false or integer 0 or null, then the statement is evaluated as true therefore your redirect.
Post nash_logged_in() here to see what's going on there.
You wont need to use hooks for this method
post controller hook
You could just extend CI_Controller and run the Authentication library in the __constructor of the child classes that need to be authenticated.
You current controller is a little messy and it looks like a library to me, not a controller, you don't need to re-instantiate the super object if your doing it all in your controller!
However, my suggestion is to move everything to a library(as there are a number of controllers/classes that depend on it).
Some elements of your code don't make sense to me, possibly because I can't see the bigger picture from the code you have posted.
This might give you some food for though(or not) regardless this is how I would approach it.
application/libraries/authentication.php
class Authentication
{
protected $allowedClasses = array ( ) ;
protected $userId = null ;
protected $nashURL ;
const NASH_OAUTH_URL = '' ;
public function __construct ()
{
$this->nashURL = static::NASH_OAUTH_URL . '/api/v1/loggedin.json' ;
//check for a user id in session
//this may not be set yet!!
$this->userId = (isset ( $this->session->userdata ( 'id' ) ))
? $this->session->userdata ( 'id' )
: null ;
/** Load dependancies * */
$this->load->model ( 'Authentication_Model' ) ;
$this->load->library ( 'Session' ) ;
}
/**
* nashCheckLoginViaCurl
* #return boolean
*/
protected function nashCheckLoginViaCurl ()
{
if ( function_exists ( 'curl_init' ) )
{
return show_error ( "Enabled CURL please!" , 500 ) ;
}
$curl = curl_init () ;
curl_setopt_array ( $curl ,
array (
CURLOPT_URL => $this->nashURL ,
/** CHECK CURL DOCS FOR FULL LIST OF OPTIONS - FILL THE REST YOURSELF * */
) ) ;
if ( curl_errno ( $curl ) )
{
return false ;
}
$info = curl_getinfo ( $curl ) ;
$responce = curl_exec ( $curl ) ;
curl_close ( $curl ) ;
//Check and make sure responce is a BOOLEAN and not a STRING
//we will typecast below just incase
$responce = json_decode ( $responce ) ;
return ($info[ 'http_code' ] == '200' and ( bool ) $responce->loggedin === true)
? true
: false ;
}
/**
* verifyAccess
* #param CI_Controller $class (Dependancy Injection)
* #return Mixed
*
*/
public function verifyAccess ( CI_Controller $class )
{
//Is there a userId in the session
//ie: is user logged In
if ( is_null ( $this->userId ) or ! ( int ) $this->userId )
{
return false ;
}
//grab list of allowed classes
$this->allowedClasses = $this->listAllowedClasses () ;
//check to see if $class is in list of allowed classes
if ( ! in_array ( $class , $this->allowedClasses ) )
{
return false ;
}
//check to see if nashCheckLoginViaCurl returned true
if ( ! $this->nashCheckLoginViaCurl () )
{
$this->logout () ;
return false ;
}
//return boolean or $authentication_token based on DB query
return $this->Authentication_Model->isUserIdRegistered ( $this->userId ) ;
}
/**
* logout
* #return void
*/
public function logout ()
{
$this->session->unset_userdata ( array ( 'id' => 0 ) ) ;
$this->session->sess_destroy () ;
$this->session->sess_start () ;
return redirect ( '/' ) ;
}
/**
* listAllowedClasses
* MAYBE USE A CONFIG FILE FOR THIS?
* #return array
*/
protected function listAllowedClasses ()
{
return array (
'user' , 'testing' , 'home' , 'lesson_assets' , 's3_handler' , 'ajax' ,
'api' ,
'pages' , 'invite' , 'mail' , 'partner' , 'renew' , 'store' , 'news' ,
'breathe' ,
'popup' , 'subscription' , 'lessons'
) ;
}
/**
* Load CI Super object object
*
* #param string $object
* #return object
*/
public function __get ( $object )
{
return get_instance ()->$object ;
}
}
application/models/authentication_model.php
class Authentication_Model extends CI_Model
{
public function isUserIdRegistered ( $uid )
{
$this->db->select ( 'authentication_token' )
->from ( 'users' )
->where ( 'id' , $uid )
->where ( 'authentication_token IS NOT' , 'NULL' )
->limit ( 1 ) ;
$query = $this->db->get () ;
return ( $query->num_rows () > 0 )
? $query->result ()
: FALSE ;
}
}
application/core/MY_Controller.php
class MY_Controller extends CI_Controller
{
protected $authentication_token ;
public function __construct ()
{
parent::__construct () ;
$this->load->library ( 'authentication' ) ;
}
protected function _verifyAccess ( $class )
{
$authorized = $this->authentication->verifyAccess ( strtolower ( $class ) ) ;
if ( ! $authorized )
{
//kill further script execution by returning
//redirect url
return redirect ( 'login' ) ;
}
else
{
$this->authentication_token = $authorized ;
}
return ; //return control back to the controller who called me
}
}
*Testing Different Controllers - simulate post controller hook *
class Some_Controller extends MY_Controller
{
public function __construct ()
{
parent::__construct () ;
$this->_verifyAccess ( __CLASS__ ) ;
}
}
-
class Another_Controller extends MY_Controller
{
public function __construct ()
{
parent::__construct () ;
$this->_verifyAccess ( __CLASS__ ) ;
}
}
I have a rest controller example im trying to run that is giving me a headache.
My url im trying to access is localhost/books/edit/1
For some weird reason this route seems to call the getAction with the Controller instead of the editAction. And it throws errors saying that the object doesnt exist.
The controller is,
class BooksController extends Zend_Rest_Controller {
private $_booksTable;
private $_form;
public function init() {
$bootstrap = $this->getInvokeArg ( 'bootstrap' );
$db = $bootstrap->getResource ( 'db' );
$options = $bootstrap->getOption ( 'resources' );
$dbFile = $options ['db'] ['params'] ['dbname'];
if (! file_exists ( $dbFile )) {
$createTable = "CREATE TABLE IF NOT EXISTS books (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(32) NOT NULL,
price DECIMAL(5,2) NOT NULL
)";
$db->query ( $createTable );
$insert1 = "INSERT INTO books (name, price) VALUES ('jQuery in Action', 39.99)";
$insert2 = "INSERT INTO books (name, price) VALUES ('PHP in Action', 45.99)";
$db->query ( $insert1 );
$db->query ( $insert2 );
}
$this->_booksTable = new Zend_Db_Table ( 'books' );
$this->_form = new Default_Form_Book ();
}
/**
* The index action handles index/list requests; it should respond with a
* list of the requested resources.
*/
public function indexAction() {
$this->view->books = $this->_booksTable->fetchAll ();
}
/**
* The list action is the default for the rest controller
* Forward to index
*/
public function listAction() {
$this->_forward ( 'index' );
}
/**
* The get action handles GET requests and receives an 'id' parameter; it
* should respond with the server resource state of the resource identified
* by the 'id' value.
*/
public function getAction() {
$this->view->book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
}
/**
* Show the new book form
*/
public function newAction() {
$this->view->form = $this->_form;
}
/**
* The post action handles POST requests; it should accept and digest a
* POSTed resource representation and persist the resource state.
*/
public function postAction() {
if ($this->_form->isValid ( $this->_request->getParams () )) {
$this->_booksTable->createRow ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->form = $this->_form;
$this->render ( 'new' );
}
}
/**
* Show the edit book form. Url format: /books/edit/2
*/
public function editAction() {
var_dump ($this->getRequest()->getParam ( 'edit' ));
$book = $this->_booksTable->find ( $this->getRequest()->getParam ( 'id' ) )->current ();
var_dump ($book->toArray ());
$this->_form->populate ( $book->toArray () );
$this->view->form = $this->_form;
$this->view->book = $book;
}
/**
* The put action handles PUT requests and receives an 'id' parameter; it
* should update the server resource state of the resource identified by
* the 'id' value.
*/
public function putAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
if ($this->_form->isValid ( $this->_request->getParams () )) {
$book->setFromArray ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->book = $book;
$this->view->form = $this->_form;
$this->render ( 'edit' );
}
}
/**
* The delete action handles DELETE requests and receives an 'id'
* parameter; it should update the server resource state of the resource
* identified by the 'id' value.
*/
public function deleteAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
$book->delete ();
$this->_redirect ( 'books' );
}
}
The bootstrap is,
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap {
protected function _initAutoload() {
$autoloader = new Zend_Application_Module_Autoloader ( array (
'namespace' => 'Default_',
'basePath' => dirname ( __FILE__ )
) );
return $autoloader;
}
protected function _initRestRoute() {
$this->bootstrap ( 'Request' );
$front = $this->getResource ( 'FrontController' );
$restRoute = new Zend_Rest_Route ( $front, array (), array (
'default' => array ('books' )
) );
$front->getRouter ()->addRoute ( 'rest', $restRoute );
}
protected function _initRequest() {
$this->bootstrap ( 'FrontController' );
$front = $this->getResource ( 'FrontController' );
$request = $front->getRequest ();
if (null === $front->getRequest ()) {
$request = new Zend_Controller_Request_Http ();
$front->setRequest ( $request );
}
return $request;
}
}
Can anyone see what might be causing the getAction to be called when browsing to that link ???
edit should follow the identifier, so the correct edit URL is http://localhost/books/1/edit