-
Posts
24,413 -
Joined
-
Last visited
-
Days Won
84
Content Type
Downloads
Release Notes
IPS4 Guides
IPS4 Developer Documentation
Invision Community Blog
Development Blog
Deprecation Tracker
Providers Directory
Projects
Release Notes v5
Invision Community 5 Bug Tracker
Forums
Events
Store
Gallery
Everything posted by Rikki
-
Security should never be an afterthought for your community. All too often, site owners consider beefing up their security only when it's too late and their community has already been compromised. Taking some time now to check and improve the security of your community and server could pay dividends by eliminating the cost and hassle of falling victim to hacking in the first place. Let's run down 7 ways that you can protect your community with the IPS Community Suite, from security features you may not know about to best practices all communities should be following. 1. Be selective when adding administrators Administrator permissions can be extremely damaging in the wrong hands, and granting administrator powers should only be done with great consideration. Granting access to the AdminCP is like handing someone the keys to your house, so before doing so, be sure you really trust the person and that their role requires access to the AdminCP (for example, would moderator permissions be sufficient for the new staff member?). Don't forget to remove administrator access promptly when necessary too, such as the member of staff leaving your organization. Always be aware of exactly who has administrator access at any given time, and review regularly. You can list all accounts that have AdminCP access by clicking the List Administrators button on the System -> Security page. 2. Utilize Admin Restrictions In many organizations, staff roles within the community reflect real-world roles - designers need access to templates, accounting needs access to billing, and so forth. IPS4 allows you to limit administrator access to very specific areas of the AdminCP with the Admin Restrictions feature, and even limit what can be done within those areas. This is a great approach for limiting risk to your data; by giving staff members access to only the areas they need to perform their duties, you reduce the potential impact should their account become compromised in future. 3. Choose good passwords This seems like an obvious suggestion, but surveys regularly show that people choose passwords that are simply too easy to guess or brute force. Your password is naturally the most basic protection of your AdminCP there is, so making sure you're using a good password is essential. We recommend using a password manager application such as 1password or LastPass. These applications generate strong, random passwords for each site you use, and store them so that you don't have to remember them. Even if you don't use a password manager, make sure the passwords you use for your community are unique and never used for others sites too. 4. Stay up to date It's a fact of software development that from time to time new security issues are reported and promptly fixed. But if you're running several versions behind, once security issues are made public through responsible disclosure, malicious users can exploit those weaknesses in your community. When we release new updates - especially if they're marked as a security release in our release notes - be sure to update as promptly as you can so you receive the latest fixes. Your AdminCP will also let you know when a new version is ready for download. 5. Use .htaccess protection for your AdminCP In addition to IPS4's own AdminCP login page, you can set up browser-level authentication, giving you a double layer of protection. This is done via a special .htaccess file which instructs the server to prompt for authentication before access to the page is granted. IPS4 can automatically generate this file for you - simply go to System -> Security in your AdminCP, and enable the "Add a secondary admin password" rule. And it should go without saying, but to be clear: don't use the same username or password for both your .htaccess login and your admin account, or the measure is redundant! 6. Restrict your AdminCP to an IP range where possible If your organization has a static IP or requires staff members to use a VPN, you can add an additional layer of security to your community by prohibiting access to the AdminCP unless the user's IP matches your whitelist. This is a server-level feature, so consult your IT team or host to find out how to set it up in your particular environment. If you're a Community in the Cloud customer, contact our support team if you'd like to set up this protection for your account. 7. Properly secure your PHP installation Many of PHP's built-in functions can leave a server vulnerable to high-impact exploits, and yet many of these functions aren't needed by the vast majority of PHP applications you might run. We therefore recommend that you explicitly disable these functions using PHP's disable_functions configuration setting. Here's our recommended configuration, although you or your host may need to tweak the list depending on your exact needs: disable_functions = escapeshellarg,escapeshellcmd,exec,ini_alter,parse_ini_file,passthru,pcntl_exec,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,show_source,shell_exec,symlink,system Another critical PHP configuration setting you need to check is that open_basedir is enabled, especially if you're hosted on a server that also hosts other websites (known as shared hosting). If another account on the server is comprised and open_basedir is disabled, the attacker can potentially gain access to your files too. Naturally, Community in the Cloud customers needn't worry about either of these steps - we've already handled it for you! So there we go - a brief overview of 7 common-sense ways you can better protect your community and its users. As software developers, we're constantly working to improve the behind-the-scenes security of our software, but as an administrator, there's also a number of steps you should take to keep your community safe on the web. If you have any tips related to security, be sure to share them in the comments!
-
Theme Tip: Create custom error pages with the Pages app
Rikki posted a blog entry in Community Management
When IPS4 encounters an error (be it a simple 404 Not Found or a more complex configuration issue), the user sees a standard built-in error page. That's fine in many cases, but did you know you can create your own error page using our Pages app? This is a particularly good approach for communities that use Pages for their website too. If you have built a website theme, the standard error page may not fit with your visual style, so building your own error page allows you to improve it. You might want to show some helpful links to other parts of your website, for example. Creating your error page The first step is creating your error page in Pages. Note that for this page, you must create a manual page - the Page Builder tool can't be used in this case. In order to show the error on your page, there's two special tags you should insert in the page content. When your page is shown in response to an error, Pages will swap out these tags for the relevant text. They are: {error_code} Replaced with the technical error code for this error. This code identifies the exact piece of code that triggered the error, and can be given to IPS support technicians to help diagnose problems. {error_message} Replaced with a human-friendly description of the error that occurred. Configuring Pages to use the error page Next, set Pages to display the error page. You do this in the Pages section; click the Advanced Settings button, and select your page from the list. Note that this will replace all error pages across the suite - not just errors triggered by Pages itself! Have a request for a theme tip? Let us know in the comments and we'll try and help out in a future tip! -
Theme Tip: Use HTML logic to display content to specific groups
Rikki posted a blog entry in Community Management
HTML Logic is our name for the additional tags available in IPS4's templates that allow runtime logic to be executed. It comprises if/then/else statements as well as loops and more. Since HTML Logic has access to all of the underlying PHP framework in IPS4, it's very powerful and a lot can be achieved with it. One common use is to limit certain content within a template to particular member groups. Let's see how that might be done. Showing or hiding content only to guests We'll first look at a simpler idea: showing or hiding content specifically to guests (i.e. anyone who isn't logged in). Within IPS4, the \IPS\Member::loggedIn() object contains information about the current user. Guests always have a member_id of NULL (i.e. no value), so we can simply check that value in our logic tag: {{if \IPS\Member::loggedIn()->member_id === NULL}} This content *only* shows to guests, since they have a NULL member_id. {{endif}} {{if \IPS\Member::loggedIn()->member_id}} This content *only* shows to logged-in users since their member_id is a number, which will equal true. {{endif}} Showing content only to specific groups Let's go a bit further and this time show content to specific (primary) member groups. First, you need to get the IDs for the group(s) you want to deal with. You can find this by editing the group in the AdminCP, and making a note of the id parameter in the URL. On my installation, the Administrator group is ID 4 so we'll use that in our example. Once again, we're using the \IPS\Member::loggedIn() object, but this time we're using the member_group_id property. {{if \IPS\Member::loggedIn()->member_group_id === 4}} This content only shows to members in the "Administrators" group (ID 4 in our example) {{endif}} Working with multiple groups at once Following the code above, you could simply repeat the check against \IPS\Member::loggedIn()->member_group_id several times, for each ID you want to allow. However, since our templates allow arbitrary PHP expressions to be used, there's a neater way: use an array of member group IDs you want to allow, and check against that using PHP's in_array function. Here's an example where we only show content to group IDs 2, 4 and 6: {{if in_array( \IPS\Member::loggedIn()->member_group_id, array( 2, 4, 6 ) )}} This content only shows to members in groups with the ID 2, 4 or 6. {{endif}} Have a request for a theme tip? Let us know in the comments and we'll try and help out in a future tip! -
IPS Connect is a cross-domain single sign on and single point of authentication system that allows login credentials to be shared across multiple web applications. Furthermore, basic member management is also shared across those separate installations allowing you to manage users in one website and have those changes propagate to all of your other websites. While the IPS Community Suite natively supports IPS Connect with minimal configuration from the administrator, developers can also add IPS Connect integration capabilities to their own web applications as well. This guide outlines how to use IPS Connect within the Community Suite, as well as how to develop both "master" and "slave" IPS Connect applications. IPS Connect architecture IPS Connect comprises one master installation, and one or more slave applications. With this system, slave installations notify the master of any changes to data, and the master installation propagates this change to all other slaves. When any requests that are a part of IPS Connect are made by a slave application, they are always sent to the master application. The master application is then responsible for calling to all of the slave installations to notify them of any changes they need to be aware of (which means the master application must maintain a database of all slave applications that are connected). If you have an established site and a new site, the established site should be the master installation, and the new site should be the slave. Warning IPS Connect for IPS Community Suite 4 is not backwards compatible with IPS Connect for IP.Board 3. While we strived to make the upgrade process painless, the need to expand and improve IPS Connect meant that the 3.x API calls could no longer be processed while adding multiple new features to the set of existing features. Events propagated by IPS Connect Requests that IPS Connect will propagate to other installations include: Logging in Login requests are processed by the master application and credentials are shared across all sites in the network. Single sign on When a user signs in to one application they will be signed in to all other applications, even if those other applications live on different domains. Logging out When you log out of one application you are logged out of all applications. Registering When a new user account is added, it is added to all sites in the network. Changing usernames, email addresses and passwords These requests are propagated to all sites in the network. (Note: you can disable username changes from being shared amongst sites in the Community Suite - see below for details). Banning users When you ban a user on one site, the user is banned on all sites. Account validation If a user has registered and you require account validation, that user will be required to validate their account before they can access any site in the network as a fully registered user. Account deletion If a user is deleted, they will be deleted on all sites across the network. Account merges If two user accounts are merged, the merge will be copied to all sites in the network. Disabling username propagation You can disable username changes from propagating to all sites within a network. This can be useful when you want to share login credentials amongst all of your sites, but want user accounts to otherwise appear to be separate. To do this with the Community Suite you must create a file called constants.php in your root directory (where index.php is), or edit the existing one if it already exists. Paste the following code into the constants.php file (if you are editing an existing file, omit the opening <?php tag): <?php define( 'CONNECT_NOSYNC_NAMES', TRUE );
-
Introduction to responsive classes IPS4's CSS framework is responsive, meaning elements adapt according to the size of the display the users chooses to use. In most cases, the existing classes already outlined in this guide take care of it for you; for example, menus automatically adjust, and tab bars collapse into a dropdown menu on mobile phones. There may be times when you need to control on which devices sizes elements show or hide themselves. For example, if you add a custom footer element, you may want to only show it on desktop, and hide it from tablets or phones. The responsive classes that IPS4 provides allow you to control this kind of behavior. Responsive sizes used by IPS4 For the purposes of the media queries we use to control responsiveness, the following sizes represent each device: Phones - up to 767 pixels wide Tablets - between 768 pixels and 979 pixels wide Desktops - 980 pixels and wider Basic show/hide functionality The CSS framework includes two groups of three classes that show or hide elements on desktop, tablet and phone-sized devices, respectively. The classes act in an exclusive way; that is, if you use the show classes, any devices that don't match will not show the element. The opposite is also true; if you use the hide classes, then the element will not show on those devices but will show on the others not specified. The classes are: ipsResponsive_hidePhone ipsResponsive_hideTablet ipsResponsive_hideDesktop ipsResponsive_showPhone ipsResponsive_showTablet ipsResponsive_showDesktop You can combine these as needed. For example: <div class='ipsResponsive_hidePhone ipsResponsive_hideTablet'> This element *will not* display on phones or tablets, but *will* display on desktops </div> Additional classes to control element display When using the show classes outlined above, you typically need to include an additional class that controls how the element is rendered. This can be one of the following: ipsResponsive_block ipsResponsive_inlineBlock ipsResponsive_inline <div class='ipsResponsive_showDesktop ipsResponsive_block'> This element will *only* show on desktop sizes, and will render as a block-level element. </div> These additional classes aren't usually necessary when using the hide classes.
-
It is possible to create custom layouts for the Staff Directory page. You will need to write the templates using HTML, so to do this you will need to be familiar with HTML, as well as basic PHP logic. In the AdminCP, go to Customization -> Themes and click the "Edit HTML and CSS" button for your default theme. Then, from the "New" dropdown, choose "HTML Template". You can name the template whatever you like, and fill the rest of the form out with the following details: Variables: $users Location: Add to an existing location Existing Location: front Group: Add to an existing group Existing Group: staffdirectory Application: System You will then need to navigate to your newly created template within the many on the left (it will be under core -> front -> staffdirectory) and here is where you write the HTML code for your template. The template will be passed a $users variable which is an array of \IPS\core\StaffDirectory\User objects. Your code can include template logic and template tags You can use one of the existing templates as an example. After this you will also need to create another template with the same name (and settings above) but with "_preview" appended to the name (for example, if the template you just created is called "myCustomLayout" the template you create now will be "myCustomLayout_preview") which contains the HTML to display on the form when creating a Staff Directory group in the AdminCP. Since you know what the layout will look like, you can make this quite simple. You will need to repeat these steps for every theme you have installed (except child themes which will inherit the templates of their parents). Once this is done, when creating a Staff Directory group, you will see the template you have created as one of the available options.
-
IPS Community Suite has two special pages which exist entirely outside of the normal theme and language systems: error.html Displayed if there is an error so severe it is not possible to display a normal error screen (for example, if your database server is offline, it is not possible to obtain the theme and language details). upgrading.html Displayed while an upgrade is in process. You create different html pages to be used in these scenarios and instruct IPS Community Suite to use your pages instead. This will ensure that even if the contents of the files are changed in an upgrade, your customizations are maintained. To do this: Create the html pages as desired, and save them in the root directory of your community's installation. If you do not already have a constants.php in the root directory of your community's installation, create one with the following contents: <?php Open constants.php and add the following lines, replacing the names of the html files with the names of the files you have created: define( 'ERROR_PAGE', 'custom_error.html' ); define( 'UPGRADING_PAGE', 'custom_upgrading.html' );
-
Warning You should only change your cookie options if you have a genuine need. Incorrectly setting these options may cause issues for users in your community. Why you may need to override cookie options IPS Community Suite 4 intelligently detects the most appropriate values to set cookies for your site effectively and securely. For most users these default values work perfectly and you will not have a need to override the default settings. If you find, however, that these default options are not appropriate for your site (for instance, if you are integrating your Community Suite with an external website), then you can override the default detected options through the constants.php file. How to set up overrides in constants.php If you do not have a constants.php in your site root already, create one with just the following line at the top. This file can be used to set many different power-user level options (occasionally, some settings in the ACP will have you add to this file as well). <?php After this line you can set the following constants to override the default cookie options. Be aware that you should ONLY set the values you need to set, and leave the rest of the constants commented out. //define( 'COOKIE_DOMAIN', '.example.com' ); //define( 'COOKIE_PREFIX', 'prefix_' ); //define( 'COOKIE_PATH', '/' ); // If your front end website does not serve over SSL but your community suite does, you may need to set this //define( 'COOKIE_BYPASS_SSLONLY', TRUE );
-
The IPS Community Suite has the capability, which is enabled by default, to check that the IP address of the current request matches the IP address when the session was first started, and if the IP address has changed to treat the current visitor as a guest and force them to login again. This is a security precaution designed to prevent a user's session from being hijacked by someone else at a different location. While this is a good security precaution and should generally be left enabled, there can be times where this is undesirable in some hosting environments. For instance, if you are required to use the site through a proxy and the proxy IP address may change on every request, the IP address checking may prevent you from staying logged in. Changing Proxy IP Settings If you are having trouble staying logged in to your site due to this functionality, there are some settings you may need to adjust on your site. There is a setting in the ACP labeled "Trust IP addresses provided by proxies?" which allows the software to detect the original IP address of a user who is visiting through a proxy, and use that instead. In most cases, enabling this setting is what you will need to do to resolve the issue. This setting can be found in the following location in your ACP System -> Settings -> Advanced Configuration It is important to understand that enabling this setting can allow users to fake their IP address to avoid IP address bans set up in the software. On the same screen, there is a setting labeled "Check IP address when validating session?". This setting allows the software to verify that the IP address of the current request matches the IP address when the session was first started. In most environments, this should be left enabled, however in some situations (such as an intranet where all users visit from the same IP address) you may need to disable this setting to avoid session collisions. ACP IP Checks In some rare situations, you may have trouble logging in to the ACP due to the IP address checking with an error message "Your IP address does not match this session.". In order to stop this issue, you should try enabling the "Trust IP addresses provided by proxies?" setting, and if that does not resolve the issue then disable the "Check IP address when validating session?" setting, however in order to do so you must get logged in to the ACP first. There is a filesystem-level constant available that will allow you to temporarily disable IP address checking in order to get into the ACP and toggle the aforementioned setting. If you do not already have a file called constants.php in your Community Suite root directory (where conf_global.php is located), create one with an opening PHP tag like so: <?php // Constants go here Add the following line to your constants.php afterwards: define( 'BYPASS_ACP_IP_CHECK', TRUE ); Save this file, and you will now be able to access the ACP in order to change the "Trust IP addresses provided by proxies?" setting. After toggling this setting, remove the constant you just added and verify if you can stay logged in to the ACP. If so, you need not do anything else. If you continue to have trouble, add the constant again, log in to the ACP, and then toggle "Check IP address when validating session?" off and save. Afterwards, remove the constant from constants.php again and you should remain logged in to the ACP without further issue.
-
When you use custom templates for a Pages database, you'll often need custom CSS to go along with it to provide the styling. There's two main ways of doing this: CSS files within Pages Pages allows you to create CSS files, and then associate them with particular custom pages of your community (you create these in the AdminCP, under Pages > Templates > CSS). So simply create your CSS file, and associate it to the page that your database is displayed on. The benefit of this method is it applies to all themes, so it's great if you want your database to look the same on all themes. Of course, this is also the drawback - you can't easily use it for per-theme customization. Targeting the database classname in theme CSS Alternatively, you can target the database classname in your normal theme CSS files. When a database is inserted into a page, IPS4 helpfully adds a classname to the body element, which makes it really simple to style that page in particular. If your database key is myDatabase, then the classname added to the body element would be cCmsDatabase_myDatabase. Use this in your selectors and you can style everything exactly how you need: .cCmsDatabase_myDatabase .ipsButton_important { /* Style important buttons differently in this database, for example */ } Combine both methods! Of course, you can use both approaches when it makes sense. Create a CSS file within Pages for the basic structural styling that will apply regardless of which theme the user uses, and then in each theme target the database classname to customize it for that particular theme - perfect for the colors, font family and so on.
-
Many of the regular visitors to our community won't have failed to notice the new look we launched last week. Now that the dust has settled, I thought it was a good time to explain why we've made the change. Streamlined access to everything we offer Ever since IPS was founded in 2002, our community has been distinct from our website. The community is also where we kept all kinds of resources, from guides to the Marketplace. For those customers who know us well and enjoy hanging out in our community (and we have many who have been with us since that day in 2002!), this is no problem. Unfortunately, the downside is many new and potential customers didn't see everything we have to offer: all the wonderful addons our contributors offer, additional support resources, plentiful advice from other community administrators, and more. In addition, we've always used the default theme that our software ships with, but with our self-service demo system now being the primary way new customers get to try out our software, this has become less important. So, we took the decision to move some parts of the community to the website for more exposure and easier discovery by new visitors. We made some tweaks to our navigation so that finding these areas is easier than before. And, of course, we've brought the website header over to the community, giving it a fresher look and more consistent navigation, wherever you happen to be on our website. Of course, all of our website is built in IPS4, as you would expect. Whereas before our website existed on a separate installation, as part of the update we merged our community and website together. This means you can sign in from anywhere, see your notifications and so on. This is just the first step we've taken on improving what we offer and how we offer it. We have many plans in progress. You may have seen the theme tip we posted this week, which is the first in a series of regular tips we'll be sharing to help you get the most out of the IPS Community Suite. We'll also be highlighting some of the incredible work our customers do, whether it's a unique use of our software, or something in our Marketplace that adds a great feature. Stay tuned!
-
Occasionally, you'll want to be able to change the style of a particular element on a particular page, without affecting similar elements on other pages. For example, lets say you wanted to change how the .ipsPageHeader element looks in topic view, to make the title bigger, but without changing it for all other pages that also use .ipsPageHeader. Adding a classname - the wrong way One method would be to edit the template for topic view, add a classname to the element, and then create a style using that new classname as the selector. This works, but it has a drawback - because you've edited the template, IPS4 won't be able to automatically update it for you when you upgrade to newer versions of the IPS Community Suite. We always suggest you try and avoid editing templates directly, for this reason. Using page-specific selectors - the right way There's a better way - every page in IPS4 includes some special attributes on the body tag that identify the app, module and controller being viewed. You can use these to write a CSS selector to only target pages that match the ones you want. Going back to our example, we could target .ipsPageHeader in topic view like so: body[data-pageapp="forums"][data-pagemodule="forums"][data-pagecontroller="topic"] .ipsPageHeader { ...your styles } This works because topic view is generated by the topic controller, in the forums module, in the forums app. All pages in the IPS Community Suite follow this controller/module/application structure. You can mix and match these. If you want to style all page headers in the forums app only, you could simplify the above to: body[data-pageapp="forums"] .ipsPageHeader { ...your styles } You can find out the right values to use by going to the page you want to target, viewing the source in a tool like Web Inspector, and finding the body tag - look for the special data-page* attributes that are used for that page. By including these styles in your custom.css file in your theme, you can target specific elements on specific pages, without making it difficult to upgrade your community later.
-
The IPS Community Suite uses CKEditor to power its rich text editing capabilities. Often, when developing a theme for you own community or for distribution, you'll also want to style the editor to match. CKEditor itself is a very large and complex project. It has many hundreds of CSS classes, and explaining how to style each part of the editor is beyond the scope of this guide. We recommend you check out CKEditor's website if you need more information. That said, here's some tips to guide you. Using custom.css to style the editor Prior to IPS 4.1, we used CKEditor's older iframe mode. What this meant is that the entire text editor existed inside of an iframe on the page (although this was seamless and transparent to users). Since styles of a parent page do not apply to the contents of iframes, the only way to style the editor was to edit the CKEditor skin that we shipped with the product. This meant working outside of the IPS4 theme system, but more importantly it made distributing your changes more difficult because you'd also have to distribute your CKEditor skin. In IPS 4.1 onwards, we use the newer div mode. Instead of using an iframe, the editor is built inside a div element right on the page. This is great news for themers, because it means the CSS styles you create within IPS4 will be inherited by CKEditor automatically. So, to start styling the editor, you can simply open your custom.css file in your theme, and using a tool such as Google Chrome's Web Inspector (or the equivalent in your browser of choice), inspect the HTML that CKEditor generates and use that to develop your styling. When you save your custom.css file, you'll see it applies to the editor too. Above: inspecting CKEditor's generated HTML with Web Inspector allows you to see which CSS styles (right) are being applied to each part of the editor, helping you identify which class names you should be using in your own CSS. Or build a standalone CKEditor skin If you intend to make more substantial changes to the editor, you may still want to consider developing it as an actual CKEditor skin instead. CKEditor has a very mature skin framework that can be used for advanced changes. Consult the CKEditor for more information on creating a skin. If you go this route, you would export the CKEditor skin, and ship it with your IPS Community Suite theme. When an administrator installs your theme, they can install the CKEditor skin in the AdminCP too. So, that seems quite straightforward - in almost all cases, simply edit the custom.css file you use in your theme, and you can customize CKEditor to match your theme! But, there's gotchas... There are exceptions, of course. Even in div-mode, CKEditor still generates some iframes. For example, when you click a dropdown menu in the editor (e.g. Font), CKEditor actually builds an iframe for the menu. This introduces the same problems we discussed above, again! Unfortunately, there is no simple answer here. As before, styles you build into your custom.css file won't apply to these special areas where CKEditor uses an iframe. For many theme designers, this won't be a huge problem - being able to edit the 95% of CKEditor available to custom.css will be sufficient. But if you really do need to style the contents of those iframes, the only option is to do it within CKEditor's own skin system (since it loads those CSS files within its iframe). This isn't too problematic if you're only concerned with styling your own community. The CSS files CKEditor uses can be found in /applications/core/interface/ckeditor/ckeditor/skins/ips (if your theme uses the default CKEditor theme we provide). Edit the editor.css file in this directory to adjust the styles (this is a compressed CSS file, so add your own CSS at the end - don't edit existing CSS in the file!). If you plan to distribute your IPS4 theme, however, and you need to style these areas of CKEditor that still exist in an iframe, you'll need to go back to using CKEditor's skin system, and distributing a CKEditor skin with your theme.
-
The following is a complete example of a class using \IPS\Node\Model. This is a category node in the Downloads app. <?php namespace IPS\downloads; /** * Category Node */ class _Category extends \IPS\Node\Model implements \IPS\Node\Permissions { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Default Values */ protected static $defaultValues = NULL; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'downloads_categories'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'c'; /** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'position'; /** * @brief [Node] Parent ID Database Column */ public static $databaseColumnParent = 'parent'; /** * @brief [Node] Node Title */ public static $nodeTitle = 'categories'; /** * @brief [Node] ACP Restrictions * @code array( 'app' => 'core', // The application key which holds the restrictrions 'module' => 'foo', // The module key which holds the restrictions 'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix" 'add' => 'foo_add', 'edit' => 'foo_edit', 'permissions' => 'foo_perms', 'delete' => 'foo_delete' ), 'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4) 'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete" * @encode */ protected static $restrictions = array( 'app' => 'downloads', 'module' => 'downloads', 'prefix' => 'categories_' ); /** * @brief [Node] App for permission index */ public static $permApp = 'downloads'; /** * @brief [Node] Type for permission index */ public static $permType = 'category'; /** * @brief The map of permission columns */ public static $permissionMap = array( 'view' => 'view', 'read' => 2, 'add' => 3, 'download' => 4, 'reply' => 5, 'review' => 6 ); /** * @brief Bitwise values for members_bitoptions field */ public static $bitOptions = array( 'bitoptions' => array( 'bitoptions' => array( 'moderation' => 1, // Require files to be approved? 'comment_moderation' => 2, // Require comments to be approved? 'reviews_mod' => 4, // Reviews must be approved? ) ) ); /** * @brief [Node] Title search prefix. If specified, searches for '_title' will be done against the language pack. */ public static $titleSearchPrefix = 'downloads_category_'; /** * @brief [Node] Moderator Permission */ public static $modPerm = 'download_categories'; /** * @brief Follow Area Key */ public static $followArea = 'category'; /** * [Node] Get title * * @return string */ protected function get__title() { return \IPS\Member::loggedIn()->language()->get("downloads_category_{$this->id}"); } /** * [Node] Get whether or not this node is enabled * * @note Return value NULL indicates the node cannot be enabled/disabled * @return bool|null */ protected function get__enabled() { return $this->open; } /** * [Node] Set whether or not this node is enabled * * @param bool|int $enabled Whether to set it enabled or disabled * @return void */ protected function set__enabled( $enabled ) { $this->open = $enabled; } /** * [Node] Add/Edit Form * * @param \IPS\Helpers\Form $form The form * @return void */ public function form( &$form ) { $form->add( new \IPS\Helpers\Form\Translatable( 'cname', NULL, TRUE, array( 'app' => 'downloads', 'key' => ( $this->id ? "downloads_category_{$this->id}" : NULL ) ) ) ); $form->add( new \IPS\Helpers\Form\Translatable( 'cdesc', NULL, FALSE, array( 'app' => 'downloads', 'key' => ( $this->id ? "downloads_category_{$this->id}_desc" : NULL ), 'editor' => array( 'app' => 'downloads', 'key' => 'Categories', 'autoSaveKey' => ( $this->id ? "downloads-cat-{$this->id}" : "downloads-new-cat" ), 'attachIds' => $this->id ? array( $this->id, NULL, 'description' ) : NULL, 'minimize' => 'cdesc_placeholder' ) ) ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'cbitoptions_moderation', $this->bitoptions['moderation'] ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'cbitoptions_comment_moderation', $this->bitoptions['comment_moderation'] ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'cbitoptions_reviews_mod', $this->bitoptions['reviews_mod'] ) ); // etc... } /** * [Node] Save Add/Edit Form * * @param array $values Values from the form * @return void */ public function saveForm( $values ) { if ( !$this->id ) { $this->save(); } foreach ( array( 'cname' => "downloads_category_{$this->id}", 'cdesc' => "downloads_category_{$this->id}_desc" ) as $fieldKey => $langKey ) { \IPS\Lang::saveCustom( 'downloads', $langKey, $values[ $fieldKey ] ); unset( $values[ $fieldKey ] ); } foreach ( array( 'moderation', 'comment_moderation', 'reviews_mod' ) as $k ) { $this->bitoptions[ $k ] = $values["cbitoptions_{$k}"]; unset( $values["cbitoptions_{$k}"] ); } parent::saveForm( $values ); } /** * Get URL * * @return \IPS\Http\Url */ public function url() { return \IPS\Http\Url::internal( "app=downloads&module=downloads&controller=browse&id={$this->_id}", 'front', 'downloads_cat', $this->name_furl ); } }
-
Front-end permissions allow administrators to control which member groups can perform which actions (for example, being able to view the content items inside the node, creating new content items, commenting on content items, and so forth). Your nodes can have up to 7 different permissions, customizable depending on the needs of your application. Implementing permissions changes the behavior of a few node methods, and adds several new permission-based methods you can call. Configuring your model In order to use front-end permissions, your model needs to extend the \IPS\Node\Permissions interface, like so: class _ExampleModel extends \IPS\Node\Model implements \IPS\Node\Permissions { //... } Next, there's three properties you need to define on your model. public static $permType = 'string'; A unique (to your application) key that represents this set of permissions in the suite, e.g. 'forums' or 'categories'. public static $permApp = 'string'; Your application key, enabling the suite to associate your permissions to your application correctly. public static $permissionMap = array(); This property is an array that maps the permission keys you want to define to the permission index, which must either the value 'view' or an integer between 2 and 7. As noted above, you must at least define a key with view as the value; you don't need to define all 7 permissions if you don't plan on using them. As an example: $permissionMap = array( 'view' => 'view', 'read' => 2, 'add' => 3, 'reply' => 4, 'export' => 5, 'download' => 6 ); In this example, we define the required view key, as well as five other keys our application will use. The keys you define here are the means by which you will check permissions in other methods later, and also form part of the language string keys you'll define to name your permissions. Some keys are required if your models implement certain other functionality: view (can see the node) is always required (defined as the value 'view') read (can read content items) is always required add (can add content items) is always required reply (can reply to content items) is required if your content items can be commented on review (can review content items) is required if your content items support reviews The permission matrix interface in the AdminCP will show the permissions in the order you define them here. The column names are defined by the language string with the key perm__{$key} where $key is the key from the above array, so you will need to add these to your application's lang.php file. Changed behaviors after implementing \IPS\Node\Permissions Once implemented in your model, the following three methods will (by default) only return the child nodes that the currently-logged in member has permission to view: hasChildren() childrenCount() children() These methods take two optional parameters that allow you to control this behavior. The parameter signature is the same for all three; children() is shown here as an example. array children( [ string $permissionCheck='view' [, \IPS\Member $member=NULL ] ] ) $permissionCheck (string, optional, default 'view') Determines which permission is checked when working with children; should be one of the permission keys you defined earlier. The view permission is used if none is specified. $member (\IPS\Member, optional) Controls which members' permissions are checked. By default, the currently-logged in member is used, but you can override this and specify a different member here. In addition to the above, the loadAndCheckPerms() method will now throw an OutOfRangeException exception if a node is loaded that the currently-logged in member doesn't have permission to view. Additional methods boolean static canOnAny( string $permission [, \IPS\Member $member ] ) Returns true if the user has the requested permission on any node that uses this model. $permission (string, required) The permission type to check. $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member is used. boolean can( string $permission [, \IPS\Member $member ] ) Returns true if the user has the requested permission on the node object. $permission (string, required) The permission type to check. $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member is used. array permissions() Returns an array representing the node's row from the core_permission_index table, which contains which groups have which permissions. The array contains the keys perm_view and perm_2 through perm_7, representing the permission types you configured. Each of these values is a comma-separated list of group IDs that have this permission. For example: $permissions = $myNode->permissions(); $groupsWithPermission = explode( ",", $permissions['perm_2'] ); // perm_2 might be 'read' in our node foreach( $groupsWithPermission as $id ) { echo "Group #{$id} has permission to read items in this node"; } This method is primarily useful when constructing queries based on groups that have a given permission.
-
In most cases, you'll want to support Admin Restrictions in your model classes because in the IPS Community Suite, this is how site owners control access to various parts of the AdminCP among their staff. How Admin Restrictions work Admin Restrictions work by calling methods on your model that represent actions the administrator is taking. The method called in the model will return a boolean indicating whether the admin has permission to perform that action. The supported action methods are: canAdd() (i.e. can the admin add new nodes of this type) canEdit() (i.e. can the admin edit existing nodes) canCopy() (i.e. can the admin duplicate nodes) canManagePermissions() (i.e. can the admin edit user permissions for this model) canDelete() (i.e. can the admin delete nodes) The \IPS\Node\Model class defines these methods automatically for you. You'll create the admin restrictions you want to support in the Developer Center, and then map them to these action methods in your model. Setting up Admin Restrictions The first step is to use the Developer Center to create the restrictions for your app. Each controller within each module in your app can have separate restrictions. Restrictions are always shown as simple Yes/No fields, so are phrased as a question like "Can _____?". For example, "Can manage forums" or "Can edit feed imports?". There's a handful of supported permission types that you can implement. They are: access (i.e. can the admin access this node at all) manage (i.e. can the admin view this node) add edit copy permissions delete Note that access and manage do not have associated action methods (i.e. there is no canAccess or canManage method you can use in your model); instead, the Dispatcher will automatically check these permissions, if defined, and prevent access if they are not set to Yes by the site owner. Add a restriction by clicking the plus icon next to the relevant controller. You'll see a prompt to enter a key, which is how this restriction will be referred to in your code. It'll also form your language string for translating the restriction description, with the language key being r__{key} where {key} is the key you enter in this prompt. It is easiest if you keep restriction naming conventions, and set your key in the format {controller}_{permission}, where {controller} is the relevant controller in your model, and {permission} is one of the types in the list above. For example, in the forums controller for the Forums app, we have: forums_manage forums_add forums_edit ...and so on. Keeping this format makes setting up your model easy. Don't forget to add the relevant language strings to your application's language file. That's all you need to do in the AdminCP. Next, you'll set up your model. Configuring your model To implement the restrictions in your model, you need to add a static property: protected static $restrictions = array(); There's two required keys you'll need to add to this array: app - the key of your application module - the module which contains the restrictions you're applying You'll also need to supply at least one additional key, depending on the kind of behavior you want: prefix - if you named your restrictions using our convention, discussed above, you can simply specify the prefix here, and the model will automatically use the correct permissions. For example, if you named your restrictions forums_add etc., the prefix you'd specify for this value would be forums_. all - if you want to use a single permission for all of the actions, simply specify it using this value. map - if you want to manually specify which action should check which permission, add a value named map. This should be an associative array with the keys shown below. The value for each key should be the restriction key you created in the developer center that will be checked for that action. add edit copy permissions delete Overloading permission methods Although the base model defines and implements the action methods for you, there may be situations where you need to overload them in your own model class. For example, if we assume our app has a setting that means adding new child nodes makes no sense, we might do: // app/sources/ExampleModel/ExampleModel.php function canAdd() { if( $this->some_setting ) { // Can't add nodes if this hypothetical setting is true, so we force FALSE here return FALSE; } // Remember to call the parent method in other cases return parent::canAdd(); } Using the permission methods Most of the time, you won't need to manually use the action methods provided for admin restrictions. The \IPS\Node\Controller class automatically calls them as needed to build the interface, showing appropriate elements according to what the administrator is allowed to do. However, you can call them if needed, like so: $item = \IPS\forums\Forum::load( 1 ); echo $item->canAdd();
-
In most situations, nodes will allow other nodes to be children, thus forming a parent-child relationship with each other. An example of this is forums, where a forum can contain sub-forums, which can contain further sub-forums, and so on. Each forum is a node, and they exist as a tree structure, with parent and child forums. To support parent/child relationships, your node model simply needs to define a parent property (see below). Doing so will make a number of inherited methods available to your class. Additionally, administrators will be able to reorder and change the parents of nodes in the AdminCP (although this behavior can be configured). Configuring your model class public static $databaseColumnParent = 'string'; Simply define this property in your class to implement parent/child relationships. The value should be the name of the database column (without prefix) which contains the ID number of the node parent. public static $nodeSortable = boolean; If false (default is true), then nodes will not be sortable by administrators in the AdminCP. Configuring your controller class By default, your node controllers do not need any properties or methods added to support parent/child relationships. However, there are some properties you can define to control their behaviors. protected $lockParents = boolean; If true, nodes will not be able to be moved out of their parents. They'll only be able to be reordered within their existing parent. protected $protectRoots = boolean; If true, root nodes will not be able to become child nodes, and child nodes will not be able to become root nodes. Supported methods After implementing parent/child relationships in your model, the following methods will be available. public mixed parent() Returns the immediate parent node, or NULL if this is a root item with no parent. public \SplStack parents() Returns a stack of all parents (the immediate parent node, that node's parent, etc. up to the root node). public boolean hasChildren( [string|null $permissionCheck='view' [, \IPS\Member|null $member=NULL [, boolean $subnodes=TRUE [, mixed $where=array() ]]]] ) Returns true of false indicating whether this node has child nodes. $permissionCheck The permission key to check to make a node count (pass null to not check permissions) $member The member to use as the context when checking permissions (pass null to use the currently-logged in member) $subnodes Whether to only count immediate children. If false, all children of the node will be counted, regardless of depth. $where Additional where clauses to pass to the query public int childrenCount( [string|null $permissionCheck='view' [, \IPS\Member|null $member=NULL [, boolean $subnodes=TRUE [, mixed $where=array() ]]]] ) Returns the number of children to this node. Accepts the same parameters as hasChildren, above. public array children( [string|null $permissionCheck='view' [, \IPS\Member|null $member=NULL [, boolean $subnodes=TRUE [, array|null $skip=NULL [, mixed $where=array() ]]]]] ) Returns an array of child nodes. $permissionCheck The permission key to check to make a node count (pass null to not check permissions) $member The member to use as the context when checking permissions (pass null to use the currently-logged in member) $subnodes Whether to only count immediate children. If false, all children of the node will be counted, regardless of depth. $skip An array of child IDs to skip $where Additional where clauses to pass to the query public boolean isChildOf( \IPS\Node\Model $node ) Returns true or false indicating whether this node is a child (at any depth) of the provided $node.
-
The base class that your own node classes will extend is \IPS\Node\Model. This class provides a wide range of specialist methods for working with your node data. \IPS\Node\Model in turn extends \IPS\Patterns\ActiveRecord, providing standard ways for fetching and interacting with the underlying data in your database. <?php namespace IPS\yourApp; class _ExampleModel extends \IPS\Node\Model { //... } Your model can then be loaded in your controllers like so (see Active Records for more information): $item = \IPS\yourApp\exampleModel::load( 1 ); Specifying your class properties Node classes require a few static properties to configure their behavior. Many come from \IPS\Patterns\ActiveRecord. protected static $multitons = array(); Required. Inherited from \IPS\Patterns\ActiveRecord. Simply needs to be defined by subclasses of \IPS\Patterns\ActiveRecord. public static $nodeTitle = 'string'; Required. A language string key for a plural word that describes what your nodes are, for example "Forums" or "Categories". public static $databaseTable = 'string'; Required. Inherited from \IPS\Patterns\ActiveRecord. Specifies the database table that this ActiveRecord maps to. public static $databaseColumnOrder = 'string'; Required. Should be the column in your database table (without prefix) which contains the position number that nodes will be pulled in order of (the central code will handle setting and using the value, but you have to create a field for it - it should be an INT column). public static $databasePrefix = 'string'; Optional. Inherited from \IPS\Patterns\ActiveRecord. Specifies the field prefix this table uses. public static $databaseColumnId = 'string'; Optional (default: 'id'). Inherited from \IPS\Patterns\ActiveRecord. Specifies the primary key field of this database table. public static $nodeSortable = boolean; Optional (default: true) Determines whether nodes will be sortable by administrators in the AdminCP. public static $modalForms = boolean; Optional (default: false) Determines whether forms for adding/editing nodes in the AdminCP will show in a modal popup. Specifying your class methods In addition to the methods inherited from \IPS\Patterns\ActiveRecord, the \IPS\Node\Model class also provides additional methods (some of which are required in your own class). public void url() Required. Should return an \IPS\Http\Url object pointing to the location on the front-end where users can view the content items in your node. public void form( \IPS\Helpers\Form $form ) Required. Should define form elements on the $form object that will be used to display a form for adding/editing nodes in the AdminCP. public static array roots() Returns an array of nodes. If you implement Parent/Children relationships it will return only those with no parent. public static array search( string $column, string $query, string $order=NULL, mixed $where=array() ) Returns an array of nodes that match the search. public array getButtons( string $url [, boolean $subnode=FALSE] ) Should return an array of buttons to show in the node tree for nodes in the AdminCP. The base class defines the following buttons automatically (based on the configuration of the node model): add edit permissions copy empty delete You can define additional buttons. Be sure to call parent::getButtons( $url ) in your own class first to set up the default buttons. Getters & Setters Because \IPS\Node\Model extends \IPS\Patterns\ActiveRecord, getters and setters can be defined to handle certain properties of nodes. \IPS\Node\Model defines many for you, so you only need to define your own if you need to process fields in different ways. For example, you'll always define a get__title() method, but you'll almost never need to manually define a get__id() method. Property Description Get Set Default value $_id Should return the ID number of your node. The value of the “id” column in the database. $_title Should return the title of your node. Empty string $_description Should return the description of your node, or NULL if not applicable. NULL $_badge Can be used to return a badge to show when viewing nodes in ACP. See phpDocs. NULL $_icon Can be used to display an icon in the row when viewing nodes in the ACP. Return a CSS class name for the icon. NULL $_enabled If implemented, will add an “Enabled” / “Disabled” badge that can be clicked on to toggle status. You should implement this if your nodes have a concept of being enabled/disabled. NULL $_locked If you are using $_enabled, this can be used to specify that an individual node cannot be enabled/ disabled and is locked in it’s current status. NULL $_position Returns the position of the node. The value of the column in the database represented by $databaseColumnOrder $_items The number of content items in the node NULL $_comments The number of comments on content items in the node NULL $_reviews The number of reviews on content items in the node NULL Model forms When viewing nodes in the AdminCP, IPS4 automatically builds forms to add/edit them. You must therefore define a form method in your model that should build the form elements to show. For example: public function form( \IPS\Helpers\Form $form ) { $form->add( new \IPS\Helpers\Form\Translatable( 'category_title', NULL, TRUE, array( 'app' => 'yourApp', 'key' => ( $this->id ? "yourApp_category_{$this->id}" : NULL ) ) ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'category_example', $this->example ) ); } This builds a form with two elements - a Translatable field that allows the admin to set a localized title for the node, and a YesNo field for another (imaginary, in this case) property. Note: consult the form helper documentation for more information on building forms. When the form is saved, any fields which match columns in the database table will be set automatically. However, you may need to do additional work. In this example, because the title is translatable, it cannot be stored in a specific database column, so we need to store it in the language system. This can be done by overriding the saveForm() method. For example: public function saveForm( $values ) { // Need to do this as if we’re creating a new node, we // won’t have an ID yet and the language system will need one to // store the title. if ( !$this->id ) { $this->save(); } \IPS\Lang::saveCustom( 'yourApp', "yourApp_category_{$this->id}", $values['category_title'] ); parent::saveForm( $values ); }
-
When creating admin controllers designed to work with a particular node model (for example, the forum manager screen in the Forums app), IPS4 provides a special node controller you can extend to get a lot of automatic functionality, instead of building it yourself manually. This controller provides an interface for viewing and managing nodes (adding, editing, reordering etc.) \IPS\Node\Controller itself extend \IPS\Dispatcher\Controller, so all of the standard controller methods are still available to you. Using \IPS\Node\Controller The only requirement to use the node controller is to add a $nodeClass property to your model which defines the class that your model uses: namespace \IPS\yourApp\modules\front\yourModule; class _yourController extends \IPS\Node\Controller { /** * @brief Has been CSRF-protected */ public static $csrfProtected = TRUE; protected $nodeClass = 'IPS\yourApp\YourModelClass'; // ... Your controller methods and properties } No other methods are required in this controller (although you can add other request handlers if desired); the bulk of its functionality is automatic. Please see Security Considerations for further information about the csrf protection.
-
Nodes in IPS4 are a structural concept used to organize content items. They resemble a tree, with parent nodes containing child nodes, which may contain other child nodes or content items, and so on. In IPS4, nodes serve a range of uses where a parent/child relationships are required, but the most common use is as a category hierarchy where nodes represent categories and containers for content items. This is the use that we'll focus on in this documentation. Generally speaking, nodes are admin-created entities. The interfaces for managing them exist in the AdminCP. Note: Nodes do have the ability to have sub-nodes of a different class to themselves. Because this is complicated and not usually needed for content items, methods and properties pertaining to it have been omitted in this documentation. Example The most obvious example of a node structure is that of categories, forums and topics in the Forums app. In code, categories and forums are actually the same thing - they are nodes represented by the Forum model (the term 'category' is simply used as a way to clarify functionality for users). Therefore, a parent forum can have child forums, and those child forums can themselves have child forums, or contain topics which are the content items. The node models in IPS4 handle the forum structure, while the content models handle the content items (the content models are discussed in a separate section of this developer documentation). | Parent Forum (Forum node model) |-- Child Forum (Forum node model) |---- Topics (Topic content item model) |--- Child Forum (Forum node model) |--- etc. Composition IPS4 provides a number of classes that assist with implementing node functionality. Each part will be discussed in more depth in later sections, but they include: Model - \IPS\Node\Model Provides data access and manipulation methods for nodes by extending \IPS\Patterns\ActiveRecord Controller - \IPS\Node\Controller Extends the standard dispatch controller to add automatic support for interfaces to manage node items in the AdminCP. Helpers The most relevant helper for nodes is \IPS\Helpers\Tree which builds an interactive tree interface (although this helper isn't only limited to working with nodes).
-
In an MVC application, the Model is responsible for interacting with data, and handing it to the Controller. This is no different in IPS4. Generally speaking, an instance of a model refers to a thing. For example, if you had forums and topics, Forum would be a model, as would Topic. The model for each would then provide methods allowing you to interact with them. When designing models for your application, list the things your application works with - those are likely to be your models. Name & location A model name should always be a singular noun; that is, a model always refers to one thing. Model names should also always be PascalCase. Models belong in the /sources folder of your application. As discussed in Autoloading, within /sources, you should have a subdirectory that takes the name of your model, with the model itself being inside that subdirectory (along with any supporting modules). /sources/topic/topic.php - Incorrect: lowercase folder/model name /sources/Topics/Topics.php - Incorrect: plural model name /sources/Topic/Topic.php - Correct Once again referring to the Autoloading guide, the classname within the PHP file should be prefixed with an underscore. For example, if your file was named Topic.php, the model class it contains should be named _Topic. Base models There are a few base models that your own models will frequently extend. Each will be discussed in more depth - including a full overview of the methods they provide - in their respective sections later in this documentation. The important ones are: \IPS\Node\Model - Methods for working with nodes, a tree-like structure useful for things like categories (and much more) \IPS\Content\Item - Methods for working with content items, e.g topics, gallery images \IPS\Content\Comment - Methods for working with comments (including forum posts) \IPS\Content\Review - Methods for working with reviews These four base models all implement IPS4's Active Record pattern, providing a consistent interface for creating instances of your models and working with data within them. Example /exampleApp/sources/Counter/Counter.php A very simple standalone model that provides a method for storing and incrementing a number. <?php namespace IPS\exampleApp; class _Counter { protected $counter = 0; public function incrementCounter () { $this->counter++; } } /exampleApp/modules/front/example/index.php A controller that creates an instance of the above model, then calls the incrementCounter method it provides. namespace IPS\exampleApp\modules\front\example; class _index extends \IPS\Dispatcher\Controller { public function manage() { $myCounter = new \IPS\exampleApp\Counter(); $myCounter->incrementCounter(); } }
-
In an MVC application like IPS4, the job of the controller is to handle requests from users. They are, in effect, the middleman, requesting data from the models, doing necessarily processing, and handing it off to the view to display or otherwise output. As discussed in Routing & URLs, methods in controllers map directly to a URL. When you visit a URL like community.com/index.php?app=core&module=messenger&controller=messenger, the controller at /applications/core/modules/front/messenger/messenger.php is initialized to handle the request. All controllers exist in the /modules directory of an application. This directory has two sub-directories: /front - for modules that handle front-end (public) functionality /admin - for modules that handle admin control panel functionality Admin controllers can only be accessed when the user is logged into an account with administrator permissions. Anatomy of a controller Here is a basic controller: <?php namespace IPS\core\modules\front\example; class _example extends \IPS\Dispatcher\Controller { public function manage() { //... } public function otherMethod() { echo "Hello world"; //... } } In this example, our controller is called example. As a result, it is saved in a file named example.php, which allows IPS4 to locate it. Our namespace should be IPS\<app>\modules\<location>\<module>, where app is the key of your app, location is either admin or front, and module is the name of the module this controller belongs to. As discussed in Autoloading, the name of the controller class should be lowercase (matching the filename), and prefixed with an underscore; this allows IPS4 to properly locate and load your controller when needed. At a minimum, controllers should extend \IPS\Dispatcher\Controller. There are other classes you can extend instead for additional functionality, which will be discussed later. Methods inside a controller are request handlers, and are called when the do parameter in the URL matches the method name. In our example above, the URL community.com/index.php?app=core&module=example&controller=example&do=otherMethod would simply echo "Hello world". Special methods The execute method in a controller is run for every request to a handler in that controller. This means it's a good place to do any work that applies to all handlers - including CSS or JS files for the module, for example. If you define the execute method in your controller, you should always call the parent afterwards: public function execute () { // Your work... parent::execute(); } The manage method is the default handler in a controller. If no do parameter is present in the URL, then the manage method will be called. Non-request methods If your controller has methods that aren't request handlers (e.g. utility methods), you should prefix them with an underscore. These methods aren't accessible by URL like the standard methods. protected function _someHelper () { // A method that our request handlers can call } Advanced controllers While most controllers will extend the \IPS\Dispatcher\Controller class, there are other controllers that may be extended instead to provide additional functionality (and they in turn extend \IPS\Dispatcher\Controller themselves). They include: \IPS\Node\Controller Handles functionality for nodes - tree-like data structures used extensively in IPS4 \IPS\Content\Controller Additional functionality for IPS4's content items \IPS\Api\Controller Handles API requests (this controller does not extend \IPS\Dispatcher\Controller) These controllers will be discussed in more detail in later parts of this guide. CSRF Protection CSRF (Cross-site request forgery) is a type of web attack where a user is made to perform an action they did not intend to perform. In order to protect against this type of attack, a unique key is generated for each user and must be included with actions that make state changes, and then validated before those changes are saved. The Invision Community software makes it easy to do this. Any URL that performs a state change (e.g. updates a value in the database or deletes a row in the database) should perform a CSRF check before making that change. The first step is to include the user's CSRF key in the URL to the controller handler. When using the \IPS\Http\Url class to generate a URL, you can do this easily by calling ->csrf() against the URL object. If you are generating the URL using a template helper, you can simply pass csrf="true" in the tag. $url = \IPS\Http\Url::internal( "app=application&module=foo&controller=bar" )->csrf(); {url="app=application&module=foo&controller=bar" csrf="true"} This will add a URL parameter &csrfKey=(unique key) to the URL, which you can then easily check in your controller method like so: \IPS\Session::i()->csrfCheck(); This method will automatically show an error if the CSRF key is not valid. That's all you have to do! Keep in mind this practice should be done for any state change (any time the database is altered by following a URL in the software).
-
Before you begin developing plugins or applications for the IPS Community Suite, you need to enable Developer Mode. Developer mode causes the software to load required files directly from the filesystem, rather than cached versions or from the database. Warning Developer Mode will cause the software to run much slower than usual and may introduce security vulnerabilities. You should only enable Developer Mode if you are a PHP developer intending to develop Applications and Plugins and should only do so on a local installation that is not web-accessible Enabling Developer Mode Follow these steps to enable Developer Mode on your installation: Download the Developer Tools, making sure you download the correct version for the version of IPS Community Suite you are using. Developer Tools for pre-release versions may be available, so you may need to download an older version from the "Previous Versions" section. Extract the developer tools and move them to where IPS Community Suite is installed, merging with the existing files. There is a root "dev" folder, and "dev" folders for each application. If you do not have every IPS Community Suite application installed, you should delete the folders you don't need from the Developer Tools folder before copying. The presence of Developer Tools for uninstalled applications may cause errors. If you do not already have a constants.php file in the root folder of your installation, create one. Add the following line to your constants.php file: <?php define( 'IN_DEV', TRUE ); For more information on how to use the tools which become available when Developer Mode is enabled, and for more information on developing for the IPS Community Suite, see the developer documentation. Important notes The developer tools includes the files necessary for all IPS applications. If you are enabling developer mode on an install where third-party applications and plugins are present you will also need to obtain and apply the developer tools (i.e. the "dev" folder) for those from the author. Note that when you upgrade your installation, you will need to download the updated Developer Tools.
-
What is an active record? IPS4 makes extensive use of the active record pattern, where each active record object represents a row in your database, and the object provides methods for interacting with that row (including adding new rows). For example, since the \IPS\Member model returns an active record, we can fetch and interact with a member row in the database like so: $member = \IPS\Member::load(1); $member->name = "Dave"; $member->save(); IPS4's base active record class is \IPS\Patterns\ActiveRecord. Most of the model classes you'll work with (including \IPS\Node\Model, \IPS\Content\Item, \IPS\Content\Comment and \IPS\Content\Review) extend the active record pattern, meaning they all offer a consistent interface for working with your data. It's important you make use of the active record methods provided instead of accessing the database directly, as when you add additional features to your content item, the methods will perform more complicated tasks. Configuring ActiveRecord classes When writing a class that extends \IPS\Patterns\ActiveRecord in the inheritance chain (even if you're not extending it yourself directly), you need to specify some properties that allow the class to locate records in the database. They are: public static $databaseTable = 'string'; Required. Specifies the database table that this ActiveRecord maps to. public static $databasePrefix = 'string'; Optional. Specifies the field prefix this table uses. For example, if your table named its fields item_id, item_name, item_description and so on, then $databasePrefix would be set as item_. This allows for automatic mapping of properties to database columns. public static $databaseColumnId = 'string'; Optional (default: 'id'). Specifies the primary key field of this database table. Loading records static \IPS\Patterns\ActiveRecord load( int $id [, string $idField=NULL [, mixed $extraWhereClause=NULL ]] ) Example: $row = \IPS\YourClass::load(1); This method retrieves a row with the ID 1 from the database and returns it as an active record instance of your class. Results are cached, so you can call load for the same item multiple times, and it will only perform one query, returning the same object by reference on subsequent calls. $id (Integer; required) The primary key ID of the record to load. $idField (String; optional) The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId) $extraWhereClause (Mixed; optional) Additional where clause(s) (see \IPS\Db::build for details) Throws InvalidArgumentException if $idField does not exist in the table, or OutOfRangeException if a record with the given ID is not found. Note: this method does not check user permissions on the item being loaded. When writing a class that extends one of the content models (\IPS\Node\Model, \IPS\Content\Item, \IPS\Content\Comment or \IPS\Content\Review, for example), you should instead use loadAndCheckPerms when loading data for the front end. This is a method with the same signature as the load method above, but loadAndCheckPerms will throw an OutOfRangeException if the user does not have permission to view the record. static \IPS\Patterns\ActiveRecord constructFromData( array $data [, boolean $updateMultitonStoreIfExists=TRUE] ) Example: $row = \IPS\YourClass::constructFromData( \IPS\Db::i()->select(...)->first() ); If you have retrieved a database row by manually querying (for example, to fetch the most recent record), and you need to build an ActiveRecord object from the data, this method allows you to do so without having to call load (which would cause another database query). $data (Array; required) Database row returned from \IPS\Db, as an array. $updateMultitonStoreIfExists (Boolean; optional) If true, will update the current cached object if one exists for this ID Updating data on an ActiveRecord You can get or set the data for a record simply by setting properties on the object, which map to your database column names. If you configured the $databasePrefix property in your class, then you should not include the prefix when you reference properties. Example (assuming our database table contained columns named title and description): $item = \IPS\YourClass::load( 1 ); $item->title = "My record title"; echo $item->description; Note that to actually update the database, the save method should be called after you've made all of your changes (see below). If your class needs to perform processing on properties before getting or setting them, you can define getters or setters for each property by adding a method named get_<property> or set_<property>. Within these methods, you can access the raw values using the $this->_data array. Example that uppercases the title property when set: // YourClass.php public function set_title( $title ) { $this->_data['title'] = strtoupper( $title ); } // OtherClass.php $item = \IPS\YourClass::load( 1 ); $item->title = 'my title'; echo $item->title; //--> 'MY TITLE' Saving & deleting records void save() Example: $item = \IPS\YourClass::load( 1 ); $item->title = 'New Title'; $item->save(); After changing data in an ActiveRecord, save must be called in order to update the database. void delete() Example: $item = \IPS\YourClass::load( 1 ); $item->delete(); Deletes a row from the database. Cloning records You can clone records simply by using a clone expression. Internally, \IPS\Patterns\ActiveRecord ensures that primary keys are adjusted as needed. Note that you still need to call save after cloning in order to create the record in the database. $item = \IPS\YourClass::load( 1 ); $copy = clone $item; $copy->save(); echo $copy->id; //--> 2 Using Bitwise flags The ActiveRecord class implements a bitwise feature, allowing you to store multiple boolean values in a single field, without needing to add new fields (and therefore database columns) to your model. Under the hood, the bitwise field is stored as an integer. You define your bitwise flags as keys with a numeric value; this numeric value doubles for each new value you add (so the order would go 1, 2, 4, 8, 16, 32, and so on). You define your bitwise flags as a static property on your model, like so: public static $bitOptions = array( 'model_bitoptions' => array( 'model_bitoptions' => array( 'property_1' => 1, // Some option for this model 'property_2' => 2, // Another option for this model 'property_3' => 4 // A third option for this model ) ) ); In this example, our model's database table would have a column named model_bitoptions - and we use this name in the $bitOptions array to identify it. We're storing three options, but you can define more by following the pattern of doubling the value each time. It's very good practice to comment each option here to explain what it does. Your ActiveRecord model will automatically provide an \IPS\Patterns\Bitwise object for this column, which implements \ArrayAccess, allowing you to get and set values as it it were an array: /* Getting a value */ if ( $object->model_bitoptions[‘property_1’] ) { // ... } /* Setting a value - remember it can be TRUE or FALSE only! */ $object->model_bitoptions[‘property_2’] = FALSE; $object->save(); /* Getting database rows */ $rowsWithPropery1AsTrue = \IPS\Db::i()->select( ‘*’, ‘table’, \IPS \Db::i()->bitwiseWhere( \IPS\YourClass::$bitOptions['model_bitoptions'], 'property_1' ));
-
Errors are a natural part of any web application, and you will need to be prepared to show errors when appropriate. Invision Community software has several powerful built in features for error handling that you should familiarize yourself with in order to best handle unexpected (or even expected, but invalid) situations. The Invision Community software throws Exceptions when appropriate, and attempts to throw the most valid exception class possible (including custom exception classes that extend default PHP exception classes). This means that many times when you are calling built in methods within the suite, you will need to wrap those calls in try/catch statements. For instance, when making an HTTP request, the request could fail for any number of reasons, and this is normal - it is important to wrap the request in a try/catch block to ensure that any issues are handled properly. Failure to do so will typically result in a generic "Something went wrong" error being shown to the end user which both is unhelpful (what went wrong?) and often inappropriate (do we need to show an error to the end user?). try { // Do something here throw new \UnexpectedValueException; } catch( \UnexpectedValueException $e ) { // Do something else instead } It is important to catch the individual exceptions when possible and either show relevant error messages, log the information as needed, or perform another action if possible (in some cases, exceptions may be expected and can even be ignored). You should avoid catching the generic \Exception class unless absolutely necessary - it is better to catch 3 separate more specific Exception subclasses instead. When you need to show an error to the end user, there is an error() method in \IPS\Output to facilitate this need. /** * Display Error Screen * * @param string $message language key for error message * @param mixed $code Error code * @param int $httpStatusCode HTTP Status Code * @param string $adminMessage language key for error message to show to admins * @param array $httpHeaders Additional HTTP Headers * @param string $extra Additional information (such backtrace or API error) which will be shown to admins * @param int|string|NULL $faultyAppOrHookId The 3rd party application or the hook id, which caused this error, NULL if it was a core application */ public function error( $message, $code, $httpStatusCode=500, $adminMessage=NULL, $httpHeaders=array(), $extra=NULL, $faultyAppOrHookId=NULL ) The first parameter should be the language string key of the error message to be shown. A full error message to display is also acceptable, in the event the message is coming from a third party or you need to replace variables within the language string. The second parameter should be a unique error code that you generate (using your application key in the code is helpful to ensure it remains unique). The third parameter is the HTTP status code to return to the end user. Error messages should always have a 4xx or 5xx HTTP status code, and you should always use the most appropriate status code possible. For instance, if the user requested a page that is invalid, you would likely return a 404 status code, while a request for a page that the user does not have permission to view should likely return a 403 status code. The $adminMessage parameter allows you to show a different message to administrators vs regular end users. This is useful to guide administrators into resolving issues themselves. For example, if the reason an error is being thrown is because the feature is disabled, you may tell the end user "You do not have permission to access this page", but you may want to instead tell the administrator "This page cannot be shown because the setting XYZ is disabled in the Administrator Control Panel". The $httpHeaders array allows you to send custom headers with the HTTP response, if needed. For example, if the reason a user reached an error page is because they are rate limited, you may want to send a Retry-After header to indicate to the user agent when they can try again. The $extra parameter allows you to include additional information (such as a backtrace) which will be shown to administrators, and the $faultyAppOrHookId is typically omitted when you are manually calling the error() method. Be aware that you should call the error() method even if the current request is an AJAX request. The method will automatically handle this situation and return the error information through a JSON response which will be handled automatically (in most cases at least) by the core AJAX javascript libraries. Often times, when an unexpected error occurs you should also log some additional data to the system logs to help diagnose the problem later. Please review our logging documentation for more information about logging data to the system logs. Error Codes The first number (the severity) is all that matters. Beyond that, you're free to use whatever you want, though I would recommend using something which is vaguely in line with what we do for consistency. Out format is: ABC/D A is a number 1-5 indicating the severity: Severity Description Examples 1 User does something that is not allowed. Can happen in normal use. User did not fill in a required form element. 2 Action cannot be performed. Will not happen in normal clicking around, but may happen if a URL is shared. User does not have permission to access requested page; Page doesn't exist. 3 Action cannot be performed. Will not happen in normal use. Secure key doesn't match; User submitted a value for a select box that wasn't in the select box. 4 Configuration error that may happen if the admin hasn't set things up properly. Uploads directory isn't writable; Facebook application data was rejected. 5 Any error that should never happen. No login modules exist; Module doesn't have a defined default section. B is a single character indicating the application (or "S" for code outside any application): array( 'core' => 'C', 'forums' => 'F', 'blog' => 'B', 'gallery' => 'G', 'downloads' => 'D', 'cms' => 'T', 'nexus' => 'X', 'chat' => 'H', 'calendar' => 'L' ) C is a 3-digit number indicating the class in which the error occurred. We just started at 100 for each application and every time we need a new one, add a number on. It's just to make it unique - we don't reverse-lookup the number anywhere. D is then an identifier error within the class. The first error code in the class is given 1, if we need more than 9, the next one gets "A" then "B", etc. So, as a random example, the code "2X196/1" is a level-2 severity error in Commerce. It has the code "196/1" - "196" is \IPS\nexus\modules\front\checkout\checkout and "1" means it was the first error which was added. Some tips: Please don't just copy them blindly from existing code. Use descriptive error messages and make use of the ability to show a different error message to admins where appropriate. If it's a severity 4 error, you probably want to show an admin message. The HTTP status code you use is important so make sure you set that properly. Don't use HTTP 500 for an error code with severity 1 or 2 or a HTTP 4xx error for a code with severity 4 or 5. HTTP 404 and 403 will usually be severity 2, HTTP 429 and 503 will usually be severity 1.