abdus
Members-
Posts
743 -
Joined
-
Last visited
-
Days Won
42
Everything posted by abdus
-
Glad to help, @benbyf
-
Using CSRF protection with AJAX doesn't work as expected
abdus replied to abdus's topic in API & Templates
@matjazp, yes, I set AJAX header ('X-Requested-With' = 'XMLHttpRequest') explicitly with JS, and $config->ajax returns true. -
Are all podcasts in MP3 format? If 'Content-Type' => 'audio/mp3 header and mime type of podcasts do not match, you'd get the error. Try removing 'Content-Type' => 'audio/mp3' from $headers array. In case it doesnt work, https://gist.github.com/benrolfe/5453074 claims to solve iTunes byte range problem. Changing the $file_path at line 2 to $page->mp3->filename should be enough.
-
Hey, I've been trying to implement some progressive enhancements to take advantage of modern browsers, modern JS and CSS capabilities etc. One thing I've got stuck is to CSRF protection using $session->CSRF. I'm echoing hidden token using <?= session()->CSRF->renderInput('contact') ?> inside the form, and I can validate the token with plain POST request (without AJAX) just fine using session()->CSRF->hasValidToken('contact'). For AJAX, I'm setting headers x-<tokenname>:<tokenvalue>. It shows up in both Chrome DevTools correctly, and on backend lke HTTP_X_<TOKENNAME> => <TOKEN_VALUE> as expected, so there's no miscommunication. But, when I try to validate it, it fails no matter what. After some debugging I realized at each request, a new token is created, which invalidates the one I'm sending. Here are the relevant parts from the core. Inside /wire/core/SessionCSRF.php, where validation occurs <?php public function hasValidToken($id = '') { $tokenName = $this->getTokenName($id); $tokenValue = $this->getTokenValue($id); // ... if($this->config->ajax && isset($_SERVER["HTTP_X_$tokenName"]) && $_SERVER["HTTP_X_$tokenName"] === $tokenValue) return true; if($this->input->post($tokenName) === $tokenValue) return true; // if this point is reached, token was invalid return false; } it gets token name from here where it checks inside $_SESSION variable <?php public function getTokenName($id = '') { $tokenName = $this->session->get($this, "name$id"); // Why can't it find token inside $_SESSION global? if(!$tokenName) { $tokenName = 'TOKEN' . mt_rand() . "X" . time(); // token name always ends with timestamp $this->session->set($this, "name$id", $tokenName); } return $tokenName; } I dont understand why it cannot find correct token and regenerates? Does session not last until browser closes? I can save other data to $session, and get it just fine, am I missing something?
-
Not a definitive solution, but you can use strftime for locale-specific dates. From its documentation then you'd do something like this. <?php function formatDate($format, $timestamp) { // set first possible locale from the list setlocale(LC_TIME, ['no_NO', 'no_NN', 'no_NB']); return strftime($format, $timestamp); } One thing to note is that you need to use strftime specific date format.
-
getModuleConfigInputfields add button field with different action
abdus replied to ---'s topic in API & Templates
Something like this works with Process type modules. First you create a submit button, change its name to your action <?php $submit = $this->modules->get('InputfieldSubmit'); $submit->name = 'action_name'; $submit->value = 'Action Title'; $submit->icon = 'save'; $form->add($submit); Then inside <?php public function execute() // or ready() or init() { $out = ''; // ... if($this->input->post('action_name')) { // do your thing $this->message('Did my thing'); $out .= 'Here are the results'; // ... } return $out; } To achieve the same thing inside plain modules, you need to check if($this->input->post('action_name')) inside ready() / init() function. However, Process modules are more suitable for these types of tasks. You get custom urlSegments that you can redirect to and process using execute[Segment] functions. Like so <?php namespace ProcessWire; class ProcessMyAction extends Process implements Module { public static function getModuleInfo() { return [ 'title' => 'My Action', 'summary' => 'Does my action when you need to', 'version' => '0.0.1', 'permission' => 'page-edit', 'icon' => 'cogs' ]; } public function execute() { /** @var InputfieldForm $form */ $form = $this->buildForm(); $out = $form->render(); if($this->input->post('action_name')) { $out .= 'Just did my thing'; } return $out; } public function executeHi() { // intercepts `./hi` url segment $this->message("Well hello"); return "<p>You clicked hi!</p>"; } public function executeBye() { // intercepts `./bye` url segment $this->setFuel('processHeadline', 'Goodbye?'); $this->error('Not so fast!'); return "<p>You clicked bye!</p>"; } public function ___install() { // set up process page that is reachable from dropdown menu try { $p = new Page(); $p->template = 'admin'; $p->parent = $this->pages->get($this->config->adminRootPageID)->child('name=setup'); // under /admin/setup $p->name = 'my-action'; $p->process = $this; // when clicked from dropdown menu, execute() function runs $p->title = 'My Action'; $p->save(); $this->message("Created action page at $p->path."); } catch (WireException $e) { $this->error('Cannot create action page'); $this->error($e->getMessage()); } } public function ___uninstall() { // delete admin page $p = $this->pages->get('name=my-action, has_parent=2'); if($p->id) { $p->delete(); $this->message("Removed Action page at $p->path"); } } protected function buildForm() { /** @var InputfieldForm $form */ $form = $this->modules->get('InputfieldForm'); /** @var InputfieldName $fName */ $fName = $this->modules->get('InputfieldName'); // ... $form->add($fName); $submit = $this->modules->get('InputfieldSubmit'); $submit->name = 'action_name'; // ... $form->add($submit); return $form; } }- 1 reply
-
- 3
-
-
- getmoduleconfiginputfields
- module
-
(and 1 more)
Tagged with:
-
Why not use an IDE instead of a text editor? Any IDE (at least PHPStorm) should analyze and index the core, your code and autocomplete to correct members of classes with close to zero configuration. PHPStorm, combined with functions api (set $config->useFunctionsApi = true inside config.php), is a godsend in this regard. Since Atom is just an advanced text editor, it (presumably) does not analyze your code, so it just autocompletes depending on what you write inside that file, (and not even inside the project). Using Atom to do an IDE's job is way over its head. Please correct me if I'm wrong, I'm extrapolating from my experiences with VS Code and Sublime Text, both of which are nowhere near an IDE.
-
Trying to save multiple page reference fields.
abdus replied to timothy.m.wilson's topic in API & Templates
Wild guess, maybe it's only finding the first blog post, and not the others. I'm not sure $blog_page->labels = $labelIDS; works, since it's assigning plain arrays to a PageArray (as all page fields --set to accept multiple pages-- are). Try this one. I replaced multiple calls to DB with $pages->findIds, which makes single call, so it's faster. I also replaced $blog_page->labels = $labelIDS; with PageArray::add($ids) method, which can take single id or array of ids (not documented but it works). As labels in your XML set as label1|label2, suitable as $pages selector, you dont need to split it. Uncomment echo statements to see what pages are the issue <row> <id>1041</id> <labels>lifestyle|savings</labels> </row> <?php foreach ($xml->row as $items) { //get page id to update and set output formatting to false $blog_page = $pages->get('id='.$items->id); if(! $blog_page->id) { // echo for debugging // echo "Cant find page with id $items->id <br/>"; continue; } // Get matching page ids at once, returns array of ids // https://processwire.com/api/ref/pages/find-i-ds/ $labelIDS = $pages->findIds("name=$items->labels"); if(! count($labelIDS)) { // echo for debugging // echo "Cant find labels $items->labels for page $items->id\n"; continue; } $blog_page->of(false); $blog_page->labels->add($labelIDS); $blog_page->save(); }- 2 replies
-
- 2
-
-
- page reference field
- php
-
(and 2 more)
Tagged with:
-
@ryanC, have you set field to accept single file, or with no limits? If you've set (or didn't change anything) Max files allowed as 0, then $match->result_pic is a WireArray, if it's 1, then it's a single Pagefile. Something like this should work. <?php foreach($matches as $match) { echo "<li><a href='$match->url'>$match->title</a>"; echo "<div class='summary'>$match->summary</div></li>"; // this should work no matter what // check if result_pic is a WireArray if($match->result_pic->count) { $url = $match->result_pic->first()->url; } else { // it's a Pageimage $url = $match->result_pic->url; } echo "<img src='$url'>"; If you know what type of image field this is, then you can remove checks with if statement, and use one or the other $url and echo directly. The reason it wasn't working is that PHP expects simple expressions inside double quoted string "" for string interpolation, so only $a, or $a->b works, the remaining parts are echoed as strings. This would mean echo "<img src='$match->result_pic->first()->url'>"; would only interpolate the first $match->result_pic part, and replace it (with presumably the id of the image). For more info http://php.net/manual/en/language.types.string.php#language.types.string.parsing
-
It's not for regeneration, but reading caches from DB together (by using preloadFor or by giving an array of cache names) into $cache instance (that was created at boot). This way no further calls are done to DB, they will be served from memory directly. I hope this part from the core makes it clear <?php // /wire/core/WireCache.php // Preloaded cache values, indexed by cache name protected $preloads = array(); /** * Preload the given caches, so that they will be returned without query on the next get() call * After a preloaded cache is returned from a get() call, it is removed from local storage. */ public function preload(array $names, $expire = null) { if(!is_array($names)) $names = array($names); $this->preloads = array_merge($this->preloads, $this->get($names, $expire)); } where $this->get() builds SQL query and fetches caches from database. So, if you're fetching caches once, it makes no difference to use $cache->get(['name', 'another']) or $cache->preload(['this', 'that']) then $cache->get(), but for multiple calls, it does. What you should be using to regenerate caches is just calling $cache->save() and it will regenerate and override previous caches. Edit: Considering the second sentence of the function documentation, the preloaded caches are flushed from the memory once they are requested with get(), so it's just useful for reducing DB calls to one.
- 1 reply
-
- 3
-
-
Yeah, that might be confusing somewhat. To summarize the changes: The part starting with $file = open($page->mp3->httpUrl, 'r'); should be <?php $filePath = $page->mp3->filename; $file = fopen($filePath, 'r'); fseek($filePath, $offset); $data = fread($filePath, $length); fclose($filePath); to read parts of the file client requested directly from the disk. While sending the file to the client, you should replace this part <?php // send partial file wireSendFile($page->mp3->filename, $options, $headers); } else { // send file wireSendFile($page->mp3->filename, $options); } with print($data); like so <?php // send partial file print($data); } else { // send file print($data); } Because you've set headers earlier, you dont need to set them again. With these changes, hopefully, you'll have saved server from wasting some precious bandwidth. Good luck
-
@benbyf, I am not sure if this is the whole code, but I want to point out some parts. <?php // you should use $page->mp3->filename to prevent unnecessary (and slower) network request // let PHP read file from the disk directly $file = fopen($page->mp3->httpUrl, 'r'); fseek($page->mp3->httpUrl, $offset); $data = fread($page->mp3->httpUrl, $length); fclose($page->mp3->httpUrl); // also, this forces client to download whole file wireSendFile($page->mp3->filename, $options, $headers); // whereas sending the parts you prepared is just cheaper print($data); So, the current implementation just sets the headers iTunes was checking for, but it sends whole file, not the parts client requested. This might total to an unnoticeable difference for lower traffic files, but you should opt for sending only the parts client requested. Just my 2 cents.
-
Merhaba @Cengiz Deniz, The problem is you're trying to iterate over children of some PageArray ($pages->find() returns PageArray), but only Page objects can have children like @Robin S pointed out. You should directly iterate over them like foreach($subs as $tree_item){} and you should be ok. Another problem would be that you're surrounding your selector inside double quotes. <?php $selector= 'template='.$template.', limit=4, sort=random'; $subs = wire('pages')->find("$selector"); // your selector would become "'template=...'" // you should $subs = wire('pages')->find($selector); // or just use double quotes for variable interpolation to strings $subs = wire()->pages->find("template=$template, limit=4, sort=random"); Now, I'll show you a different, but better (IMHO) way to tackle this. Instead of using a function, I prefer refactoring this to its own partial template whose only purpose is to render given category. And instead of manually counting columns, I prefer using array_chunk(). <?php namespace ProcessWire; // inside /site/templates/home.php // (or whereever you're building your categories) $category = $pages->get($catPageId); $categoryItems = $pages->find("template=$template, limit=4, sort=random"); $rendered = wireRenderFile('partials/category', [ 'category' => $category, 'categoryItems' => categoryItems, 'colCount' => 4 // or whatever ]); // then echo your rendered output anywhere echo $rendered; // Inside /site/templates/partials/category.php $colCount = $colCount ?? 2; // if not provided, default col count is 2 // divide posts into arrays of $colCount items. // you need standard PHP arrays, which you can get using getArray() method $chunked = array_chunk($categoryItems->getArray(), $colCount); ?> <style> /* separate your styles from markup, and even better, into its own CSS file */ .item { margin-bottom: 10px; padding-top: 10px; padding-bottom: 10px; } </style> <div class="category"> <!-- Render anything about category here --> <h2><a href="<?= $category->url ?>"><?= $category->title ?></a></h2> <!-- Then rows --> <?php foreach($chunked as $row):?> <div class="row"> <!-- Then items inside rows --> <?php foreach($row as $item): ?> <div class="item col-sm-<?= $colCount ?>"> <h3 class="item__title"> <a href="<?= $item->url ?>"><?= $item->title ?></a> </h3> <p class="item__summary"><?= $item->summary ?></p> </div> <?php endforeach ?> <div class="row"><!-- remember to close your rows --> <?php endforeach ?> </div> Hope this was helpful. Ask away if you have further questions.
-
If you have control over the server and you're able to install PECL module (pecl_http) on PHP, then http_send_file() function is the most straightforward way to implement partial downloads. Otherwise you have to implement it manually using a code similar to one in SO link above. http://php.net/manual/fa/function.http-send-file.php
-
It looks like you need to set some headers to enable byte range requests and check incoming requests for byte ranges that client is currently at. Check out this SO answer: http://stackoverflow.com/a/157447
-
Here's how you might dynamically create it with ProcessWire without tinkering with .htaccess files. Create a new template, call it robots, and set its URLs > Should page URLs end with a slash setting to no, and Files > Content-Type to text/plain. You should tick disable Append file and Prepend file options as well. Optionally set its Family > May this page have children to no, and Family > Can this template be used for new pages to one. Family > Optionally Set allowed templates for parents to home only. Create a new page under homepage, set its template to robots, and name as robots.txt. Create a new template file at /site/templates/robots.php, inside it you type <?php namespace Processwire; // render different robots.txt depending on your own conditions. if ($config->debug) { // use PHP_EOL to create multiline strings echo <<<PHP_EOL User-agent: * Disallow: / PHP_EOL; } else { echo <<<PHP_EOL User-agent: * Disallow: PHP_EOL; } and done. You should be able to see robots.txt at the url /robots.txt.
-
This is what I use. It groups items into an array with years as keys, then you iterate over them and display your output. <?php $items = $pages->find('selector'); $grouped = array_reduce($items->getArray(), function (WireArray $carry, Page $item) { $year = date('Y', $item->published); if (!$carry->has($year)) { $carry->set($year, new WireArray()); } $carry->get($year)->add($item); return $carry; }, new WireArray()); ?> <?php foreach ($grouped as $year => $yearItems): ?> <h2>Year: <?= $year ?></h2> <?php foreach ($yearItems as $item): ?> <!-- render your items of certain year here --> <?= $item->title ?> <?php endforeach; ?> <?php endforeach; ?>
-
Hmm, one surprising advantage would be addition of shortcodes like in Wordpress. Modules can require, (install if necessary), then hook into Hanna Code and define their own shortcodes to be used in fields. One example would be <?php class EmbedShortcodes extends Wire implements Module { public static function getModuleInfo() { return [ 'title' => 'Shortcodes', 'version' => '0.0.1', 'author' => 'abdus', 'summary' => 'Adds `embed` shortcode', 'href' => 'https://abdus.co', 'autoload' => true, // set to true if module should auto-load at boot 'requires' => [ 'TextformatterHannaCode', ], 'installs' => ['TextformatterHannaCode'], ]; } public function ready() { $this->addHook('TextformatterHannaCode::getPHP', $this, 'renderEmbed'); } protected function renderEmbed(HookEvent $e) { if($e->arguments(0) === 'embed') { $e->replace = true; $e->return = '<div>embedded output</div>'; } } } This works fine for predefined hanna codes, but, for arbitrary shortcodes, it requires a change in the logic of the module where it fetches available hanna codes from database here <?php protected function getReplacement($name, array $attrs, &$consume) { $database = $this->wire('database'); $sql = 'SELECT `id`, `type`, `code` FROM hanna_code WHERE `name`=:name'; $query = $database->prepare($sql); bd($name, 'before'); $query->bindValue(':name', $name, \PDO::PARAM_STR); $query->execute(); if(!$query->rowCount()) return false; // here if it cannot find the hanna code it does nothing. // rest of the function } It should be refactored instead into ___getReplacement(), which allows defining access to hanna codes outside the DB. Once done, we can just [[embed url=youtube.com/asdfgh]] to embed a video etc.
-
I made a PR for it. https://github.com/ryancramerdesign/ProcessHannaCode/pull/19 Changes include: Updating module to ProcessWire namespace. This requires standard library classes be prefixed with \ (PDO -> \PDO etc.) Changing internal references inside ProcessHannaCode for TextFormatterHannaCode to use the module instance in $this->hanna instead. Changing getPHP, getJS, and adding getText method for hooking. This allows hooking into TextformatterHannaCode::getPHP, TextformatterHannaCode::getJS and TextformatterHannaCode::getText. Inside these hooks you check for the hanna code name, then do as you wish, like so wire()->addHookBefore('TextformatterHannaCode::getPHP', function (HookEvent $e) { if($e->arguments(0) === 'refer') { $e->replace = true; $args = $e->arguments(2); // $e->return = wireRenderFile('refer', $args); $e->return = 'rendered template output'; } });
-
It's harder to edit files inside Ace editor where you can't get proper debugging, type hinting, autocomplete etc. So what I propose is a way to hook into the module. Then we could: <?php // filtered hook for some specific Hanna code wire()->addHook('TextformatterHannaCode::getPHP(name="random-post")', function(HookEvent $e) { // the arguments specified by the user, inside an associated array $args = $e->arguments(0); // build markup, // since $vars parameter is an array // we can pass it directly as variables for the template $e->return = wireRenderFile('template/file', $vars); $e->replace = true; }); and then inside a php file, you perform your checks, build your markup with given parameters, and return it. Making this function hookable is quite easy, this is enough <?php public function getPHP($name, $code, array $attrs) {} // would become public function ___getPHP($name, $code, array $attrs) {} // just prefixing alone probably woudn't allow filtering hooks though.
-
reusable block across multiple pages of same template
abdus replied to DarsVaeda's topic in API & Templates
All you need is a textarea field. You can store any kind of text (including HTML) inside textarea fields. Now, you probably have body field created for you, if you do, use it, if not create a textarea field named markup, and under Details tab change its editor to CKEditor. Create a new template for the reusable block named common. Add markup field (or body) to this template. If you need images etc add that field too. Create a new page with this template. Add your content. Inside the templates that will be using this block get common content by $common = $pages->get('template=common'): $block = $common->markup; // (or body) Then echo this block in anywhere inside your html. -
How to add HTTP header when using template caching?
abdus replied to abdus's topic in API & Templates
Second edit: hooking after Page::render works <?php wire()->addHookAfter('Page::render', function (HookEvent $e) { header('x: test'); // works });- 1 reply
-
- 3
-
-
I'm guessing since PW skips processing template files and serves directly from /site/cache/, HTTP headers I set inside templates are ignored. Is there a hook that I can use to run code just before cached page is sent to user? Would using /site/finished.php work? Edit: finished.php is run after page is sent, so it's not working
-
Multisites with same templates and different databases
abdus replied to shivrajsa's topic in General Support
Nope, no change <?php // doesnt work $config->paths->set('assets', $config->paths->site . '/assets-second/'); $config->urls->set('assets', $config->urls->site . '/assets-second/'); -
Multisites with same templates and different databases
abdus replied to shivrajsa's topic in General Support
Paths echo whatever you set them to, but try uploading a file, do you get /site/test/files directories? In my case I didnt get new directories even though paths seem to change, it kept uploading to /site/assets/files