AppApi icon indicating copy to clipboard operation
AppApi copied to clipboard

Multi Language and ProCache do not work

Open thomasaull opened this issue 4 years ago • 10 comments

The module uses a before ProcessPageView::execute to provide an endpoint for the API. However this hook is executed before the Mutli-Language and Cache Modules, so these do not work properly (Multi-Language fields for example would return the default language in every case ❓). There is a bit more of a description of the problem in this post: https://processwire.com/talk/topic/20006-module-restapi/page/3/?tab=comments#comment-190070

Possible solutions:

  • Use ProcessPageView::pageNotFound Hook
  • The module creates a page in the page-tree which handles the routing to the api endpoint (the RestAPI module did this in very early versions, the logic for creating/deleting on install/uninstall could potentially be pulled from there)

thomasaull avatar Aug 13 '20 13:08 thomasaull

+1 LoginRegisterPro doesn't work either. I submitted a bug in the LoginRegisterPro-support-forum. Ryan states: " I don't have experience with the module you mentioned, so can't speak to what potential conflicts there might be between LoginRegisterPro and that module. But the error you indicated points to LoginRegisterPro being loaded before PW's API has reached a ready() state. The "property of non-object" is referring to the $page API variable, meaning ready() state has not yet occurred in PW, so PW is still booting up. Likely it's still in the init() state — that's too early to load modules and execute any code that uses the PW API, because the PW API is not yet fully present at that stage in the boot process. It's kind of like trying to start a car while it's in gear, it's not going to be able to. I think you can likely solve this just by executing in, or anytime after, the ready() state. The ready state literally means 'API ready' to use."

spoetnik avatar Aug 17 '20 17:08 spoetnik

I have just uploaded version 1.0.4 that makes use of ProcessPageView::pageNotFound. I like this way instead of creating a page in the page-tree, because in doing so AppApi remains independent of the page tree.

In my tests everything looks normal. @thomasaull @spoetnik , I would be happy if you could give me some feedback if the module now works for your purposes as well. And of course, you both are mentioned as contributors to this new version. Thanks for your help 🥇

Sebiworld avatar Aug 17 '20 21:08 Sebiworld

@thomasaull In my current project I managed to get multi-language to work by passing the language-id (if not default) as a parameter with every request. In my handler-class I only had to set the language from the param to processwire:

$lang = wire('input')->get->pageName('lang');
if (!empty($lang) && wire('languages')->get($lang)) {
    wire('user')->language = wire('languages')->get($lang);
} else {
    wire('user')->language = wire('languages')->getDefault();
}

With this, every requested multi-language field returns the requested language. But to be honest, the project is not finished yet and I cannot guarantee if this solution covers every edge-case. Let me know, how you would do it!

Sebiworld avatar Aug 17 '20 21:08 Sebiworld

Sorry for the late reply. Converted my side-project to AppApi. An almost drop-in replacement.

Too bad the issue with LoginRegisterPro still exists.

In the LoginRegisterPro module a:

if($page->id === $pageId) {
  // we have the page we want and config value is correct			
}

gives an error: "Trying to get property 'id' of non-object",

The $page-object is not available.

spoetnik avatar Sep 15 '20 14:09 spoetnik

If I want to output the contents of a specific page, I must manually find the requested page (based on GET-Params or path) and do $page->render(); . Only in the template-files of this page will $page->id be available. Before that, we have no page (you could make api-outputs manually without rendering a page).

I am not familiar with LoginRegisterPro. How do you integrate it? On which requests does it throw this error? Did you write something in your Routes.php to trigger it with an api call?

Sebiworld avatar Sep 26 '20 10:09 Sebiworld

This is how I integrate LoginRegisterPro

   /**
   * Create a new registration and send confirmation email on success
   * 
   * A LoginRegisterException is thrown on all error conditions.
   *
   * @param string $email Email address for new account
   * @param string $pass Password for new account
   * @param array $data Assoc array of any other fields to populate in new account
   * @return bool Returns true on success, throws LoginRegisterException on error
   * @throws LoginRegisterException
   * 
   */
  public static function registerUser($data) {

    AppApiHelper::checkAndSanitizeRequiredParameters($data, ['email|email', 'pass|string']);

    $email = $data->email;
    $pass = $data->pass;

    $loginRegister = wire('modules')->get('LoginRegisterPro');
    $codes = $loginRegister->codes(); // LoginRegisterProCodes
    $register = $loginRegister->form('register'); // LoginRegisterProRegister
    $email = wire('sanitizer')->email($email);
    $userName = $email ? $loginRegister->emailToName($email) : '';
    $password = new Password();
    $form = $register->form();
    $emailInput = $form->getChildByName('register_email'); // InputfieldEmail
    $passInput = $form->getChildByName('register_pass'); // InputfieldPassword
    $password->pass = $pass;
    $emailInput->val($email);
    $passInput->val($pass);
    $error = '';

    // check for errors
    if(empty($email) || empty($userName)) {
      $error = 'Invalid email';
    } else if(empty($pass)) {
      $error = 'Password required';
    } else if(!$this->loginRegister->allowUserEmail($email)) {
      $error = 'Email already in use';
    } else if(!$this->loginRegister->allowUserName($userName, $error)) {
      $error = 'Registration error - ' . $error; 
    } else if(!$passInput->isValidPassword($pass)) {
      $error = 'Password error - ' . implode(', ', $passInput->getErrors(true));
    } else if(!$this->allowRegistration($form)) {
      $error = 'Registration disallowed';
    }
    
    // throw exception if any errors detected
    if(strlen($error)) throw new LoginRegisterException($error);
    
    // values that we'll be saving with registration data
    $values = array(
      'register_name' => $userName,
      'register_email' => $email, 
      'register_pass' => [ $password->hash, $password->salt ],
    );
    
    // add values from $data array, adding "register_" prefix on each
    foreach($data as $key => $value) {
      if(strpos($key, 'register_') !== 0) $key = "register_$key";
      if(empty($values[$key])) $values[$key] = $value;
    }

    // setup confirmation code
    do {
      $code = $codes->create($loginRegister->codeLength, $loginRegister->codeMethod);
    } while(!$codes->add($code, $email, LoginRegisterProCodes::typeRegister, $values));

    // send confirmation email
    $register->_callMethod('sendConfirmationEmail', [ $email, $code ]); 
    $register->logSuccess('New registration', $email);
    
    return true;
  }

