Jump to content

millipedia

Members
  • Posts

    81
  • Joined

  • Last visited

  • Days Won

    2

Posts posted by millipedia

  1. I had to do this just the other week. We had an old CMSMS site that we'd moved to PW.

    We had a db table with the old user names and passwords in which we stuck on the new site, and then just created our own login page which first checked to see if we had the user in PW already. If the user isn't already in PW then check the password against the old table; If it matches then use that information to add a new user to PW. The users don't really notice that anything has changed.

    Here's the code I used for that - you'll have to adapt to your circumstances of course but it worked well for our move.

    if ($input->post('ml_name')) {
    
        // when processing form (POST request), check to see if token is present
    	// (we have a CSRF field on our login form)
    
        if ($session->CSRF->hasValidToken()) {
            // form submission is valid
            // okay to process
        } else {
            // form submission is NOT valid
            throw new WireException('CSRF check failed!');
        }
    
        $ml_name = $sanitizer->name($input->post('ml_name'));
        $ml_pass = $input->post('ml_pass');
    
    	// dont allow the guest username
    	if ($ml_name === 'guest') {
             throw new WireException('Invalid username');
        };
    
    
        // do we have this user in PW already?
        $u_exists = $users->get("$ml_name");
    
        if ($u_exists->id) {
    
            // user exists
            // lets try and log them in
            try {
    
                if ($session->login($ml_name, $ml_pass)) {
                    $session->redirect('/');
                } else {
                    $ml_feedback = 'Unknown details.';
                }
            } catch (WireException $e) {
                // show some error messages:
                // $e->getMessage();
            }
        } else {
    
            // ok - well do we have this user in the old CMS table?
            $query = $this->database->prepare("SELECT *
            FROM `cms_module_feusers_users` WHERE username = :login_name LIMIT 1;");
    
            try {
                $query->execute(['login_name' => $ml_name]);
            } catch (PDOException $e) {
                die ('Error querying table : ' . $e->getMessage());
            }
    
                // we've got a user from the old table
                if($query && $row=$query->fetch()) {
    
                    $ml_feedback='Is in old table';
    
                    $hash=$row['password'];
    
                    // handily the old auth just uses password_verify
                    if(password_verify($ml_pass, $hash)){
    
                        // Add this user to the PW users
                        $new_user=$users->add($ml_name);
    
                        if($new_user){
    
                            $new_user->pass=$ml_pass;
                            $new_user->addRole('members');
    
                            $new_user->save();
    
                            $log->save("new_members_site", $ml_name . " added as user");
    
                            $u = $session->login($ml_name, $ml_pass);
                            if($u) {
                                $session->redirect('/');
                            } else {
                                die("Error in logging in. Please contact admin.");
                            }
                            
                            $ml_feedback='new user added to PW';
                        }else{
    
                            $ml_feedback='Unable to add new user. Please let admin know';
                        }
    
                    }else{
                        $ml_feedback='No matching records found.';
                    }
                }else{
                    $ml_feedback='No record found.';
                }
    
        }
    }

    and this is the login form that the we had on our new login page - this and the above was all in a single template.

      <form id="ml_login_form" class="ml_login_form" method="POST">
                <?php
                echo $session->CSRF->renderInput();
                ?>
    
                <label for="ml_name">Username</label>
                <input id="ml_name" name="ml_name" type="text" value="<?= $ml_name ?>" required>
                <label for="ml_pass">Password</label>
                <input id="ml_pass" name="ml_pass" type="password" value="<?= $ml_pass ?>" required>
                <div style="display: none;">
                    <label for="ml_pass_bear">Password</label>
                    <input id="ml_pass_bear" name="ml_pass_bear" type="text" value="">
                </div>
                <button type="submit" name="ml_submit" class="butt">Submit</button>
                <div class="ml_feedback"><?= $ml_feedback ?></div>
            </form>

     

    • Like 4
  2. Depending on your requirements, the Stripe payment processor for FormBuilder might be worth investigating.

    You can set Paypal to be a payment option in Stripe so that users can pay using PayPal but you can keep all your transactions on the same plaform.

    Out of the box FormBuilder might not do everything you need (if you need a basket etc) but it is possible to hook into the module to update line items / prices etc.

    We recently built a basket system for a site that we'd wired directly into Stripe but then shifted over to hook into Formbuilder instead because it already did a lot of the heavy lifting in using the Stripe API.

    • Like 2
  3. So, today I learned that you can use a config-dev.php file which takes precedence over the normal config.php file if it's present.

    ... unfortunately I discovered this by accidentally uploading a config-dev file to a live site. I'd got two config files for live and staging in my project folder and just renamed them when I wanted to update a setting - I'd coincidently named the file for the staging details as config-dev.php and accidently uploaded it to production. Luckily this just meant that the content from the staging site got displayed which wasn't too out of date so hopefully no one noticed....

    Now I look into it, I can see that's it been around for ever and there's been lots of chat about it, but hey, I didn't know about it, so thought I'd stick it in this thread just in case anyone else was a daft as me.

     

    • Like 4
  4. Obviously you should try and educate your client into the advantages of building in PW (and in the difficulties of designing a pixel perfect site that works across all browser and devices) but if they really insist on being able to fiddle then WebFlow is probably the kind of thing they want. Not a PW solution, but it does work well once you get the hang of it. We lost a client of 20 years to it last year 😞

     

    • Like 3
    • Thanks 1
  5. One thing that has improved the speed at which I can mark up a design from Figma is the VS Code extension.

    I can open a design in a VS Code tab, and then not only see the the properties for an element but also have them appear as auto-complete suggestions as I type. Given that mostly I'm just grabbing one or two properties for an element that's really useful.

    figma_vs_code.thumb.png.656fe3d8f11e7a5e0f2471c578f58d37.png

    The same goes for assets; I can copy SVG elements directly to paste into my markup or export them straight into my project folder.

    If you use Figma and VS Code then it's definitely worth checking out.

    • Like 1
    • Thanks 1
  6. We're building a Capacitor app that grabs data from the OpenAI API via ProcessWire (it's a fun project - I'll write it up once it's done).

    For successful requests we're adding a CORS header:

        header('Access-Control-Allow-Origin: *');
        header('Content-Type: application/json');

    and that's all good.

    Occasionally though we'll get a timeout form OpenAI (or I've done something daft in the code) and PW will throw a 500 error.

    Because that response doesn't have the correct headers then I can't catch the error in the app.

    I've tired adding the header into

    wire()->addHookBefore('Page::errorPage', function($event) {

    but I'm not convinced that 500 errors even get there.

    Suggestions as to how I can a header gratefully received....

     

  7. On 7/22/2023 at 4:42 PM, bernhard said:

    Sounds great. How does the placeholder look like? Or how do we define it? Do you have any helpful screenshots? 🙂 

    What about GDPR? How would that work with this module? Or is that what you mean by "only load if interacted with"?

    The placeholder background images are pulled from YouTube / Vimeo, so whatever thumnail you set there will be what gets shown. Then a pretend play button that gets added which defaults to the Vimeo blue / YT red buttons:

    default_video_styles.thumb.jpg.a6c4e290d31a42beca076609818e4137.jpg

    When you click the button the placholder is replaced with the relevant iframed video.

    The module has a config option that will let you change the colour of that button.

    It's not too much effort to style the play button if you want to make it bespoke (as we did for https://osborne.coop/ ).

    Now - GDPR is a good question. All we're doing when the page loads is pulling an image in. Neither Privacy Badger nor uBlock are complaining about anything when we load an image from YouTube, but Privacy Badger does flag a cookie from Vimeo (uBlock doesn't mind). We use the no-cookie domain for YouTube as well of course.

    It doesn't look as if lite-youtube or lite-vimeo offer the option to use a local placeholder image but it's probably doable. Not enrirely sure how we'd configure it in a TextFormatter tho.

     

    • Thanks 1
  8. As requested by @fuzenco (quite a while ago... sorry for being slow), I'm just creating a separate thread for our TextFormatterLiteVideoEmbed module rather than hijacking the Lite YouTube Embed Module thread.

    TextFormatterLiteVideoEmbed is a text formatter that replaces YouTube and Vimeo urls in text and textarea fields with custom web components that only load the video iframe when they've been interacted with. This is a million (approximately) times faster than loading the video when the page loads, especially if you have multiple videos on a page. You can check Paul Irish's original YouTube version to see what it's all about.

    There are config options to set the button colours for the components, and one day I'll get round to adding all the other parameters we ought to have. I've used it on several live sites at the moment and is our go-to module for embedding videos now so hopefully will keep adding to it but I'm happy to take requests if anyone needs anything specific adding.

    It was originally based on the TextformatterLiteYouTubeEmbed module so props to @jacmaesfor creating that.

    The code is available on Github at https://github.com/millipedia/TextformatterLiteVideoEmbed and I'll add it to the modules repository once I've tidied up the code (it's getting there slowly).

     

    • Like 5
    • Thanks 1
  9. Damn - ninja'd by @slkwrm but I was just saying the same thing:

     

    There's been a lot chat in this forum recently about htmx so I'm giving it a go on a dashboard webapp we're currently building and it's been working very well indeed.

    In our case we've built a module where we create URL hooks that handle ajax requests from htmx

    // initialize the hook in your AutoLoad module
    public function init()
    {
    
    	wire()->addHook('/api/log-activity', function($event) {
    		include('ajax_log_activity.php');
    	});
    
    }

    Then it's just a case of marking up your button or form to post whatever you need to that url:

    <form id="log_form" hx-post="/api/log-activity" hx-target="#log_form_bits">
    
    <!-- or a button -->
    
    <button class="butt butt_secondary" hx-post="/api/log-activity-delete/<?=$some_id?>" hx-target="#your_target" hx-confirm="Are you sure you wish to delete this log entry?">Delete</button>
  10. We look after a lot of non-profit sites including half a dozen schools and they're mostly hosted on Digital Ocean instances managed by Cloudways.

    Their cheapest instance is currently $11 a month and that's what we normally start and which provides good performance for most cases (there are some additional per Gb backup charges but they're normally very minimal). You can scale up instances if you need to.

    One thing we find particularly useful is being able to 'clone' sites so we keep a skeleton PW site up to date and can just replicate that to kick off a new project (no, all our sites do NOT look the same...). You can also clone a site to a staging site and then push changes from that to the live site.

    We generally stick sites behind a free Cloudfare account as well (because honestly why wouldn't you), and Cloudways also have a deal with Cloudflare where you get the enterprise level for $5 a month which seems great if you need it (we rarely do).

    We also use https://krystal.uk/ for clients who need emails - that's more of a traditional CPanel setup but works well for us.

    • Like 2
  11. 52 minutes ago, Gideon So said:

     One thing though, line 13, 14 of TextformatterLiteVideoEmbed.js should be modified as the following. Otherwise it doesn't work in pages other than the home page.

    const LiteYTEmbed = await import(tflve_script_path + '/lite-youtube.js');
    const LiteVimeoEmbed = await import(tflve_script_path + '/lite-vimeo.js');

    Hmm -  it works for me on several sites at different page depths - which webserver are you using? But you're right that I don't really think we need that leading period, and taking it out seems to  be fine, so I've made those changes as suggested.  Thanks for letting me know.

    • Like 1
  12. Slightly embarrassed to mention it given the code is currently 90% todo statements, but I did expand this module for a project I did a few months back to include Vimeo.

    You can grab the code from

    https://github.com/millipedia/TextformatterLiteVideoEmbed

    and you can see a test page here

    https://millipedia.com/video-test/

    It definitely needs a lot of tidying up, but I have it running on a couple of live sites at the moment and it hasn't destroyed them yet (ymmv).

    I'll try and find some time to polish up the code as soon as I can.

    • Like 1
  13. 1 hour ago, gebeer said:

    At the time of import of new pages you should use the same sanitizer that you use later for the exist check and assign the page name manually when creating. That should give you consistency. 

    That's such a sensible idea. I only wish I'd thought of it 3 hours ago...

    I'd still kind of like to know what the default page add is doing, but this absolutely works. Thanks for your help.

     

  14. I'm building an import tool for sucking in data from a Google spreadsheet. I'm creating pages from each row and that's all working fine.

    When I'm running the import I want to check we haven't already created a page with this title, so I'm taking the title field of the data, converting it into what I expect the name of the new page to be and then seeing it we already have a page with that name.

    This works fine most of the time, but when we have a long title the values I get from sanitizing to PageName are not the actual length of the page name created. For example:

    $title='What to do with ‘white working-class’ underachievement? Framing ‘white working-class’ underachievement in post-Brexit Referendum England';
    echo '<br>' . $title;
    
    $nice_name = $this->sanitizer->pageName($title);
    echo '<br> Pagename : '  . $nice_name ;
    
    $nice_name = $this->sanitizer->pageName($title, true);
    echo '<br> Pagename with beautify: '  . $nice_name ;
    
    $nice_name = $this->sanitizer->pageNameTranslate($title);
    echo '<br> PagenameTranslate : '  . $nice_name;

    gives the result

    What to do with ‘white working-class’ underachievement? Framing ‘white working-class’ underachievement in post-Brexit Referendum England
    
    Pagename : what-to-do-with-white-working-class-underachievement--framing-white-working-class-underachievement-in-post-brexit-re
    Pagename with beautify: what-to-do-with-white-working-class-underachievement-framing-white-working-class-underachievement-in-post-brexit-referendum
    PagenameTranslate : what-to-do-with-white-working-class-underachievement-framing-white-working-class-underachievement-in-post-brexit-referendum 

    but the page name that is actually created by that title when I do $pages->add  is:

    what-to-do-with-white-working-class-underachievement-framing-white-working-class-underachievement-in-post

    even stripping out quote marks etc I can't reliably get it to be the same length so can't test against it.

    Any clues as to what I should be using?

     

  15. Like @wbmnfktr we only really do bespoke designs.

    If a client asks us to do a 'bog standard' website then honestly we're going to point them either at a site builder ( Wix / Weebly / Squarespace ) or even (shudder) WordPress. That's because there are so many off the shelf templates for those platforms that will probably do just what they much cheaper than we can. A hosted solution also means we don't have to be involved in keeping systems up to date and applying the inevitable WP security patches.

    There's also the fact that building those kind of sites honestly isn't that satisfying.

    Website builders are just getting better and more convenient for user so I think your business will really need to offer something above what they offer in order to be a success, and this is exactly where ProcessWire can help.

    Nearly all of the sites we build have some bespoke functionality, whether it's for a campaign or gathering and presenting data in an interesting way. Using ProcessWire means we can rapidly develop that extra functionality they need to add value to their site.

    If you're not a designer then it would definitely be worth partnering with a someone who has design experience. For purely functional sites you can use Bootstrap  / UIKit etc but creating a bespoke site is definitely something a professional designer needs to be involved with. A good design is valuble to your client so something they should be paying for; probably the hard part is going to be convincing them of the value you're providing over and above just using an off the shelf design.

    • Like 3
  16. OK - this kind of works:

    In my template I've included the Cloudflare script (and added their end point to the content security policy). Then I'm injecting a placeholder into the form:

    $comments_form= $page->comments->renderForm(array(
          'requireHoneypotField' => 'email2'
           ));
    
    // add a div with class="cf-turnstil" to the form - this gets replaced with a token (after a successful challenge)
    $cft_tag='<div class="cf-turnstile" data-sitekey="yourturnstilesitekey"></div>';
    $comments_form=str_replace("CommentForm' method='post'>","CommentForm' method='post'>$cft_tag", $comments_form);
    
    echo $comments_form;

    Cloudflare replaces that with  a token if they think you're not a bot.

    Then in init.php (not _init.php) I'm hooking into ProcessPageVIew

    $this->addHookBefore('ProcessPageView::execute', function(HookEvent $event) {
    
    	if(wire('input')->post('CommentForm_submit')){
    
                    // get the Cloudflare token.
                    $cf_token=wire('input')->post('cf-turnstile-response');
    
                    // and send it to siteverify 
                    $postdata = http_build_query(
                    array(
                            'secret' => 'yoursupersecretcloudflaresecretkey',
                            'response' => $cf_token
                    )
                    );
    
                    $opts = array('http' =>
                    array(
                        'method'  => 'POST',
                        'header'  => 'Content-Type: application/x-www-form-urlencoded',
                        'content' => $postdata
                    )
                );
    
                    $context  = stream_context_create($opts);
    
                    $api_json_response = file_get_contents('https://challenges.cloudflare.com/turnstile/v0/siteverify', false, $context);
    
                   if($api_json_response ){
    
                            // check result and redirect if failed.
                            $result=json_decode($api_json_response,TRUE);
    
                            if(!($result['success'])){
                                    // die or redirect or whaterver you fancy.
    								// print_r($result);
    								// die("Failed verification.");
    								wire('session')->redirect('/some-help-page-or-something/'); 
                                
                            }
                   }else{
                            // die or redirect or whaterver you fancy.
    						die("No response from verification server.");
                   }
    
    	}
      
    
      });

    If we have a comment that's been submitted then I check the token with Cloudflare. If it fails we can redirect or die or something - it'd be nice to fail a bit more gracefully.

    No idea how well this will deal with spam and I think I'll need to do some user testing but I think it might be useful.

    • Like 2
  17. We're building a site that's going to be using the Comments Module and I'm looking at ways to mitigate the amount of spam I'm expecting.

    Alongside the usual honeypot / Akismet solutions I was thinking of using Cloudflare's Turnstile captcha doodah.

    I was thinking of either just not rendering the form until the Turnstile challenge was completed or hooking into the Comments module to validate the Turnstile token, or hell, maybe both.

    Has anyone done something like this already? It's not obvious to me how to hook into a Comment submission - so if anyone can point me in the right direction that would be a good start.

  18. Could you first do a simple test to see if your lat and lng are between the highest and lowest values in your polygon (so treat it like a square on first pass). Then once you've got a (hopefully much smaller) list of pages that are in that rough area you can loop through the page array and see if they are actually within the polygon (either using Leaflet or there's bound to be some php code out there).

    It might not help in your particular case but it might be a good strategy to try first.

    And of course if you live near London (hello) or anywhere else at zero longitude then you'll have to deal with that...

    • Like 1
  19. It's not exactly a magazine but I did write up a move we did for a site from WP to PW:

    It has lots of tagging and authors and and so on. And that write up has some handy before and after metrics in which might help you convince them...

    • Like 4
  20. So ... we're ditching Google Analytics for one of our sites (hooray) and switching it over to Fathom.

    Because we wanted to be able to make a comparison between the old stats and the new ones we've had both installed on the site for a couple of months.
    On top of that the site is behind Cloudflare so we also have stats directly from them.

    I've just pulled together the stats for the last 30 days:

    analytics_comparison.thumb.jpg.a8ca834c8a12832a48273067cfd29406.jpg

    So thats:

    37K visitors according to Google,
    42K for Fathom
    80K for Cloudflare

    My guess is that a Google is both fussiest about tying requests to real people (cos hey that's what they're after), but also most likely to be blocked by ad blockers and privacy tools.
    Fathom is probably less likely to be blocked by privacy tools; we use a bespoke domain which hopefully isn't blocked that often.
    Cloudways isn't is going to be blocked by anything (it's gathering stats at sever level) but maybe is more lenient about what constitures a 'visitor'. I'm not really sure about that but it is a big jump from Google.

    Anyway - I thought it was an interesting comparison and handy for when you need to explain to clients that analytics are useful for trends but not much cop if you're after real numbers.

     

     

    • Like 4
×
×
  • Create New...