Jump to content

ryan

Administrators
  • Posts

    16,793
  • Joined

  • Last visited

  • Days Won

    1,540

Everything posted by ryan

  1. Which version of PW are you running?
  2. This is now present on the dev branch. It throws an exception at save() time if the field contains pages that are outside the defined rules in the Inputfield.
  3. That particular setting is meant to be translated per language. If you add /wire/modules/LanguageSupport.module to your translatable files list in Setup > Languages > [language] > then you can adjust the setLocale setting as you see fit, without having to modify the LanguageSupport.module file. Basically, anything that appears in __('') or $this->_('') or $this->_n('') or $this->_x('') is translatable.
  4. In order to get a stack trace you either have to have debug mode on or be logged in as a superuser.
  5. I recommend using this method (requires 2.3 dev branch): foreach($languages as $language) { if(!$page->viewable($language)) continue; // language marked not available $url = $page->localUrl($language); echo "<a href='$url'>$language->title</a>"; }
  6. $config->scripts and $config->styles are meant to hold filenames, not entire tags. So you want to switch things around. $config->styles->add($config->urls->templates . "styles/mystyle.css"); <?php foreach ($config->styles->unique() as $style) { echo "<link rel='stylesheet' type='text/css' href='$style' />"; } ?> Also make sure that they are happening in this order. So if you are using a head.inc file, then you would want to make sure that your stylesheet file is added sometime before the include("./head.inc"). This by the way is one reason why I think using a main.php is a little better than head.inc/foot.inc.
  7. Enhanced debug mode The dev branch also includes an enhanced debug mode in the admin. It's all been cleaned up to look better and provide more info. It also includes a new section that outlines all of the runtime attached hooks currently active in the system:
  8. No. MySQL can execute hundreds of well optimized queries faster than one unoptimized one. And MySQL can often execute lots of simple queries faster than 1 complex one. ProcessWire takes advantage of this where it can. Query counting isn't useful anymore (if it ever was).
  9. You have to consider XSS anywhere that the [untrusted] user can enter content that will be saved and echoed back to other users. In a default ProcessWire installation with no 3rd party modules, there aren't many places to consider this. The admin is designed for trusted users only. The comments module is the only thing that accepts and stores untrusted user input. It won't store markup in comments. It filters by stripping markup before storing a comment, and by entity encoding output when displaying a comment. Also consider that ProcessWire is designed to let you do what you want to do. If you want to create a text/textarea field that lets you enter script tags, then ProcessWire won't stop you. There are plenty of legitimate uses for such things. There simply isn't anywhere that an untrusted user can enter markup. But HTMLPurifier might have a place in your individual installation. If you are dealing with untrusted user input and allowing markup (HTML) then HTMLPurifier is essential. We have an HTMLPurifier module, ready to use. Content is validated and sanitized server-side, not client side. Though on text-based fields entered in the admin, we make no assumptions about what you are using them for. Even scripts are a legitimate use case (consider video embedding, etc.). Some Inputfields may have client-side validation built-in to do one thing or another (TinyMCE is an example). Though ProcessWire doesn't care about client side validation. What sanitization/validation takes place depends entirely upon the Fieldtype. From the admin side, the text-based fieldtypes are designed to contain any text, including any kind of markup. This is one reason why ProcessWire's admin is focused on trusted users only. While I don't recommend using PW's admin with untrusted users, there may be situations where you have "mostly" trusted users. In such cases, you may want to avoid things like rich text fields and focus on LML-based text formatters (safe ones, like Textile Restricted or BBCode) or even HTML Purifier. Our new CKEditor module actually does use the HTML Purifier for inline mode, since inline mode markup is not entity-encoded when presented in the admin. This is a consideration for any text fields that you don't want to support markup. Such fields might include your 'title' and 'summary' fields, but would exclude something like your 'body' field, which is designed specifically to contain markup. ProcessWire uses a type of modules called Textformatters. For any field that you want to have runtime output formatting, you can add an HTML Entities (or other types) Textformatter from Setup > Fields > [your field] > Details. This is only applicable to text-based fields. For entity encoding output yourself, use $sanitizer->entities($str). Generally you wouldn't ever use this on your managed content stored in PW. Instead, you would use it when echoing back something that the user entered, like search query: $q = $input->get->q; if($q) { $results = $pages->find("body%=" . $sanitizer->selectorValue($q)); echo "<h2>You searched for: " . $sanitizer->entities($q) . "</h2>"; if(count($results)) echo $results->render(); else echo "<p>Sorry, no results were found.</p>"; } ProcessWire's API does not use SQL queries, unless you choose to use the $db variable. So if you want to execute SQL queries, then you should follow best practices towards avoiding SQL injection. However, there are very few scenarios where any of us use SQL in developing websites or apps in ProcessWire, so it's generally a non-issue. What you do have to consider instead is selector injection, even if it's not nearly as potentially dangerous as SQL injection. So that's why we have the $sanitizer->selectorValue(); function. Note the use of it in the code segment above. Yes, all ProcessWire forms are CSRF protected.
  10. 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.
  11. 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.
  12. 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
  13. $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.
  14. 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; }
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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.
  22. 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.
  23. 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).
  24. 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;
×
×
  • Create New...