It was posted in the LoginRegisterPro support forum thread.

spoetnik avatar Sep 30 '20 07:09 spoetnik

@Sebiworld I just found another problem resulting in the usage of the hook, when trying to delete a page. Considering the following examples:

This uses a bootstrapped instance of ProcessWire, ✔️ works:

<?php namespace ProcessWire;

include("./index.php"); 

$pageToDelete = wire('pages')->get(9834);
echo $pageToDelete->deletable();

This uses an endpoint in AppApi: ❌️ doesn't work:

<?php namespace ProcessWire;

class Test {
  public static function test($data) {
    $pageToDelete = wire('pages')->get(9834);
    return $pageToDelete->deletable();
  }
}

and throws the following error:

{
  "error": "Internal Server Error",
  "devmessage": {
    "message": "Trying to get property 'id' of non-object",
    "location": "\/home\/thomasaull\/websites\/coperion-extranet\/wire\/core\/PagesEditor.php",
    "line": 296
  }
}

The line which causes this problem in PagesEditor is the following:

if($page->id === $this->wire('page')->id && $this->wire('config')->installedAfter('2019-04-04')) {

in particluar the part where $this->wire('page')->id tries to access the currently visited page. The problem, this variable is not set, because this would happen in the pageNotFound method, which the AppApi intercepts beforehand. And that's also the reason the code does work with a bootstrapped instance of ProcessWire ($page is the 404 page in this case).

I know I could just create a PR to processwire to add an additional check to the if-statement:

if(isset($this->wire('page')->id) && $page->id === $this->wire('page')->id && $this->wire('config')->installedAfter('2019-04-04')) {

But I don't feel this is the way to go since ProcessWire seems to assume the $page variable is set basically all the time with the current hook method in AppApi there might be other/similar problems in other places.

I do like the solution of providing the API endpoint with a setting in the module better, but meanwhile I guess it would in fact be better to handle the endpoint with a dedicated page. There does not seem to be any hook currently available which works…

Problems I know of are:

  • Deletion of pages throws an error
  • Multi-language does not work out of the box
  • ProCache only works with a workaround
  • LoginRegisterPro does not work according to @spoetnik

I'm not sure if it's worth it to hold on to the settings method for longer considering all these cases above.

What do you think?

thomasaull avatar Oct 02 '20 15:10 thomasaull

I found a potential workaround for the delete issue, when changing the code of handleApiRequest in AppApi.module.php to this, it seems to be working:

public function handleApiRequest(HookEvent $event) {
    if ($this->checkIfApiRequest()) {
        // Save url, this will be changed by page creation below
        $url = wire('input')->url;
        
        $page = new Page();
        $page->id = wire('config')->http404PageID;
        $page->name = 'api';
        $page->template = new Template();
        $this->wire('page', $page);
        
        // Reset url
        wire('input')->url = $url;

        // trigger ready in ProcessPageView
        $processPageView = $event->object;
        $processPageView->ready();
        
        $this->apiCall = true;
        Auth::getInstance()->initApikey();
        $router = new Router();
        $router->go();
        $event->replace = true;
    }
}

Basically it creates a new page on the fly and sets it as the default $page variable. It still feels a bit hacky, but if it's resolving all the issues I mentioned above it's probably a good alternative… To fire the ready in ProcessPageView is not necessary for my problem, but it's done in the pageNotFound function, so I thought I'd include it here aswell.

@spoetnik Can you maybe try if this resolves your issue aswell? Just temporarily change the code to test it.

thomasaull avatar Oct 02 '20 16:10 thomasaull

any updates on ([Multi Language) support ?

mustafa-online avatar Feb 18 '22 13:02 mustafa-online

@mustafa-online I recently got my AppApiPage module to work smoothly with multi language urls. Perhaps this will also satisfy your requirements?

It was a bit complicated because ProcessWires LanguageSupport functions only trigger on real urls. It does not set the needed language automatically because our api-page is only a 404 page with no linked language. But i figured out how to get the target language from the requested path and set it. Alternatively I included setting the language via a "lang" GET-param.

Sebiworld avatar Apr 30 '22 22:04 Sebiworld