Jump to content

Edison

Members
  • Posts

    55
  • Joined

  • Last visited

  • Days Won

    2

Everything posted by Edison

  1. When I installed Processwire for the first time, I noticed that Admin sessions were not lasting for a long time and it was very common to be logged out. Reading this forum I found useful information, but I could not solve this issue. I tried to modify site/config.php adding $config->sessionExpireSeconds with different values, but without success (for your awareness sessionExpireSeconds default value is 86400 seconds, ie 1 day .. so quite enough!). OK, I had no time to go deeper, so I kept doing what I had to do ... being happily logged out … time to time. Few weeks later while studying Session.php in wire/core/ I got the hint (please read the commented text) which helped me to understand how to solve it: if(ini_get('session.save_handler') == 'files') { if(ini_get('session.gc_probability') == 0) { // Some debian distros replace PHP's gc without fully implementing it, // which results in broken garbage collection if the save_path is set. // As a result, we avoid setting the save_path when this is detected. } else { ini_set("session.save_path", rtrim($this->config->paths->sessions, '/')); } } Uh! Yes, my VPS uses Debian 9 ! So I quickly did a test: echo ini_get('session.gc_probability'); echo ini_get('session.gc_divisor'); echo ini_get('session.gc_maxlifetime'); The session.gc_maxlifetime was rightly set to 86400, but session.gc_probability was 0 ! As you can easily get from Session.php routine, if probability is 0 bye bye PHP's session garbage collection and of course PW will not set the save_path. But why probability is set to 0 ?? This is due to the fact Debian/Ubuntu Linux overrides standard PHP’s session garbage collection by setting session.gc_probability to 0. As a consequence PHP’s standard session garbage collection will never run. Instead Debian/Ubuntu sets a specific Cron job for garbage collection with a duration of 1440 seconds, ie 24 minutes. This determines the max session duration. If you try to change session.gc_maxlifetime through $config->sessionExpireSeconds=86400 or ini_set('session.gc_maxlifetime', 86400) it will have no effect and sessions will be deleted at intervals of 24 minutes (or within 54 minutes). The Cron job garbage collection duration is defined at php.ini level, and runtime changes to session.gc_maxlifetime will not affect the Cron job timeout. Uh! Of course the simplest solution would be to change session.gc_maxlifetime in php.ini. However sometimes this is not possible (shared hosting). Another point to consider is that this change may affect multiple applications running on your server. Instead of modifying php.ini, personally I preferred to enable PHP's session garbage collection locally for Processwire only. Majority of the code is already written in PW Session.php, we have just to take benefit of it. Let's go step by step. Let's open site/config.php and do the following modifications (I choose to implement 14400 seconds session time, ie 4 hours): /** * Reduce inactivity timeout (original 86400 = 1 day) * How many seconds of inactivity before session expires */ $config->sessionExpireSeconds = 14400; /** * Enable Session Garbage Collection * Garbage Collection is disabled in Debian as default (probability set to zero) * Enable session garbage collection with a 1% chance of running on each session_start(); * Chance calculated as gc_probability/gc_divisor * Session path is defined inside wire/core/Session.php and points to site/assets/sessions * Thanks to this modification session now takes into account gc_maxlifetime set in config */ ini_set('session.gc_probability', 1); ini_set('session.gc_divisor', 100); Do not forget to lock site/config.php after modifying it. Here it is! We are done! After we have set gc_maxlifetime (through sessionExpireSeconds), enabled the garbage collection (gc_probability=1), and set its intervention probability (gc_divisor=100 ie 1% chance), we can rely on Session.php where the a session path (session_save_path()) is defined, and session is started (session_start()). In the development session of our browser let's look for our session cookie: if you go to site/assets/sessions you will find your corresponding session: Please note that during development phase, likely having little traffic, the number of sessions may grow and they will not be deleted. This depends from the 1% chance of deletion. If you want to test garbage collection is properly working or just to make sure sessions files are regularly cleaned during development, you can decrease gc_divisor to a lower rate(10=>10% ... 2=>50%). Do not forget to reestablish gc_divisor = 100 when moving to production. I hope some can find that tutorial useful in the future ... and may save several log outs!
  2. Personally when I install a CMS I prefer to set it inside a subdirectory rather than the root folder. There are different reasons for this choice. For example you may wish to have different CMS installed in different subdirectories; or multiple copies or versions of Processwire in different subdirectories; keep separated directories for production and development; and so on. Sometimes this choice is taken also to obfuscate your CMS contents. In this forum I found some helpful hints, but I could not find a turn-key tutorial explaining the detailed steps for redirection and how to hide subfolder in urls' segments. It's pretty easy. Just let's do it step by step. We are going to create a new .htaccess file in the root folder (please note it is a new one, we will leave the .htaccess file in Processwire subfolder unchanged), where we will add the section described below. Just replace yoursite.com and yoursubfolder with ... your ones! # .htaccess in root directory # Redirect to Processwire subdirectory <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{HTTP_HOST} ^(www.)?yoursite.com$ RewriteCond %{REQUEST_URI} !^/yoursubfolder/ RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ /yoursubfolder/$1 RewriteCond %{HTTP_HOST} ^(www.)?yoursite.com$ RewriteRule ^(/)?$ yoursubfolder/index.php [L] </IfModule> With this approach we can protect root files and subdirectories from redirection. Are we done ? Yes and no. After these modifications pages are redirected correctly, but in the browser link you will note that subfolder's name is still showing. This is not good, we want to hide it. We are just one step away. Let's open our site/config.php and add the following line at the end: /** * Set urls root to hide Processwire folder * This must be combined with .htaccess in root directory */ $config->urls->root = '/'; Do not forget to lock site/config.php after modifying it. If you prefer, instead of modifying config.php, you can place the above line in _init.php. And here it is! I hope this simple tutorial can be of help.
  3. Hi @Mithlesh I gave a quick look to your site html source code, it seems social media icons are made using Font Awesome, by calling their classes 'fa-twitter' 'fa-facebook' 'fa-linkedin-square': If you wish to modify them you should work on the footer of your profile, changing the section above in site/templates/_main.php. If you need any further help please let me know.
  4. H i@Manuel good point! I recalled get() to return NULL. But this happens only when field is not found (doubled checked in the API) ! So you are right, the variable is set but empty. I think @BitPoet is right, for this purpose ?: is the right operator: $a ?: $b Testing Conditional Function $a ?: $b {Null}: b $a ?: $b {Empty String}: b $a ?: $b {0 Integer}: b $a ?: $b {0 String}: b $a ?: $b {False}: b $a ?: $b {True}: 1 $a ?: $b {Full String}: a
  5. Hi @Rodd I tested PW on an OVH shared hosting (the Pro version) and I had no issues. Unfortunately in my case I needed to use PHP at 64-bit which OVH was not supporting on sharing hosting. I took the opportunity to switch to a VPS.
  6. Hi @Manuel, I am afraid not... What about using the "Null coalescing operator" (if you are using php 7) or otherwise the ternary operator: echo $page->meta_description ?? $pages()->get(1)->meta_description; Just written in the browser w/o testing, hope it can help.
  7. When you install PW for the first time choosing Uikit 3 Site/Blog Profile you feel good. You immediately understand it will take you sometime to fine tune your site, but you will not regret other popular CMS. After a short front end navigation with Uikit 3 Site/Blog Profile the theme looks simple but complete, so a great starting point. But suddenly your eyes will point at something strange … why my reserved user name which I took 10 minutes to invent … is right there in front of me: "Posted by mySecretUserName …" so that every visitor can see it ?! Uh! Let's make a step back. When you first installed PW you were asked for a user name and password. I tend to avoid generic user names like "admin" … "administrator" … but I normally build a complex user name using lowercase/uppercase/digits. If you open the post, and look at the bottom you will see that your reserved user name not only is below the post title but also in the preset data of the comment form. OK, keep calm! After a quick learning about Output Strategies and ... scrolling each file in /site/template you finally understand ... that what you are looking for is ukBlogPost() function in _uikit.php. Yes, this is the magic function that renders a blog-post, both when it is just summarized in a list of items or fully displayed in a page. The line of code that was waiting for me was: $name = $page->createdUser->name; Uh! So no mistakes, my reserved user name was going public ... After a quick course on the Admin side and reading this forum, you start to feel how PW is powerful. What we have to do to solve our problem it is simply to enhance the User template adding a publicName field to it. Let's proceed step by step. Login the Admin panel, select Setup, then Fields. Let's create a new text field named publicName, then save. Then select Setup, then Templates. In PW you can also edit system templates, but to do that you have to select the show system templates option: Once the list of system templates appears, we will select the template "user", and we will add the new field publicName, then save. Now clicking on your user name on the top you are going to open your user profile. A blank field named Public Name is now available. There we will write our public name, that.. by chance … will be "Administrator". Of course you can choose whatever you like, and any new user you are going to create will have available such a field. So each user can have a complex user name for login purposes and a public name that will be shown, for example when posting blog posts or replying to comments. Now that the new field is available we can go back to ukBlogPost() function in _uikit.php and change the code as follows: //$name = $page->createdUser->name; $name = $page->createdUser->publicName; //>>>>> REPLACE WITH THIS LINE Let's go back to the front end, and now we see that our reserved user name is no longer there: "Posted by Administrator on …". Good... I feel better. Please note that reserved user name in the comment form will be shown only for logged-in user, and you can manually overwrite it in case you have to reply a comment. Otherwise it's necessary to modify FieldtypeComments module. In particular the function render() of ContactForm.php: if($user->isLoggedin()) { //$inputValues['cite'] = $user->name; $inputValues['cite'] = $user->publicName; //>>>>> REPLACE WITH THIS LINE $inputValues['email'] = $user->email; } As I already did few other modifications to FieldtypeComments I also took the opportunity to fix that, but if you are a new PW user, please wait to modify FieldtypeComments until you are more familiar with the environment. It will be necessary to clone the module as described in the first steps of this tutorial: I hope some new user will find those information of help. Wish a good week start to everybody!
  8. Thank you @LostKobrakai you are right, IP address processing makes sense for spam protection reasons. Personally I prefer to manage IP filtering directly at server level instead of CMS level. For this purpose (and other reasons) I setup a POST Flood jail with Fail2ban on the server side, without storing IP addresses in the CMS Database. The problem rises when you store the IP address in the CMS database, both because you have to quantify for how long you are going to store it, and because you are directly associating IP address with user name and email. However under GDPR all of this is legitimate provided you inform the user in advance and get an explicit consent. IP Anonymizing is just a possible option on the table, as once IP is anonymized is no longer considered "personal data" under GDPR. However since name and email are stored, an explicit consent will have to be asked anyhow. It is important to note that another reason for comments IP tracking (and storage) is for legal purposes in the event of disputes over the content of the comments.
  9. For those who have the "opportunity" to deal with GDPR … and are implementing PW FieldtypeComments it's worth making a Privacy assessment. When a visitor submits a comment, is going to visibly provide personal data: the commenter's name (or nickname) and email. Going a bit deeper, we can see that a cookie named CommentForm is created, it stores visitor's name and email, with session long duration. If you wish more details, this cookie is entirely managed at javascript level in comments.js. Of course this cookie is very helpful as it pre-fills the comment form with commenter's information to speed up future comments submissions. Let's make a step further and have a look to field_comments table inside the database. As you can see (PhpMyAdmin), in addition to the commenter's name (cite) and email, FieldtypeComments is also tracking the commenter's IP address. Uh..! Should we ring an alarm bell ... and remove all the comments from our blog … ? Of course not ... The simplest approach would be to make sure your privacy policy and cookie policy are taking into account all those aspects, including IP tracking, to make your visitors aware which personal data you process, why and for how long do you keep them. In light of that, it gets important to ask the commenter an explicit consent to personal data processing by a checkbox when submitting the comment form. More specifically regarding IP tracking you may ask yourself if you really need to track the commenter's IP address. You should also get information if you-have-to-do-it because of your country laws. If you concluded that you are not interested or obliged to track commenter's IP address, how to disable it ? Commenter's IP address is gathered in function processInput() of CommentForm.php in FieldtypeComments module. $comment->ip = $this->wire('session')->getIP(); Unfortunately I could not find any preset option to stop it, but (... obviously!) it is sufficient to change it to: $comment->ip = ''; However do not forget not to modify the original FieldtypeComments in wire/modules/Fieldtype/FieldtypeComments, but copy it to site/modules/FieldtypeComments. Please refer to the initial part of this tutorial for the detailed steps to duplicate the module and make PW aware of which module version to use. An alternative to entirely disable ip tracking could be to anonymize it. $comment->ip = A::anonymizeIp(wire('session')->getIP()); Through the function here below. Please note, in this function I could not test if ipv6 anonymizing works as expected. class A { public static function anonymizeIp(string $ip) { $ipv4Mask = "255.255.255.0"; //strip last octect $ipv6Mask = "ffff:ffff:ffff:0000:0000:0000:0000:0000"; //strip last 80 bits $ipLen = strlen(inet_pton($ip)); $ipMask = $ipLen == 4 ? $ipv4Mask : ($ipLen == 16 ? $ipv6Mask : ''); return inet_ntop(inet_pton($ip) & inet_pton($ipMask)); } ) And here we are! Happy Privacy to everybody !
  10. When your blog content is growing to some thousands of pages and you have comments scattered here and there, sometimes you start wondering if placing comments below each post is the best approach. Of course this totally depends on your project and contents. In my case I came to the conclusion I could provide a better user experience switching to a Forum-like solution, where discussions were organized by macro subjects and not below each individual post. Of course there are many products and services providing fully featured forum solutions, but getting only few comments per day I was reluctant to setup a dedicated forum and to deal with users' profiles. A nice thing of the comments system is that users do not need to create account and password, but simply use their email address. And here we are ... why not to setup a very basic Forum-like solution using PW Comments? But please do not get too excited… we will just use comments in a way that looks-like-a-forum. I think this solution makes sense if you have a limited number of comments per day spread on some topics. If you need to deal with high discussions traffic with several users ... do it with a fully featured forum solution !! To implement this Forum-like solution we will take benefits of (1) PW pages/templates flexibility (why you should be here otherwise…), (2) core module FieldtypeComments, (3) Uikit 3 Site/Blog Profile, (4) some of the tutorials I recently released about PW Comments (listed at the end). OK, let's start. Login into the Admin panel, select Setup, and then Fields. Let's create a checkbox field named "sticky" that we can use to keep some forum subjects on top of the communities. Now let's move to Setup, Template. We will create a new template with name "communities'. In addition to "title" field, we will add also "body" and "sidebarRight" fields (already available in the profile): Then we create a template, that we will use for individual communities, with name "community". In addition to "title" field, we will add "date", "body", "images", "comments" fields (already available in the profile) and the "sticky" field we have just created. If you wish, as I did, you can also associate some category fields. If you use Multi-Language is a good idea to use the field "date", as ,differently from createdStr, it will automatically return a locale-formatted date as you defined in the field. Now that we have created the templates, we need to customize them. Reopen the "communities" template. In the Family tab: allow the template to have children, allow only one page with this template, select "community" as only allowed template for CHILDREN. Then in the URLs tab: select allow page numbers. This will return a paginated list of communities. We reopen the "community" template. In the Family tab: do not allow the template to have children, allow to create new pages, select "communities" as only allowed template for PARENT. Now in the URLs tab: select allow page numbers. This will be necessary to create paginated comments. Let's go to the page-tree, under Home we will create a new page named "Community" using the templates "communities" (note the final s), this will be the page listing all the communities. Under the Community page, we will create the individual communities. Give the pages the name you wish and use the template "community" (note w/o the final s). It may be good creating one with the name "New Forum Requests" checking its "sticky" field so that it remains on top. As to implement this Forum-like solution, the comments pagination is a must, at this stage you will have to implement it as described in this tutorial: Now we are ready for coding (very little!) the php templates. We need to associate php files to the two templates we have created. In site/templates create two new php files communities.php and community.php. Open the template blog.php and copy its content to communities.php, then repeat the same operation copying the content of template blog-post.php to community.php. Great! We are few steps away from our Forum-like solution. Modify communities.php as follows: ... echo ukHeading1(page()->title, 'divider'); echo page()->body . '<br><hr>'; //>>>>> ADD THIS LINE //$posts = page()->children('limit=10'); //echo ukBlogPosts($posts); $posts = page()->children('sort=-sticky, sort=title, limit=10'); //>>>>> ADD THIS LINE echo ukBlogPosts($posts, ['moreText' => '', 'moreIcon' => '']); //>>>>> ADD THIS LINE ... Then modify community.php as follows: // blog post content echo ukBlogPost(page()); // comments if($page->hasField('comments')) { //>>>>> ADD THIS LINE $comments = page()->comments; // comment list if(count($comments)) { //echo ukHeading3("Comments", "icon=comments"); echo ukComments($comments->find("sort=-created"), ['paginate' => true]); //>>>>> ADD THIS LINE - READ PAGINATION TUTORIAL (NEEDED) echo '<hr>'; } // comment form $gdpr = __('I agree with the processing of my personal data. I have read and accept the Privacy Policy.'); //>>>>> ADD THIS LINE - READ GDPR TUTORIAL (OPTIONAL) echo ukHeading3(__('Join the discussion'), "icon=comment"); echo ukCommentForm($comments, ['labels' => ['gdpr' => $gdpr]]); //>>>>> ADD THIS LINE - READ GDPR TUTORIAL (OPTIONAL) } //>>>>> ADD THIS LINE Very good (hope so...). Now let's see the result. When browsing to the Community page we will get the list of the communities. In my example I decided to keep sticky the one where users can submit suggestions for new forum subjects. Selecting one of the communities we have created, we are going to see (1) a description of the community, (2) a paginated list of comments, (3) and the comment form at the bottom. Please note that in this picture pagination is kept with a limit of three items just to fit the image … It makes sense to have 10-15-20 comments per page depending on your preferences. And we are done, here it is. If you wish you can implement some additional features as described in the tutorials below. Not mandatory but probably makes good sense to enable the honeypot field as described in this tutorial as anti-spam measure: If you have the "pleasure" to deal with GDPR matters, here is described how to add a privacy checkbox to your comment form: and finally if you wish your Forum-like solution to be Multi-Language here it is what to do: I hope you enjoyed and wish you a nice week-end ! PS: it's a couple of tutorials I miss the emoj … hope they will come back in this forum soon !!
  11. While working on optimizing Comments field for my blog, I noticed it could have been useful to have a GDPR checkbox in the form, to get privacy acceptance before a comment is submitted. At the beginning I though to inject the checkbox processing the rendered comment form, but as I already modified FieldtypeComments to create a Language field, I decided to continue on that road to add the GDPR checkbox. We will not modify the original FieldtypeComments in wire/modules/Fieldtype/FieldtypeComments, but copy it to site/modules/FieldtypeComments. Please refer to the initial part of this tutorial for the detailed steps to duplicate the module and make PW aware of which module version to use. Now that FieldtypeComments is duplicated, we can proceed with the necessary modifications. Inside the module there are 14 files, but do not worry ... only ContactForm.php and (optionally) comments.css will have to be modified. First we will modify the $options class property of ContactForm.php to add a new 'gdpr' label. Later we will use this option to pass the label's text associated with the checkbox. protected $options = array( … 'labels' => array( 'cite' => '', // Your Name 'email' => '', // Your E-Mail 'website' => '',// Website 'stars' => '', // Your Rating 'text' => '', // Comments 'submit' => '', // Submit 'starsRequired' => '', // Please select a star rating 'gdpr' => '', // >>>>> ADD THIS LINE ), As a second step it will be necessary to create the markup of the checkbox and of its label. We will do that by modifying function renderFormNormal() in ContactForm.php. Uikit 3 Site/Blog Profile is indirectly calling this function, so for my purpose it was enough. In case your application is using threaded comments, it will be necessary to modify also renderFormThread(). "\n\t\t<textarea name='text' class='required' required='required' id='{$id}_text' rows='$attrs[rows]' cols='$attrs[cols]'>$inputValues[text]</textarea>" . ... "\n\t</p>" . "\n\t<p class='CommentFormGdpr {$id}_gdpr'>" . //>>>>> ADD THIS BLOCK - START "\n\t\t<input class='uk-checkbox' type='checkbox' name='gdpr' value='checked' required='required'>" . "\n\t\t<label for='{$id}_gdpr'>$labels[gdpr]</label>" . "\n\t</p>" . //>>>>> ADD THIS BLOCK - END $this->renderNotifyOptions() . ... The last ContactForm.php modification will include our checkbox in processInput() to block comments submissions if GDPR checkbox is not filled. Please note this will operate in case you do not place "required" in the <input> directive. ... $errors = array(); foreach(array('cite', 'email', 'website', 'stars', 'text', 'gdpr') as $key) { //>>>>> ADD 'gdpr' in the array ... Now let's see how to call the modified form. If you are using Uikit 3 Site/Blog Profile you will have simply to modify the template file where ukCommentForm() is called (example: blog-post.php template). There we will prepare our checkbox message and pass it to ukCommentForm() as an argument option. echo ukHeading3(__('Join the discussion'), "icon=comment"); $gdpr = __('I agree with the processing of my personal data. I have read and accept the Privacy Policy.'); echo ukCommentForm($comments, ['labels' => ['gdpr' => $gdpr]]); However, if you are using comments in multiple template files, it makes more sense to directly modify ukCommentForm() presetting the new options inside the function body: $defaults = array( 'headline' => '', 'successMessage' => __('Thank you, your comment has been posted.'), 'pendingMessage' => __('Your comment has been submitted and will appear once approved by the moderator.'), 'errorMessage' => __('Your comment was not saved due to one or more errors.') . ' ' . __('Please check that you have completed all fields before submitting again.'), // >>>>> SEE THE HONEYPOT TUTORIAL 'requireHoneypotField' => 'email2', //>>>> ADD THESE FOUR LINES 'labels' => array( 'gdpr' => __('I agree with the processing of my personal data. I have read and accept the Privacy Policy.'), ), ); Before testing, we will modify the file comments.css adding these two directives (that's purely optional): .CommentFormGdpr input[type=checkbox] { background-color: white; } .CommentFormGdpr label { padding-left: 10px } Now let's test our form. Once it is filled with our data and comment it will look like this: If the user is pressing the submit button without accepting the GDPR checkbox, the comment will not be submitted and an error is displayed (in case you have used "required" in <input> you get this tooltip box, otherwise you will get an alert message): Now we accept the box After acceptance and submission, the comment form will be successfully sent. The standard success message is shown and the form is again displayed empty (with just cite and email pre-filled) ready for a next comment submission. Again I hope you will find some useful hint with that.
  12. Hi @neonwired, guest and superuser are default roles. Documentation says: "The guest role is automatically given to all anonymous site users. You should not give this role any access other than page-view permission." You can find further details here. If you wish to assign specific permissions you can easily create custom roles. Just as an example for a blog website, you may create visitor, author, editor roles and assign them their specific permissions.
  13. Hi @Torsten Baldes, you could try the following: wire('user')->setTrackChanges(Wire::trackChangesOn | Wire::trackChangesValues); wire()->addHookAfter('Pages::save', function($event) { // >>> ON PAGE SAVE $page = $event->arguments(0); if($page->template->name === 'user') { // >>> APPLY ONLY TO USER TEMPLATE $changes = user()->getChanges(true); // output changes as log entry wire('log')->save('testchangelog', print_r($changes, true) ); $m = new WireMail(); $m->from('Your email From'); $m->to('Your email To'); $m->subject('Your email subject'); $m->body('Your email text'); $m->send(); } }); Note: made a couple of corrections and changed to WireMail as per @horst suggestion. Still testing if changes are recorded...
  14. Hi @iipa , I think so... ? Here is an example taken from PhpSpreadsheet documentation: <?php require 'vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('A1', 'Hello World !'); $writer = new Xlsx($spreadsheet); $writer->save('hello world.xlsx'); Php "use" directive provide you an alias for the namespace, so thanks to that in the above example you can simply instantiate this class's library as: $spreadsheet = new Spreadsheet(); Alternatively you could call the class with the fully qualified class name which includes both namespace and class: $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); Of course if you have several calls... the "use" directive avoids to keep repeating the fully qualified name each time. ? Without informing PHP about the namespace of your class or function, it will search for it inside Processwire namespace where not finding it will return an error. ?
  15. When a blog post article receives many comments ? your start to consider to apply pagination to comments. Looking around this forum, I found some useful hints, but I couldn't find how to take full advantage of PW built-in pagination. In particular the ukBlogPosts() function in Uikit 3 Site/Blog Profile renders a list of paginated posts. ukBlogPosts() is calling a ukPagination() function which permits to navigate the posts. ? After some unsuccessful attempts to use it with CommentArray … ? I decided to insist. ? And here is the result. The ukPagination() function in _uikit.php expects an argument $items of type PageArray. Of course a CommentArray ... is not a PageArray ... ? so we have to remove this blocking point. Going deeper inside the function you will notice that pagination is achieved through a call to a render() method of MarkupPagerNav. Let's look at its declaration: public function ___render(WirePaginatable $items, $options = array()) Interesting ? here $items are declared of type WirePaginatable. What's that? ? a class? No! It's an interface! ? Looking around further I discovered that WirePaginatable interface is used by PageArray, PaginatedArray, and CommentArray.. bingo! ?? We got what to do first, modify ukPagination() function in _uikit.php changing $items argument type from PageArray to WirePaginatable: //function ukPagination(PageArray $items, $options = array()) { function ukPagination(WirePaginatable $items, $options = array()) { //>>>>> REPLACE WITH THIS LINE Now the function accepts our CommentArray as argument, but to make it work we need to set its Limit, Start, and Total attributes as PW does automatically when dealing with pages. In order to do that we will use the WirePaginatable methods setLimit(), setStart(), setTotal(). In this process our Start attribute will have to be calculated dynamically to consider the page number when navigating the comments. To do that we will largely ? modify the function ukComments() in _uikit.php: function ukComments(CommentArray $comments, $options = array()) { if(!$comments->count) { //>>>>>NEW-start if(input()->pageNum > 1) { // redirect to first pagination if accessed at an out-of-bounds pagination session()->redirect(page()->url); } return ''; } //>>>>>NEW-end $defaults = array( 'id' => 'comments', 'paginate' => false, //>>>>>NEW 'limit' => 3, //>>>>>NEW ); //if(!count($comments)) return ''; $options = _ukMergeOptions($defaults, $options); $language = user()->language->id; //>>>>>NEW $comments = $comments->find("language=$language"); //>>>>>NEW if($options['paginate']) { //>>>>>NEW-start $limit = $options['limit']; $start = (wire()->input->pageNum - 1) * $limit; $total = $comments->count(); $comments = $comments->slice($start, $limit); $comments->setLimit($limit); $comments->setStart($start); $comments->setTotal($total); } //>>>>>NEW-end $out = "<ul id='$options[id]' class='uk-comment-list'>"; foreach($comments as $comment) { $out .= "<li class='uk-margin'>" . ukComment($comment) . "</li>"; } $out .= "</ul>"; if($options['paginate'] && $comments->getTotal() > $comments->count()) { //>>>>>NEW-start $out .= ukPagination($comments); } //>>>>>NEW-end return $out; } As you will notice we have added two options to the function [paginate => false] and [limit => 3]. In such a way the ukComments() function will operate as before. If you wish to use pagination you will have to pass [paginate => true] as argument of the function. Of course you can freely choose the limit value or by pre-setting it in the function or by passing it as an argument to the function. Please note that the modification implements the necessary changes (2 lines) to make comments language-aware as described in the tutorial: We are now just to steps away to see our paginated comments. ? First you will need to enable pagination in the templates where you want comments to be paginated. Login to PW Admin and select your template. In the tab URLs enable the "Allow Page Numbers?" option. Second you will need to modify the associated php template, in our example blog-post.php, to change the call to ukComments() as follows (sorting is not necessary, depends on your preferences): //echo ukComments($comments); echo ukComments($comments->find("sort=-created"), ['paginate' => true]); //>>>>> REPLACE WITH THIS LINE And here we are! Below a snapshot our paginated comments! ??? As you can see on the bottom of the comments you have the standard PW pagination navigator, without being forced to rewrite a new one for the purpose! ? I hope you can find something useful with this tutorial.
  16. Hi @iipa I am not familiar with PhpSpreadsheet, ? but more generally speaking you can upload an external library calling a require_once() in profile ready.php or in site/config.php. I load all my libraries from site/config.php calling a simple loader /** * MRCMOD03: Add custom library Argo * */ include_once(__DIR__ . '/includes/Argo.php'); which load all my functions and class libraries (same as PW does in wire/core/boot.php file) <?php namespace Argo; $preloads = array( 'argo/Functions.php', 'argo/Object.php', 'argo/Controller.php', 'argo/Widget.php', 'argo/WidgetTracker.php', 'view/ContactForm.php', 'view/CookieBanner.php', 'view/FollowButtons.php', 'view/RecentPosts.php', 'view/ShareButtons.php', 'view/LegalNotice.php', 'controller/CookieController.php', ); foreach($preloads as $file) { include_once(__DIR__ . '/' . $file); } unset($preloads); As library will have different namespace than Processwire namespace, do not forget to apply "use" directive before using a library class or function in your template ? use \Argo\ShareButtons; ... $sb = new ShareButtons(); I hope you can find some hints with that.
  17. While working on the comments form of my blog, I thought to add an honeypot field in comments form to reduce spam. ? Honeypot is simply an hidden field added to a form. Honeypot field can have any name and is made invisible normally with a css directive {display: none}. Web users, being unable to see the field, will not fill it, while spam bots majority will not detect its invisibility and will populate the field. Once the form is submitted with a not-empty honeypot field it is very likely we are dealing with spam. ? In this post you can find more details about honeypot technique. While studying FieldtypeComments module and in particular CommentForm.php, to my great surprise ? I realized that PW already supports honeypot for Comments Form. ?? This feature has been introduced with PW 3.0.40. Normally this honeypot field is disabled, so it was enough to understand how to enable it! And as often is the case with PW ... it is super easy. ? If in your profile you are directly working with CommentArray, you will just have to enable honeypot passing it as an option to the renderForm() function of CommentArray class, example below: $comments->renderForm(['requireHoneypotField' => 'email2']); And .. we are done! ?? If you will look at the html of your Comment Form you will see an additional line CommentFormHP, that's the hidden honeypot field. In case you are using the Uikit 3 Site/Blog Profile, the renderForm() function is called in _uikit.php, ukCommentForm() function. If you wish that honeypot field is applied to every comment form of your site, just add the requireHoneypotField option to the list at the function start: ... 'errorMessage' => __('Your comment was not saved due to one or more errors.') . ' ' . __('Please check that you have completed all fields before submitting again.'), requireHoneypotField' => 'email2', // >>>>> ADD THIS LINE ); ... Otherwise if you wish to add honeypot in comment form on selected templates only, do not modify ukCommentForm(), but pass the option requireHoneypotField when calling the function in your template: ukCommentForm($comments, ['requireHoneypotField' => 'email2']); Now that we enabled it, let test if honeypot works. ? In the browser development section let's select the honeypot field and disable css {display:none} to show it. A new field will appear: If the spam bot is going to fill the field with a value and submit the form, an error is returned and comment will not be submitted ? That approach is great as spam comments will not be even saved inside the table field_comments. ? I hope this can be of help if somebody needs to enable this PW comments feature.
  18. Understand @MilenKo, basically you would like users from multi-language countries (Canada or Switzerland) to be able see all the comments written in their national languages (ie: English and French for Canada, or French, German, and Italian for Switzerland). That's sounds a bit more complex, but feasible. ? I would still keep language ID field in the comments table, but you may wish to re-think filtering and detection. Filtering will need to identify user's country. Using third party services it is possible to get user's country from IP address. In PW environment you can find a great example of country detection in the module CookieManagementBanner by @adrian. I have not tested it, but it looks promising. You will then need a simple data structure (array or db-table) to associate (only) the multi-language countries with their respective languages. If a country is not multi-language (default) you will filter comments just based on " $comment->language === user()->language->id ", otherwise for multi-language countries you will filter the comments matching with all the country's languages (language1 or language2 or …) as per your data structure. Only drawback… if your Canadian user is in business trip in another country ... it will not work as if in Canada! ? Language detection may remain unchanged to " comment->language = user()->language->id ". But this does not solve the case you described: a Canadian user reading the website in French, but posting a comment in English. Simplest solution could be to add a <select> option in the comment form with the list of multi-language country's languages (only for users ip-identified as being from a multi-language country) to get the information about comment's language directly from the user (why to make it more difficult ... ?). If you do not wish to ask the user, you may think to apply an algorithm of language detection to the comment string (just googled this on the net https://github.com/patrickschur/language-detection), but this approach may lead to some imprecisions in associating the comment's language. These are just some ideas, I hope you may find some hint!
  19. It is very simple. ? The language ID is picked up from the language the user is reading the website. This takes place in CommentForm.php, processInput() modified line: $comment->language = wire()->user->language->id; The comments are picked (filtered) when read by the profile. This takes place in _uikit.php, ukComments() modified lines: $language = user()->language->id; ... if($comment->language === $language) $out .= "<li class='uk-margin'>" . ukComment($comment) . "</li>";
  20. As I had to add a custom field to FieldtypeComments, I came through this post. Just in case may be of help for anybody in the future. I prepared a tutorial you can find here:
  21. Thank you @MilenKo. Currently my blog is based on Wordpress and multi-language support comes from Polylang plug-in. It has around 20K posts, now in two languages, and will be switched to Processwire in August. ? Comments are language-aware because there are unique post_id for each language (n languages -> n post records ?), so each comment is indexed to the corresponding post_id in the given language. I think Processwire approach to multi-language is dramatically more efficient, as you have only one post_id (n languages -> 1 post record ?). I just had the issue that comments were language-agnostic, but adding this simple language_id in the comments table I have replicated the same behaviour. ??
  22. Thank you @dragan, I think you are right ? , it would make sense for @ryan to make comments language-aware in the core. Should be a small adaptation, and considering comments filtering takes place at profile level, PW users could freely choose to have language-agnostic or language-aware comments.
  23. Finally I solved adding a language field by modifying the FieldtypeComments module. A detailed tutorial here.
  24. Normally in multi-language websites comments are associated to the language of the page where they have been submitted. A couple of weeks ago I realized that PW Comments field is not language sensitive. This results in a discussion in several languages below an article, as blog-post language is not used for comments filtering. In some cases this may be a plus as you can show a broader discussion in multiple language, but if other cases where a tight language consistency is required may become a limitation. ? Enhancing the Comments field to become language sensitive is not really a big deal. ? It would be enough to add a language column in the database table field_comments to store the user language ID. Thanks to this additional column, each comment would be associated with the language ID of the page where it has been submitted. To achieve that I went through a modification of FieldtypeComments. As it took some time to understand the core module, I thought to write a tutorial hoping this may be of help for others in the future. ? Apart the specific purpose of the language field, in principle this could be useful anytime is necessary to add a custom database column (field) to Comments. FieldtypeComments is a core module located in wire/modules/Fieldtype/FieldtypeComments. As usual we will not work directly on core modules as our modification may be overwritten during PW updates. ? We will make a copy of /FieldtypeComments directory with all its content to site/modules/FieldtypeComments. As we want to overwrite the original FieldtypeComments and keep using the module ProcessCommentsManager, we are not going to rename FieldtypeComments, its directory and files. Now we can Login PW Admin panel, select Modules, then Site. Here we press the button Refresh. PW should inform you that FieldtypeComments module is duplicated and you have to choose which one to use. In the alert click on FieldtypeComments, it will open the module settings: Please select to use /site/modules/FieldtypeComments/FieldtypeComments.module, and make sure to press the submit button on the top right. Remain on the same page and click on InputfieldCommentsAdmin. It will open another module settings. Here select to use site/modules/FieldtypeComments/InputfieldCommentsAdmin.module and make sure to press the submit button on the top right. The two modules are now no longer visible under Core Modules but in Site Modules. Now you can test your comments, adding a new one and approving it with ProcessCommentsManager in the Admin panel just to make sure nothing went wrong. If you will need to revert back to the original core module, in Site Modules click on their links. The module settings will open and there you can select the /wire/modules version. OK now it's time to make our code modifications. Inside the directory /site/modules/FieldtypeComments you will find 14 files (3 modules, 7 php, 2 css, 2 js). ? Do not worry! we will modify only 3 files: FieldtypeComments.module, Comment.php and CommentForm.php. ? We start with FieldtypeComments.module, where we will update the database schema. After that the new language ID column can be read/written from/to the database. We will modify two functions: getDatabaseSchema() and ___sleepValue(): public function getDatabaseSchema(Field $field) { ... $schema['ip'] = "varchar(15) NOT NULL default ''"; $schema['user_agent'] = "varchar($maxIndexLength) NOT NULL default ''"; $schema['language'] = "int unsigned NOT NULL"; //>>>>> ADD THIS LINE $schemaVersion = $field->schemaVersion; ... public function ___sleepValue(Page $page, Field $field, $value) { ... $a = array( 'id' => $comment->id, 'status' => $comment->status, 'data' => $comment->text, 'cite' => $comment->cite, 'email' => $comment->email, 'created' => $comment->created, 'created_users_id' => $comment->created_users_id, 'ip' => $comment->ip, 'user_agent' => $comment->user_agent, 'language' => $comment->language, // >>>>> ADD THIS LINE ); Now we will modify Comment.php where we will set the language property modifying __construct() and set() functions: public function __construct() { ... $this->set('downvotes', 0); $this->set('stars', 0); $this->set('language', 0); // >>>>> ADD THIS LINE } public function set($key, $value) { if(in_array($key, array('id', 'parent_id', 'status', 'flags', 'pages_id', 'created', 'created_users_id', 'language'))) $value = (int) $value; // >>>> MODIFY THIS LINE else if($key == 'text') $value = $this->cleanCommentString($value); Now that we should be capable to read/write the language ID from/to the database, we need to make each comment aware of the user language. It is CommentForm.php turn.. where we will modify processInput() function: public function processInput() { ... $comment->parent_id = (int) $data->parent_id; $comment->language = wire()->user->language->id; // >>>> ADD THIS LINE $errors = array();\ ... As in my case I had some test comments, I decided to use PHPMyAdmin to add the language column into table field_comments using SQL. ? If you plan to do the same, please make sure to backup your database before, to avoid surprises.. ? After creating the new column, I manually updated the language ID of the test comments. ALTER TABLE `field_comments' ADD COLUMN `language` int unsigned NOT NULL AFTER `user_agent`; Now select a blog-post you have available in multiple languages and write new comments for each language. If no issue occurred, in the database table field_comments you should see (with PhpMyAdmin ) the new comments with the user language ID in the language column. This sounds great ... ? but still we have an issue. ? If we look at the blog-post comments and we switch the language, we still have in the comments a discussion in multiple languages. ? We will have to filter the page comments to display only the ones corresponding to the user language. This will depend on your theme profile. As in my case I am using Uikit 3 Site/Blog Profile I had to modify _uikit.php function ukComments(): function ukComments(CommentArray $comments, $options = array()) { ... $out = "<ul id='$options[id]' class='uk-comment-list'>"; $language = user()->language->id; // >>>>> ADD THIS LINE foreach($comments as $comment) { //$out .= "<li class='uk-margin'>" . ukComment($comment) . "</li>"; // >>>>> COMMENT OUT ORIGINAL LINE if($comment->language === $language) $out .= "<li class='uk-margin'>" . ukComment($comment) . "</li>"; // >>>>> REPLACE WITH THIS ONE } After this modification only the comments corresponding to the user language will be displayed ! ? Picky ones.. like me.. ? may notice that comments count in the blog-post header are showing the total count for all the language. For my project this is ok, as I am fine with all comments being counted, doesn't matter the language. But in case you want to fix it, that is pretty simple. Still to work on _uikit.php, now on function ukBlogPost(): function ukBlogPost(Page $page, $options = array()) { ... $moreIcon = ukIcon($options['moreIcon']); $categoryIcon = ukIcon($options['categoryIcon']); //$n = $page->get('comments')->count(); // >>>>> COMMENT OUT ORIGINAL LINE $n = $page->get('comments')->find("language=$language")->count(); // >>>>> REPLACE WITH THIS LINE $numComments = $n ? "<a href='$page->url#comments'>" . ukIcon('comments') . " $n</a>" : ""; ... I hope you may find something helpful in this tutorial ! ?
  25. Thank Elabx, that's very helpful. I will make further trials with markup regions in hooks, but as I was a bit in a hurry I solved it the old way ? ... making a str_replace on $event-return. wire()->addHookAfter('Page::render', function($event) { $event->return = str_replace('<header>', '<header>' . $this->banner->markup(), $event->return) . $this->banner->assets(); });
×
×
  • Create New...