Jump to content

ryan

Administrators
  • Posts

    16,715
  • Joined

  • Last visited

  • Days Won

    1,516

Everything posted by ryan

  1. A little bit of new fun stuff on the 2.3 dev branch: Anonymous functions and hooks You can now use PHP 5.3+ anonymous functions when attaching hooks. All the syntax below will work either from your template files, modules, or any Wire derived class. If using outside of template files or Wire-derived classes, then replace $this with wire(). Though anywhere that you can use $this you can also use wire(), so syntax is also a matter of preference. Example: Add a "hello()" function to all pages that returns "Hello World!": $this->addHook('Page::hello', function($event) { $event->return = "Hello World!"; }); Result: echo $anyPage->hello(); // outputs: Hello World! Example: Add a 'hi' property to all Users that contains the value "Hi [their name]!". $this->addHookProperty('User::hi', function($event) { $user = $event->object; $event->return = "Hi $user->name!"; }); Result: echo $user->hi; // outputs: Hi Ryan! Example: Add a grandparent() function to all pages. $this->addHook('Page::grandparent', function($event) { $page = $event->object; $event->return = $page->parent->parent; }); Result: $myPage = $pages->get('/about/news/categories/sports/'); echo $myPage->grandparent()->url; // outputs: /about/news/ Example: Save a message to a log file for each new page that is added. $pages->addHookAfter('added', function($event) { $page = $event->arguments(0); $this->message("Added new page: $page->path", true); }); Result: The message is added to log file /site/assets/logs/messages.txt when a new page is added. Example: Add a 'createdString' property to all Page objects that is the same as $page->created except that it outputs in a human readable format rather than a unix timestamp: $this->addHookProperty('Page::createdString', function($event) { $page = $event->object; $event->return = date('Y-m-d H:i:s', $page->created); }); Result: echo $page->createdString; // outputs 2013-04-30 03:05:00 This just touches on the possibilities with some overly simple examples. But you can use these anywhere you'd use any other hooks: from within modules, templates, or anywhere in ProcessWire. I think that the anonymous function syntax is particularly convenient within templates, when you can define your own helper functions in your head.inc or init.php or whatever file you have starting your templates. Don't forget you can add stuff in /site/templates/admin.php too in order to hook into things that happen during administration. Error and message logs ProcessWire has always maintained logs, but I'm guessing that most don't know how to write to them. So I setup a simpler syntax for you to save things to PW's system log files: $this->message("This text will be saved to /site/assets/logs/messages.txt", true); $this->error("This text will be saved to /site/assets/logs/errors.txt", true); This is the same as these functions worked before, except for the second "true" argument, which tells it to write to the log file. From the admin side, it'll still display these messages interactively. If you only want it to log and not display the messages in the admin, then replace the "true" with "Notice::logOnly". Though if you are doing this from templates, then you don't need to consider that since templates (other than the admin template) don't typically output ProcessWire's notices. --- Edit: I changed the wire() syntax in the examples to $this. Though either works from template files and modules, but I think $this syntax is more familiar to most.
  2. Since the password has to be retained to send to the service at runtime, there's not much point in trying to hash it. And if you encrypt it, who are you ultimately trying to prevent from seeing it? I suppose it depends on what the password is ultimately used for. But I don't think you should try to over think it too much because we're talking about one password for [presumably] a non-critical service… not a database of passwords for multiple users that are likely spread out over multiple services. The problems from storing passwords in plain text or loosely hashed become real when you are dealing with user accounts at some scale beyond yourself. But in your case, in order for someone to get to that single password, they will have had to already compromised the system and broken into the database. So long as you aren't building a banking application or something high security, I think it's reasonable to just store the single password in the module config? After all, the database password itself is ultimately in plain text on all web servers too. But it is secure enough for all of us to trust our sites to. If you find that the password you need to store really is something that needs more security than the database itself, let me know and I may be able to suggest a couple things. As for the inputfield, try using InputfieldText with ->attr('type', 'password'); rather than InputfieldPassword. InputfieldPassword assumes that the password is not reversible, so it doesn't attempt to re-populate the field with it.
  3. I'm embarrassed to admit that I send them myself, using a script I wrote more than a decade ago. It's always worked fine, so I've just stuck with it. But I agree with the guys that say it's better to use a service. Someday I will take that advice myself too. But if you want to use what I'm using, here you go… First you'll need a text file with your subscribers in it. 1 email address per line. It can be only a few, or it can be thousands. subscribers.txt bob@email.com jim@bigcompany.com mike@little-org.com Now you'll need the email content. You'll want 1 HTML file and 1 text file. Both should have the same content: template.html <html> <head> <title>test email</title> </head> <body> <h1>Test</h1> <p>This is a test email</p> </body> </html> template.txt TEST This is a test email email.php Place this PHP file in the same directory as the above files. Edit the DEFINEs at the top as necessary. I apologize in advance for this rather awful email solution, but hey it works, and that's why I haven't bothered to rewrite it in 10 years. To run, place the files on a web server and then load in your browser. It should start emailing immediately, once every 3 seconds. If you are sending thousands, then it might take several hours. But because it sends slow, it never seems to caught up in any filters. <?php /** * GhettoMailer v1.0 * * © 1994 by Ryan Cramer * */ define("NAME_FROM", '"Your Name Here"'); define("EMAIL_FROM", "you@domain.com"); define("REPLY_TO", "you@domain.com"); define("ERRORS_TO", "you@domain.com"); define("SUBJECT", "ProcessWire News & Updates - April/May 2013"); define("SECONDS", 3); // seconds delay between each email sent define("TEXT_ONLY", false); // set to true if not sending HTML email define("TEST_MODE", false); // set to true if just testing a send define("TEST_MODE_EMAIL", "you@domain.com"); // email to send to when testing define("SUBSCRIBERS_FILE", "subscribers.txt"); // file containing subscribers, 1 email per line define("TEMPLATE", "template"); // file containing email to send: template.html and template.txt define("X_MAILER", "GhettoMailer 1.0"); /**************************************************************************************/ ini_set("auto_detect_line_endings", true); function mailTextHtml($to, $subject, $message_text, $message_html, $headers) { // exactly like regular mail function except sends both text and html versions $semi_rand = md5(time()); $mime_boundary = "==Multipart_Boundary_x{$semi_rand}x"; $headers = trim($headers); // in case there is a trailing newline $headers .= "\nX-Mailer: " . X_MAILER . "\n" . "MIME-Version: 1.0\n" . "Content-Type: multipart/alternative;\n boundary=\"$mime_boundary\""; $message = "This is a multi-part message in MIME format.\n\n" . "--{$mime_boundary}\n" . "Content-Type: text/plain; charset=\"utf-8\"\n" . "Content-Transfer-Encoding: 7bit\n\n" . "$message_text\n\n" . "--{$mime_boundary}\n" . "Content-Type: text/html; charset=\"utf-8\"\n" . "Content-Transfer-Encoding: 7bit\n\n" . "$message_html\n\n" . "--{$mime_boundary}--\n"; $success = @mail($to, $subject, $message, $headers, "-f" . ERRORS_TO); return $success; } /**************************************************************************************/ $start = 0; if(!empty($_GET['start'])) $start = (int) $_GET['start']; $subscribers = file(SUBSCRIBERS_FILE); if(isset($subscribers[$start])) { $line = trim($subscribers[$start]); $total = count($subscribers)-1; $email = trim($line); $subject = SUBJECT; if(isset($_GET['pause'])) { $meta = ''; $content = "[$start/$total] Paused. <a href=\"email.php?start=$start\">Resume</a><br />"; } else { $meta = '<META HTTP-EQUIV=Refresh CONTENT="' . SECONDS . '; URL=./email.php?start=' . ($start+1) . '">'; $content = "[$start/$total] Emailing <u>$email</u><br />" . '<a href="email.php?pause=1&start=' . ($start+1) . '">Pause</a><br />'; if(TEST_MODE) $email = TEST_MODE_EMAIL; $headers = "From: " . NAME_FROM . " <" . EMAIL_FROM . ">\n" . "Reply-To: " . REPLY_TO . "\n" . "Errors-To: " . ERRORS_TO; $content .= "Subject: <b>$subject</b><br />"; $bodyText = file_get_contents(TEMPLATE . ".txt"); if(TEXT_ONLY) { mail($email, $subject, $bodyText, $headers, "-f" . ERRORS_TO); } else { $bodyHtml = file_get_contents(TEMPLATE . '.html'); mailTextHtml($email, $subject, $bodyText, $bodyHtml, $headers); } $handle = fopen("email.log", "a"); if($handle) { fwrite($handle, "[$start/$total]: $email\n"); fclose($handle); } } } else { $meta = ''; $content = "Subscriber emailing finished. $start emails sent."; } ?> <html> <head> <?=$meta; ?> </head> <body> <?=$content; ?> </body> email.php
  4. $body = preg_replace('{<script[^>]*>.*?</script>}is', '', $body); $body = preg_replace('{<!--.*?-->}is', '', $body); The key here is to change the default "greedy" matching to be "lazy" matching using the .* followed by a question mark: .*? That ensures that it will match only to the closest closing tag rather than the [default] furthest one. That way it won't wipe out legitimate copy. Also the "s" at the very end lets it traverse as many lines as needed to complete the match. Without that, it would only match opening and closing tags on the same line.
  5. The notices system actually doesn't have anything to do with jQuery UI other than that the default admin theme makes use of jQuery UI class names when generating the markup for notices. But for your own front-end, you can make use of the $notices API variable to output them however you want. It can be as simple as this: echo "<ul>"; foreach($notices as $notice) { $class = $notice->className(); $text = $sanitizer->entities($notice->text); echo "<li class='$class'>$text</li>"; } echo "</ul>"; Then you would want to style the two type of notices in your CSS: .NoticeMessage { color: green; } .NoticeError { color: red; }
  6. The value returned by $page->url() will reflect the URL of that page in whatever the user's language is. Because the LanguageSupportPageNames module sets the language based on URL, it doesn't matter what your user's language setting is. It only matters what URL they are at. But you can get the page's URL in any language by passing the language to the localUrl() function: $url = $page->localUrl($language); Also make sure you are running the 2.3.0 dev branch because this is a module in development, and the version in the dev branch is much newer. You may want to use LanguageSupportPageNames for production use until 2.3.1.
  7. If you don't find someone that wants to do it as a paid job, post more details in one of the other boards and we can help you figure it out on your own.
  8. You'd have to get into tens of thousands of pages, if not more, before you would be able to detect a speed difference. That's been my experience at least. MySQL has apparently optimized their LIKE terms very well, or have figured out how to optimize to an index when it's there.
  9. All modules now have their own short URL off mods.pw. The letter combo used is the base 62 value of the module's ProcessWire page ID minus 1000. For example "3y" translates to 1246. Just a way to keep them as short as possible.
  10. I don't think that many have done this, so we can't say for sure what the issues might be. One that I am aware of is a name conflict between WordPress's gettext() functions and our language functions. Though not sure if it still matters if you aren't using multi-language.
  11. Double check that all of your module files in /site/modules/ are getting copied over. I've experienced a similar error on occasion when I copy over a site, but forget that some of my stuff in site/modules/ was actually symlinks to a shared modules directory… and the modules didn't get copied over to the server. Also, if you aren't copying over the /cache/ and /session/ folders, then at least make sure that you at least re-create these folders on the server and make sure they are writable.
  12. ryan

    foot.inc

    The only reason the default profile uses a head (header) / foot (footer) naming convention is because this is what WordPress does, and it's not far off from what Drupal does. So the point is to provide some point of reference relative to other systems. It's not really the best approach to use once you know what you are doing… though it's also fine to use if it suits your needs.
  13. Regarding Approach #1, I think you'd want to use the LanguageSupportPageNames module included with PW 2.3.0 dev branch. I'm responding to your special cases with that context: You have checkboxes next to each language page name. This controls whether the page is published in each language. There is no need to keep track of this since the URL structure dictates the language. This also makes it very SEO friendly. ProcessWire takes care of setting the user's correct language for you, based on the URL. That's what code internationalization is for. For example, an editable footer: <p id='footer'><?php echo __('Thank you for visiting. Copyright 2013 by Somebody.'); ?></p> The following is replying to approach #2, separate trees: The structure is up to you. Whether or not you start your root with an "en/" for your default language is based entirely on how you structure your site. So if you don't want to have "en/" (for example) for your default language, then don't built off a page called "en". Also not necessary since structure would dictate language. You would examine the $page->rootParent to determine language and set it at the top of some common include file (like a head.inc or init.php): if($page->rootParent->template == 'language-home') { // or whatever your template is called $user->language = $languages->get($page->rootParent->name); } else { $user->language = $languages->get('default'); } The same about code internationalization above applies here. Anything static text you put in a __() function becomes translatable. If something is translatable then it is also editable, even for the default language if you want. If you still didn't want to have text originate from the template, then you could always create a settings page in each tree. I would probably just add my "footer_text" as a field in the language-home page.
  14. Update to the last post– I am adding this validation check to FieldtypePage so that the above won't be necessary in PW 2.3.1 and higher (it will throw an exception).
  15. I think you've got it right. Because a removeAll(); is an action that is ultimately reflected in the database when the page is saved (rather than when you called removeAll), it's a good idea to do that save() after the removeAll() so that it's not getting mixed in and potentially voiding the other operations. Also wanted to recommend adding validation to this: $page->invoice_terms = (int) $input->post->edit_status; Something like this: // using find rather than get ensures the item is retrieved with access control $item = $pages->find((int) $input->post->edit_status)->first(); // double check the item you are trying to add is what you expect by checking // something like the template or the parent, etc. if($item && $item->template == 'your-expected-template') $page->invoice_terms = $item;
  16. Also, you don't need anything more than this: foreach($page->side_bar_widgets as $widget) echo $widget->render();
  17. Your children() operation is just finding the pages that have at least one repeater item that matches the criteria. So you are doing the right thing by performing another find (or foreach) on the result. Just to rule out any possible localization, date conversion, or ProcessWire version issues, try replacing your 'date("Y-m-d");' with just 'time();', for both queries.
  18. Do you see posted comments on the backend? Try moving your renderForm(); somewhere before any output occurs, and stuff the result in a variable that you can output later. For example, try this somewhere before any HTML output: $commentsForm = renderForm(); Then in your comments rendering: <div class="comments"> <?=$page->comments->render(); ?> <?=commentsForm ?> </div> <!-- /comments --> Not sure this is ultimately necessary, but it might help to isolate the issue.
  19. Why not just put both in posts.php? I think it could be confusing to separate tags across files. Since you just want to wrap the pagination, I think what you want is ultimately this line in /site/templates/blog.inc, function renderPosts(): // if there are more posts than the specified limit, then output pagination if($posts->getLimit() < $posts->getTotal()) $out .= $posts->renderPager(); change to: // if there are more posts than the specified limit, then output pagination if($posts->getLimit() < $posts->getTotal()) $out .= "<div id='your-div'>" . $posts->renderPager() . "</div>";
  20. Actually $file->basename is there too. It's the same thing as $file->name. The $file->name is mainly there for consistency with other PW objects, in that we want everything in ProcessWire to reliably have a 'name' property. But I usually use $file->basename myself.
  21. When I post code in here, I'm usually writing it in the browser. That means it's a starting point that may need testing and debugging, rather than a full working solution. Those error messages should help to get to the bottom of it, but I think we need to see the code that is actually in use. Can you post or paste the file here?
  22. I've been working on something similar recently too. Let us know how it goes and if you run into any issues.
  23. LanguageSupport is a core module and has to be upgraded with the rest of the core. So make sure you are replacing your entire /wire/ directory, and not just /wire/modules/LanguagesSupport/. Most likely you didn't see the checkboxes because they are a component of InputfieldPageName. But don't just go upgrade that module–upgrade the entire core.
  24. I haven't tried this module with languages support, but thanks for letting me know that they are not compatible. You should be able to retrieve the $languages API variable via the wire() function, i.e. wire('languages') or wire()->languages are both valid syntax. $languages is a PageArray and can be iterated. Multi-language field types implement FieldtypeLanguageInterface. If you wanted to find all multi-language fields in the system, you could do this: foreach(wire('fields') as $field) { if($field->type instanceof FieldtypeLanguageInterface) { // this is a multi-language field } } Something else to mention about multi-language fields (all of which are text-based, currently) is that their value is actually an object of type LanguagePageFieldValue, rather than a string of text. Its __toString() value is reflective of the user's current language. When outputFormatting is on (as it is on the front-end), it is converted to a string automatically. But when outputFormatting is off (as it is in the admin) the value is a LanguagesPageFieldValue object. There is also another type of multi-language field known as a Multi-language alternate field.These are quite a bit simpler in that they are just a regular field with any fieldtype. But they are recognized as multi-language by name convention. If you have a field named "myfield" and another named "myfield_es", and you have a language installed named "es", then "myfield_es" is assumed to be the Spanish version of "myfield", while "myfield" is assumed to be the default language version. On the front-end (outputFormatting on) ProcessWire knows to substitute one for the other when the user's language matches the field name. If there is a lot of code involved in configuration of an autoload module, then I prefer to put it in a separate file so that it's not taking up any memory for the non-admin requests that don't need it.
  25. I think I understand the request, but feel this probably belongs as a module since it such a specific need. My opinion has always been that the core should only have features that would be regularly used in at least 30% of installations. Otherwise we run the risk of having too much, over-configuration, etc. There are actually some modules we now have in the core that aren't used in at least 30% of installations (anyone ever used FieldtypeCache or some of the lesser known Textformatter modules we include?), so these will likely be moved out of the core in an upcoming version of PW. When it comes to meeting specific needs, I prefer to update the core to add new hooks where necessary (for modules) rather than adding new functionality or configuration. I think your case is one that does sound like a useful capability, but I'd prefer to assist you in implementing it as a module rather than adding it to the core. I'm sure some others will find it useful too, so would like to see it in the modules directory too.
×
×
  • Create New...