Jump to content

bmacnaughton

Members
  • Posts

    148
  • Joined

  • Last visited

Everything posted by bmacnaughton

  1. That's getting there - if I were in control of $sanitizer then I'd be set. But I'm creating a page via API so my code looks like (and sorry, I should have included this): $p = new Page(); $p->template = 'template-name'; $p->name = "$prod--$variation"; So I don't have control over calling $sanitizer - PW is doing that internally.
  2. That doesn't actually change the page name though - I already have what I want in $name, right?
  3. I'm trying to create a pagename (for internal use, not for presentation to the users) with multiple dashes in the middle. Here's an excerpt of my code: $p = new Page(); $p->template = 'template-name'; $p->name = "$prod--$variation"; ProcessWire seems to apply $sanitizer->pageName($name, true) which converts multiple dashes to a single dash so the page name doesn't have "--" in it. How can I force it to have multiple dashes?
  4. Thanks. I hadn't seen those functions. Any idea why it doesn't return a language-specific name by default? The URL is language-specific by default, so $page->localUrl() isn't really needed as I can get that value just by using $page->url.
  5. We have a three language web site - 'de', 'en', 'fr' with 'de' being the default. There are pages for which we want to use the name (it's unique within its parent-scope) as in the attachment. But when I fetch the name it does not return the name for the language - it always returns the name for the default language (guertel, in this case). I've tried $page->getLanguageValue($lang, 'name'), but name is not a "real" field, so it returns null. I can hack around it by extracting the last segment of the URL associated with that page (which is language sensitive) but it seems like I must be missing something. Is there some way to fetch $page->name for the $user->language setting?
  6. This is probably really simple but I am unable to get the two letter code for the default language. $languages->default gives me a numeric code (the page ID for that language). $user->language give me the numeric code again $user->languages->name gives me "default" if it is the default language, else the two-letter code for the language. But I don't see the two letter code for the language in $config (I might be missing it) nor anywhere else. I've hardcoded around this now, so the question is largely academic, but I'd prefer not to hardcode it. Being able to get the two character code, and not 'default' for the default language would be equally helpful.
  7. OK, thanks. I guess I'm exploring a little bit. PW makes localization pretty straightforward; this will be a bit more challenge.
  8. Same as @LostKobrakai I don't follow. Lost suggest both vue-i18n (which I've looked at) and sending text string via AJAX. Do you use one or both approaches?
  9. I had situations come up that just seemed like AJAX was the right way to handle interactions with the ProcessWire server - pages with an element like a button or link that should cause an action to occur but shouldn't require a form or actually following a link - it should just take the action and only update the toggle (a checkbox in this case) when the interaction is completed. Another use case is with a large page on which there are multiple possible interactions. When the page is heavy enough that redrawing results in a less than optimal user experience then it's nice to be able to submit a form without having to redraw the page in order to update the relevant parts. So with that preamble, here's what I put together. I was going to try to clean it up a bit but that has prevented me from posting this so I figured it's better to post it and clean it up if there is any interest. You'll see references to the namespace whale - the name of our project - that would ultimately be removed. There are two major components - the PHP side and the client-side. On the PHP side there are two functional areas: 1. "wrapping" an entity to be inserted into the HTML on a page Wrapping (the function 'makeContainer()' puts a predefined wrapper around one of three types of objects: FormBuilderForm, InputfieldForm, or Template). The wrapper provides context and attaches classes that allows the client JavaScript code to find the wrapper and figure out what to do with it. // // define a function that makes a "form" of a single button. // function makeButton ($label) { // get the form $form = wire('modules')->get("InputfieldForm"); $form->attr('action', './'); $form->attr('method', 'post'); $submit = wire('modules')->get("InputfieldSubmit"); $submit->attr('id+name', 'submit'); $submit->attr('value', $label); $form->add($submit); return $form; } // wrapper function to set label on submit button function requestUserDeleteList() { return makeButton('Do it!'); } // // makeContainer wraps the rendered InputfieldForm in HTML so the client JavaScript can recognize it and handle // AJAX interactions with the server. // It returns the InputfieldForm object and the HTML to be inserted into the page. Note that makeContainer // is in a different namespace so it requires the function name must be qualified with the \ProcessWire prefix. // list ($form, $deleteUsersHTML) = ajax\Request::makeContainer('do-something', '\ProcessWire\requestUserDeleteList'); 2. helping with the processing of an AJAX request that is submitted to the page. Helping with the AJAX request - the code is invoked on page load and determines where there is a valid AJAX request from something it wrapped. It also allows messages to be returned, classes to be added or removed from specific elements, redirects to be executed, or even wholesale replacement of DOM elements (with plenty of caveats). It will even update a submit key so it is possible for the client to execute a single transaction multiple times. // get a new request object for the AJAX transaction $request = new ajax\Request(); // if it isn't formatted correctly handle the error if (!$request->isValidCall()) { return $request->echoError(); } // get the data and function-specific contents (Whale Ajax Context) $data = $request->data(); $wac = wireDecodeJSON($data['wac']); // if ($request->id('wants-newsletter')) { if (!ajax\Request::hasCorrectProperties($data, ['wac', 'value'])) { return $request->echoError(__('invalid call')); } // implement function here } else if ($request->id('another-function')) { // implement function here } // it didn't match any of the AJAX IDs implemented return $request->echoError('not implemented'); The client code requires jQuery and is packaged as three separate functions because both the form and template processing share a common core set of functions. My original intent was to only load the form or non-form code as needed but they're small enough that it really doesn't matter. See attachments for the Request class and the client code. There are many helper functions. Here is a kind of an unfocused extract that illustrates using the code with more context (from an internal sandbox page): <?php namespace ProcessWire; using whale\ajax; // include server-side code for making forms and processing them require_once './utility/ajaxform.inc'; // custom version of ProcessWire/wire/core/WireFileTools.php render() that returns the // template object, not the rendered HTML require_once './utility/get-file-template.inc'; // START AJAX submitted form processing - decodes the request and stores results in $aaform. $aaform = new ajax\Request(); // // this page handles multiple ajax calls so I check to see if it is valid once and then check IDs. // It's also possible to use $aaform->isValidCall('get-user-delete-list') to check specifically // for a specific AJAX ID. The ID is the name provided to Request::makeContainer() when the object // is wrapped. It's also possible to make calls to $aaform->id('get-user-delete-list') to check // for a specific ID. // // to create the forms/input elements that are submitted via AJAX start with: // Request::makeContainer('unique-name-on-page', object) // unique-name-on-page will become the ID of the element that wraps your object. // object - one of ProcessWire\InputfieldForm, \FormBuilderForm, ProcessWire\Template. // if ($aaform->isValidCall()) { if ($aaform->id() === 'get-user-delete-list') { $form = requestUserDeleteList(); // process using the form. the Request object will check to make sure it's the right type. if (!$aaform->process($form)) { return $aaform->echoError(); } // build new form with usernames for selections to delete. the function getUsersToDelete() // returns a user count and a function that will make the form that includes the users in // a list of checkboxes. list($usercount, $formmaker) = getUsersToDelete(); // this returns a replacement to part of the existing DOM. There are limitations but it // handles adding a form or replacing an existing form. if ($usercount === 0) { $replacement = '<div id="ajax-place">No users to delete</div>'; } else { // we pass the $formmaker function to makeContainer(). It returns the form and the // rendered wrapper and form. list($xform, $xhtml) = ajax\Request::makeContainer('do-delete', $formmaker); $replacement = '<div id="ajax-place">' . $xhtml . '</div>'; } // this makes sure the return is formatted so the client can handle it correctly. in // this case a replacement in the DOM is being returned. The first argument is the // selector, the second is the HTML to replace the selected element with. return $aaform->echoReplacement('#ajax-place', $replacement); } else if ($aaform->id() === 'do-delete') { list($usercount, $formmaker) = getUsersToDelete(); // process using the form returned by $formmaker. this will check to make sure it's // the right type of form. This abstracts FormBuilder forms and InputfieldForms. if (!$aaform->process($formmaker())) { return $aaform->echoError(); } // a bunch of logic where the checked users are deleted $deleted = []; $failed = []; $data = $aaform->data(); foreach($data as $name => $value) { if ($name === $value) { $user = wire('users')->get("name=$name"); $email = $user->email; // delete the user and try to get it again to see if the delete worked wire('users')->delete($user); $u = wire('users')->get("name=$name"); if (!count($u)) { $deleted[] = $email . " ($name)"; } else { $failed[] = $email . " ($name)"; } } } $deleted_users = $failed_deletions = ''; if ($deleted) { $deleted_users = 'deleted:<br/>' . join($deleted, '<br/>') . '<br/>'; } if ($failed) { $failed_deletions = 'failed to delete:<br/>' . join($failed, '<br/>') . '<br/>'; } $replacement = '<div id="ajax-place">' . $deleted_users . $failed_deletions . '</div>'; return $aaform->echoReplacement('#ajax-place', $replacement); } else if ($aaform->id() === 'contact') { // here a FormBuilderForm is being loaded if (!$aaform->process($forms->load('contact'))) { return $aaform->echoError(); } // this sends a notice back. the client will place it in a predefined notice area. // Request::makeContainer() will create an area for notices (or you can supply one). // It is also possible to return errors; notices and errors get different classes. $msg = ajax\Request::makeNotice('bruce says hi'); return $aaform->echoSuccess($msg); } else { // it was a valid form but it doesn't match any ID that this page knows about. return $aaform->echoError('what is this?'); } } // normal processing to render the initial page follows as it was not a valid AJAX post // that is handled by Request(). That's a lot of code, so I won't post anymore. If people have interest I'm happy to explain or provide other bits of code, like the extracted get-file-template.inc function. Wrapping a template is similar to wrapping a form except that only certain HTML elements are tracked and each are sent to the server when they are clicked on (technically it varies). It handles radio buttons, checkboxes, links, and buttons (radios and checkboxes on "change" and links and buttons on "click"). So when a checkbox is checked an AJAX call will be made so it can be acted upon by the server. @microcipcip, @ryan, @valan (sorry to any if this isn't interesting to you - I did a quick scan of what looked like semi-related AJAX posts). ajaxform.inc ajaxclient.js
  10. I'm starting a project using vuejs and ProcessWire. I'm running into a number of questions and am hoping someone has gone down this path before. 1. I'm not sure which way to go: create a vuejs single-page-app or just create components and serve them from PW. 2. Vuejs components naturally tend to contain UI text - how do you handle about localization in that context. I like PW's localization capabilities and would prefer not to have two different systems. Is there some way to have components load all their user-visible text at runtime? I guess PW could create Vue.js apps that pass all the props to a given component when it is invoked, but that seems a bit messy. 3. Should I think about server-side-rendering or serving webpack units to the client? 4. For development, I am using webpack-dev-server and proxy API calls to another server (initially express as a mock but it will be PW) in webpack.config.js. Does this make sense? As you can tell, I'm just getting started on this so any help is appreciated.
  11. Did you ever post the vuejs + PW starter kit?
  12. Thanks - sanitizing input is important. But it's still not clear to me whether PW sanitizes database query input or not. I think it does because otherwise every user would have to do so with every field that is stored. But most uses are sanitizing for specific types of data - email, field, page-name, etc. So PW does sanitize DB queries, yes?
  13. Does this mean that PW sanitizes all database queries on its own? And that means unless I'm implementing my own $db->query() logic that I don't need to sanitize to prevent this kind of lesson:
  14. The short answer is I don't know where the internals of the DB are documented. The long answer? Read on. I found that Repeaters were a bit odd compared with other compound (indirect, recursive, whatever you call them) fields. Most "data" fields in ProcessWire are JSON but repeaters are just a list of page numbers. If you add '[' + data + ']' you can JSON decode it as an array. In JavaScript I ended up doing this: data.split(',').map(text => parseInt(text)); for the same result. It's definitely overkill for what you're doing, but I wrote a deep ProcessWire-specific DB comparison program (in JavaScript) that handles standard field types other than Matrix (because we don't use that). It also handles the Padloper field types that we have implemented so far. If you're comfortable with JavaScript it is available here: https://github.com/bmacnaughton/pw-compare. My goal, when I'm not working on more immediate concerns, is to start outputting LostKobrakai's Migrations for each of the noted differences. The module you'd want to look at is `comparison/pagefields.js`
  15. OK, I think I know what the problem is. SessionCSRF.hasValidToken() doesn't use the argument passed to $form->processInput() to get the CSRF token. It references $input->post no matter what was passed to processInput(). And with an AJAX request $input->post is empty; it seems like it should use the argument passed to $form->processInput(). (Actually, it checks for extended HTTP headers when it's an AJAX request but if they aren't present it then directly references $input->post.) The solution is to set an HTTP header X-tokenname where tokenname is the CSRF token name and the value for the header is the CSRF token value. It's odd the $form->processInput($arg) does take field values from $arg but it doesn't take the token from $arg and instead directly references $input->post.
  16. I am implementing the ability to handle form submission using AJAX. The problem I have is that even though, as far as I can tell, I convert the AJAX-submitted JSON input into the equivalent of $input->post. When I call $form->processInput() it always throws that it appears to be forged. if ($config->ajax && $_SERVER['REQUEST_METHOD'] === 'POST') { // get the file body and decode the JSON. try { $body = file_get_contents('php://input'); $json = wireDecodeJSON($body); //wire('log')->save('info', 'ajax-json:' . print_r($json, true)); wire('log')->save('info', 'json-data:' . print_r($json['data'], true) . ' sid: ' . session_id()); $fakeinput = new WireInputData($json['data']); $form = deleteRequest('xyzzy'); $form->processInput($fakeinput); } catch (Exception $e) { http_response_code(404); echo json_encode(array('reason' => $e->getMessage())); } return; The log shows that the TOKEN and the TOKEN value are the same as when a normal form is submitted (I have both on the page for testing and can submit via normal POST as well as via AJAX). The session_id() value is the same. What am I missing? Log entries ("dumb" is the normal submit button name, "fake-text" is an empty text field, "submit" is the AJAX submit button name.) (using form-post): post Array ( [dumb] => dumb-button [TOKEN1649939534X1479234443] => f4VLZ17RlXfp9KVCQr/GIhoZ3krbuWK5 ) (using ajax-post): json-data: Array ( [fake-text] => [submit] => DELETE ENDUSERS [TOKEN1649939534X1479234443] => f4VLZ17RlXfp9KVCQr/GIhoZ3krbuWK5 )
  17. Messages don't seem to show up in forms. If I set $form->message() it doesn't show up anywhere in the rendered HTML. But if I set $field->error() that shows up. I'd like to set information messages associated with a form. What am I missing?
  18. This is a good start. Let me look over it and see if I have further questions. Slightly more specifically, I'm trying to do two things - one immediate, one longer term. Immediately, I'm trying to figure out what setting $form->message() does. It's not clear to me what happens when that call is executed. Longer term, I'm trying to become familiar with the (mostly) undocumented form capabilities.
  19. I'm looking for the module(s) that contain the code used to define and process forms. I don't see anything in the dist/wire/* directories that has 'form' in the name. Does anyone know where this code is?
  20. Horst, the problem was in our code. I imagined it was more complicated than it was (multiple people making changes sometimes leads to surprises). I very much appreciate your comment about adding the "sort=-timestamp" clause to the selector. I hadn't done that before.
  21. No, I hadn't tried that but it does the trick. Apparently the initial declaration (where I get the PageArray) is used by the `./layouts/xyzzy.php` file and, I'm guessing the call to sort() causes some copy operation to occur. I would appreciate it if someone (Ryan?) could weigh in on this. It's odd to me because the `./layouts/xyzzy.php' file is appended (by being included via 'main.inc') to './xyzzy.php'. That is after the sort occurred and no new variable is created. Thanks for the shortcut that avoids the issue though.
  22. I have a situation where a pagearray appears to lose its sort order. 1. template xyzzy.php fetches a pagearray with code similar to: // in an external include file function get_pages($selector) { $found = wire('pages')->find($selector); wire('log')->save('fubar', count($found)); return $found; } // in the template xyzzy $pa = get_pages($some_selector); $pa->sort('-timestamp'); 2. main.inc is appended to the template. in main.inc an additional file is included, './layouts/xyzzy.php' 3. './layouts/xyzzy.php' then references $pa as such: <? foreach($pa as $p): ?> // do stuff with $p // log for debugging wire('log')->save('xyzzy', $p->name); <? endforeach ?> 4. The $pa that './xyzzy.php' sees is sorted while the $pa that './layouts/xyzzy.php' sees is NOT sorted. It seems like the PageArray/WireArray got copied at some point but the './layouts/xyzzy.php' file shouldn't have been included until 'main.inc' is appended. What am I missing?
  23. I don't use IDs for the reasons you mention. I don't think I made my concern very clear. Are there examples somewhere of using something like 'new Template()' and how to fill in fields and fieldgroups in order to create a new template via API? Ditto for modules. It's only by doing things via API (or admin tool) that I don't have to worry about the IDs. E.g., If I use DB primitives to copy specific pages or delete and add a field or page it will have different IDs in development and production. Having different IDs is OK if all the changes are made via PW's API. But I don't see the API for creating a template or configuring a module defined anywhere. It's the internal book-keeping I'm worried about, not our own code. I already subscribe to not using hardcoded IDs.
×
×
  • Create New...