Jump to content

Community

Leaderboard


Popular Content

Showing content with the highest reputation since 02/24/2009 in Articles

  1. 19 points
    bfarber

    Working with members

    If you are developing for 4.0 you will almost certainly need to interact with member accounts at some point. Thankfully, doing so is easy in 4.0. The primary member class in 4.0 is \IPS\Member. /* Get the currently logged in member */ $member = \IPS\Member::loggedIn(); /* Load a specific member by ID */ $member = \IPS\Member::load( 1 ); /* Load a specific member by email address */ $member = \IPS\Member::load( 'john@doe.com', 'email' ); /* Get a guest object, or create a new member */ $member = new \IPS\Member; Once you have an instance of \IPS\Member to work with, you can adjust values for that member account by simply setting properties on the object that correspond to the columns in the core_members database table. For instance, to change a member's language selection you can do the following /* Get the member reference */ $member = \IPS\Member::load( 1 ); $member->language = 2; $member->save(); You need to call the save() method after changing a value in order for it to be saved to the database, otherwise the change will only persist throughout the current page view (or until save() is called at some later point, especially if working with \IPS\Member::loggedIn()). If you are working with a guest object and you call save() against the object, a new member record will be stored to the database (i.e. this is how you create a new member). There are some getters and setters defined in this class to facilitate actions that need to occur. For instance, when setting the member's name, the member's seo name is automatically updated and the display name change log is automatically accounted for. Some special properties made available via getters: $member->group: This is a special getter (this is a property, not a method) that will return the member's group data. This method will automatically merge permissions from the primary group and one or more secondary groups, giving the member the best possible permissions based on their group memberships. $member->groupName: This will return the user's group name, formatted based on the ACP group configuration. $member->groups: This will return an array of all group IDs the member belongs to $member->real_name: This will return the member's name or an empty string for a guest, instead of returning the language string for 'guest' if the user is actually a guest. $member->birthday: This will return the member's localized birthday, taking into account the fact that providing a birth year is optional. $member->photo: This property returns the member's photo as an \IPS\Url object (which can be cast to a string to get the URL to the photo). The photo is made safe for viewing in the ACP as necessary. $member->rank: This method returns an array representing the title and image for the member's current 'rank' based on the ACP ranks configuration, taking into account the fact that both an uploaded image and "pip" images are supported. There are some methods in the \IPS\Member class you should be familiar with. $member->delete( $setAuthorToGuest=TRUE ): Call this method to delete a member. The only accepted parameter indicates whether or not to set any content authored by the member to a guest account if TRUE, or to delete that content if FALSE. Be aware that if you use any external login handlers that support deleting a member, the member will be deleted in those external databases as well. $member->flagAsSpammer(): This method will flag the user account as a spammer, removing or hiding their content based upon the administrator's configuration, and report the spammer to the IPS Spam Service (if configured to do so). $member->unflagAsSpammer(): This method will unflag the user account as a spammer, reporting the change in status to the IPS Spam Service if configured to do so. $member->age( $date=NULL ): Return the member's age. If an \IPS\DateTime object is supplied as the only parameter to the method, the age is calculated based on the supplied date. $member->location(): Return the member's current location on the site, if they are online. $member->isAdmin(): Return whether the member is an administrator or not. $member->isOnline(): Return whether the member is currently online or not. $member->isBanned(): Return whether the member is banned or not, including temporary bans. $member->inGroup( $group, $permissionArray=FALSE ): Check whether the member belongs to a group or set of groups. $group can be a group ID (integer), an instance of \IPS\Member\Group, or an array of group IDs or instances of \IPS\Member\Group. If the user is a member of ANY group in the array, the method returns TRUE. If $permissionArray is passed as TRUE, the member is checked against the member's permission array instead of the group IDs. $member->language(): Returns an instance of \IPS\Lang for the member based on their preference, or based upon automatic detection/default language setting if the member has not chosen a language. This is used to retrieve language strings, e.g. \IPS\Member::loggedIn()->language()->addToStack( 'language_string' ); $member->url(): Return the URL to the member's profile as an instance of \IPS\Http\Url. $member->acpUrl(): Return the URL to the edit member page in the ACP as an instance of \IPS\Http\Url. $member->link( $warningRef=NULL, $groupFormatting=FALSE ): Return an HTML link to the member's profile. If $warningRef is passed as a string, this will represent the reference key for warning the member (if the user is warned). $groupFormatting indicates whether or not to format the name displayed using the member's group formatting options set up by the administrator. $member->profileFields(): Return the custom profile fields for the member as an array of profile field objects. $member->contentProfileFields(): This is similar to profileFields() but instead only returns profile fields designed to display next to the user's content (e.g. in the left sidebar next to a post) $member->ipAddresses(): Return the IP addresses used by the member $member->markAllAsRead(): Mark the entire site as read for the member. $member->markersItems( $app, $key ): Fetch the item markers for the member for the given application and key. $member->markersResetTimes( $app ): Fetch the item marker reset times for the member for the given application. $member->warnings( $limit, $acknowledged=NULL, $type=NULL ): Retrieve the member's warnings. Limit can be passed as an integer to limit how many are returned. If $acknowledged is NULL, all warnings are returned, or TRUE or FALSE can be passed to limit the warnings to only acknowledged or unacknowledged warnings. If $type is passed as a string, will only pull warnings of that type. $member->reputation(): This method returns the member's reputation as a language string. $member->reputationImage(): This method returns the member's reputation image. $member->encryptedPassword( $password ): Encrypt a plain text password for the member using the member's salt (you may wish to update the salt before calling this for a new password). $member->generateSalt(): Generate a new salt for the member. $member->notificationsConfiguration(): Return the member's notifications configuration. $member->following( $app, $area, $id ): Determine if the member is following a specific app, area and ID combination (all values must be passed) $member->acpRestrictions(): Return the member's ACP restrictions to determine which areas of the ACP the member can access. $member->modPermissions(): Return the member's moderator permissions, if any. $member->reportCount(): Get the number of reported content items that the member can view in the report center. $member->isIgnoring( \IPS\Member $member, $type ): Determine if the member is ignoring another member. $type must be passed and should be one of 'signatures', 'topics' or 'messages'. $member->createMenu(): Fetch the member's create menu. This is cached (and cleared automatically when needed). $member->moderateNewContent(): Determine if new content created by the member should be moderated. $member->coverPhoto(): Return the member's cover photo, if any. $member->hasAcpRestriction( $app, $module=NULL, $key=NULL ): Determine if the member has an ACP restriction set for a given $app, $module and $key combination (only $app is required). If the member does have the restriction set, it means they can access the given area. $member->modPermission( $key=NULL ): Determine if the member has a given moderator permission available for their account (pass NULL to determine if the member has any moderator permissions available). $member->canWarn( \IPS\Member $member ): Determine if the current member can warn another member. $member->checkLoginKey(): Check that the member has a login key set and it is not expired. $member->recountNotifications(): Recount the member's unread notifications. $member->recountContent(): Recount the member's content item contributions. $member->recountReputation(): Recount the member's reputation count. $member->canAccessModule( $module ): $module should be an instance of \IPS\Application\Module and the method returns whether the member can access the module or not. $member->merge( \IPS\Member $otherMember ): Merge the current member with another member account (note that content merging occurs in the background and may not happen immediately; additionally, the member account that will be removed will not be deleted until all content is merged, this is queued). $member->checkPostsPerDay(): Check the posts per day restrictions to see if the member can visit. $member->checkGroupPromotion(): Check whether the member should be promoted to another group, and do so if so. Note that save() still needs to be called manually afterwards. $member->clearCreateMenu(): Clear the cached create menu for the member. Note that applications can define MemberSync extensions to perform specific actions at the following points: onDelete: This method in a MemberSync extension is called when a member is deleted (be aware that their content may still exist in the database and be queued for deletion) onCreateAccount: This is called when a new account is created. onProfileUpdate: This is called when an existing account is updated Extensions are created in the Developer Center of an application in the Admin Control Panel.
  2. 13 points
    This guide will demonstrate step-by-step creating how to create simple plugin making use of most of the features provided. For this guide, you'll be shown how to create a plugin which displays a message at the top of every page on the community. Useful links for further reading: Plugins - Details information about Plugins and the features provided. Step 1: Creating the Plugin To begin, you'll need to have a test installation in developer mode. Once developer mode is enabled, a "Create Plugin" button will appear in the Admin CP under System --> Site Features --> Plugin. Use this tool to create your plugin, after which you'll be taken into the Plugin Developer Center. Step 2: Creating the Theme Hook The easiest way to display a message at the top of every page is to create a theme hook. Theme hooks allow you to modify the content of a template. The template we'll be modifying is "globalTemplate" which is in the "global" template in the "front" location in the "core" application. Under the "Hooks" tab, create a new hook and choose "core --> front: global" as the template group. Once it has been created, click the edit button, then choose "globalTemplate" from the menu on the left. Choose "Select Element" to bring up the contents of the template and choose the point you want to hook on - a good place is to select <div id="ipsLayout_mainArea"> and then for the content position choose "Insert content inside the chosen element(s), at the start." For now, specify the contents manually: <div class="ipsMessage ipsMessage_information">This is the global message.</div> After saving you can go to the homepage and you will immediately see the message you've just created. Congratulations, you've just created a simple Plugin! The remaining steps will show how to expand this functionality. Step 3: Using Templates It's good practice to keep all HTML in templates. While the current approach works fine, the HTML content is buried within the code of your plugin, so if someone installing the plugin wanted to modify it in some way (perhaps change the CSS classes on it), it would be difficult to do so. Fortunately, creating a HTML template is really easy. If you look into the directory on your computer/server where IPS Community Suite is installed, you'll notice a "plugins" directory - inside this you'll find a directory has been created for your plugin with the name you specified in Step 1. Inside this is a folder navigate to dev/html. Within this folder, files you create will become available as a template. Create a file called "globalMessage.phtml" - and set the following as it's contents: <ips:template parameters="" /> <div class="ipsMessage ipsMessage_information"> Now using a template! </div> The first line is just to name any parameters that will be passed into the template - which is not needed in this case. Once the file has been created, edit your theme hook and change the contents it inserts to use this template by using this code: {template="globalMessage" group="plugins" location="global" app="core"} This is a template tag which pulls in the contents of a template. All templates created by plugins are created in the "plugins" group in the "global" location in the "core" application. Once this is done, you should see the message has changed to "Now using a template!" As a side-note, if you wanted to add CSS code, you can just add css files into the dev/css folder and they will automatically be included. Step 4: Settings & Language Strings Now you have a global message - but currently there's no way to customise what it says. It would be handy if the plugin had a simple setting in the Admin CP that the admin could use to change the contents. To create a setting - go to the "Settings" tab in the developer center for your plugin and add a setting - for the key, use "globalMessage_content", and set the default value to whatever you like. It's important to make sure that your plugin starts working straight after it installs so the admin knows it's working properly, so don't leave the default value blank. Creating the setting here allocates space for your setting in the database, but you still need to create a form where the admin can edit it. To do this, again look in the directory for your plugin on the filesystem - you'll see a file called "settings.rename.php", first rename this to "settings.php" then open it up. It already contains example code to get you going. Change the first line of code (the $form->add(...) call) to this: $form->add( new \IPS\Helpers\Form\Editor( 'globalMessage_content', \IPS\Settings::i()->globalMessage_content, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => 'globalMessage_content' ) ) ); This code is using the form helper. Now when you go to the Plugins area of the Admin CP, you'll see a new "edit" button next to your plugin, when clicked, this brings up a form with a place where users can fill in a message. There is however, a problem with your form. The label for the setting just says "globalMessage_content" - obviously this needs to be changed to something more useful, for which you'll need a language string. To create one, look in the directory for your plugin again and open up the dev/lang.php file. It will contain just an empty array - add an element to this array like so: $lang = array( 'globalMessage_content' => "Message", ); The label will now say "Message". Finally, you need to actually make the user-defined message show. To do this, open up the globalMessage.phtml file you created in step 3 and change it's contents to: <ips:template parameters="" /> {{if settings.globalMessage_content}} <div class="ipsMessage ipsMessage_information"> {setting="globalMessage_content"} </div> {{endif}} That adds some template logic which detects if there is a value for our setting and only displays the message if there is, and another template tag which gets the value of our setting. If you wanted to add your own CSS, you can create CSS files in the dev/css folder of your plugin directory - any files you create there will be included automatically. Step 5: Making changes to the database If we wanted to take this plugin even further, you could add a "close" button to the message allowing users to dismiss the message once they have read it. Whether or not any given user has dismissed the message is information can be stored in the database. Open up the dev/setup folder of your plugin directory. In here you'll find a file called "install.php" - this file is ran when your plugin is installed. If you need to make subsequent database changes in future versions of your plugin, you can create new versions in the developer center, and a file will be created for each version you create - you need to make sure you add whatever changes you need both to the install.php file and the appropriate upgrade file. Open up install.php and add this code inside the step1 method: \IPS\Db::i()->addColumn( 'core_members', array( 'name' => 'globalMessage_dismissed', 'type' => 'BIT', 'length' => 1, 'null' => FALSE, 'default' => 0, 'comment' => 'If 1, the user has dismissed the global message' ) ); return TRUE; This code adds a new column to the core_members table in the database, which is the table which contains information on all the members. The column created is a BIT(1) column (which means it can store only 1 or 0) - 0 will indicate the user has not dismissed the message (so it should show) and 1 will indicate they have - the default value is set to 0. Though this code will make sure when your plugin gets installed the column will be added, you'll need to run a query on your local database so that you have it during development. Run this SQL query using whatever your preferred method is for managing your database: ALTER TABLE core_members ADD COLUMN globalMessage_dismissed BIT(1) NOT NULL DEFAULT 0 COMMENT 'If 1, the user has dismissed the global message'; After the user dismisses the message, their preference will need to be reset if the admin changes the contents of the message. To handle this, add this line to your settings.php file in your plugin directory, just after the $form->saveAsSettings(); call. \IPS\Db::i()->update( 'core_members', array( 'globalMessage_dismissed' => 0 ) ); Step 6: Creating a Code Hook Now you've allocated space in the database where the flag will be stored, you need to write code to actually set that value, and link it up with the template. You'll need to add a method to a controller which will handle the user's click. A generic-use controller intended for this sort of thing is available - \IPS\core\modules\front\system\plugins - though theoretically, you could add the method to any controller. In the developer center, create a code hook on that class and open it up (either in the Admin CP or by opening the file that has been created in the hooks directory of your plugin directory) and add the following code inside the class: public function dismissGlobalMessage() { \IPS\Session::i()->csrfCheck(); if ( \IPS\Member::loggedIn()->member_id ) { \IPS\Member::loggedIn()->globalMessage_dismissed = TRUE; \IPS\Member::loggedIn()->save(); } else { \IPS\Request::i()->setCookie( 'globalMessage_dismissed', TRUE ); } \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); } It's important to understand thoroughly what this function is doing: It first performs a CSRF check. Because this is a controller method, it is executed automatically on accessing the appropriate URL. Because it does something which affects the user (it modifies a preference) it is essential that it have a CSRF check. If the check fails, execution will be halted automatically. It checks if the current user is a registered member. \IPS\Member::loggedIn() returns an \IPS\Member object for the current user - if the user is a guest (not logged in) the member_id property will be 0. If the user is logged in, it sets the globalMessage_dismissed property to TRUE and saves the member. Since \IPS\Member is an Active Record, this will edit the column you created in step 5 for the appropriate row in the database. If the user is not logged in, it sets a cookie so that even users who are not logged in can dismiss the message. It then redirects the user back to the page they were viewing, or if the server cannot provide the HTTP Referer, the home page. Now that the action has been created, you need to adjust the template to honour the preference, and add a button that links to the dismiss action. Modify the template you created in step 3 to: <ips:template parameters="" /> {{if settings.globalMessage_content and !member.globalMessage_dismissed and !isset( cookie.globalMessage_dismissed )}} <div class="ipsMessage ipsMessage_information"> <a href="{url="app=core&module=system&section=plugins&do=dismissGlobalMessage" csrf="1"}" class="ipsMessage_code ipsType_blendlinks ipsPos_right"><i class="fa fa-times"></i></a></span> {setting="globalMessage_content"} </div> {{endif}} Step 7: JavaScript You now have a completely functional plugin which displays a message to all users that they can dismiss. For a little bit of added flair, you can make it so when the user dismisses the message, rather than reloading the page, it performs the action with an AJAX request and then the message fades out. It's really important that you only think about JavaScript enhancements after the core functionality has been written, so that users who don't have JavaScript enabled can still use your plugin, and search engines can access any content your plugin provides. To do this, you need to create a JavaScript controller. In the dev/js directory of your plugin directory, create a file named "globalMessageDismiss.js" with the following contents: ;( function($, _, undefined){ "use strict"; ips.controller.register('plugins.globalMessageDismiss', { initialize: function () { this.on( document, 'click', '[data-action="dismiss"]', this.dismiss ); }, dismiss: function (e) { e.preventDefault(); var url = $( e.currentTarget ).attr('href'); var message = $(this.scope); ips.getAjax()(url).done(function(){ ips.utils.anim.go( 'fadeOut', message ); }).fail(function(){ window.location = url; }); } }); }(jQuery, _)); To explain what this is doing: Everything other than the contents of the initialize and dismiss functions is required code for JavaScript controllers. The ips.controller.register line specifies the name of the controller. When an element with a JavaScript controller attached to it is loaded (you'll attach it to the global message in a moment) the initialize function is ran - the best practice is to only set up event handlers here and handle events in other functions. An event is being set up here to fire when any elements matching the selector [data-action="dismiss"] (you'll add that attribute to the close button in a moment) are clicked, and it calls the dismiss function when this happens. This dismiss function first prevents the event from performing it's default action (as a click will of course take the user to the target URL), then sets the variables needed (the URL that the button points to and the message box. It then sends an AJAX request to the URL that the button points to - if it succeeds, it fades out the box, and if it fails, it redirects the user to it just as if there was no JavaScript functionality. To make it actually work, you need to specify that you want your message to use this controller. In your template, add data-controller="plugins.globalMessageDismiss" to the div element, and data-action="dismiss" to the a element. For completeness, we should also adjust our action a little so that is aware of AJAX requests - while this is not strictly necessary, if it is not done, it is possible that if the redirect has to redirect to a screen which will display an error message, that the AJAX request will see this response and assume the request failed. Open up the action.php file you created in step 6 and change the redirect line to: if ( \IPS\Request::i()->isAjax() ) { \IPS\Output::i()->sendOutput( NULL, 200 ); } else { \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); } Once this is done, closing your message will now fade out without a page reload. Step 8: Downloading Congratulations, you've just created your first plugin! You can download so that you can install it on other sites or distribute it through the IPS Marketplace from the developer center. For reference, a downloaded version of the hook, as well as a zip of the plugin directory for development are attached: Global Message.xml globalMessage.zip Global Message.xml globalMessage.zip
  3. 12 points
    Mark

    Form Helper (*)

    IPS Community Suite has a powerful Form helper class allowing developers to create forms easily, with automatic validation and security. Forms can include file uploads, can be tabbed, are HTML5 ready and have lots of other features. If you are asking for user-input, you should always use the form helper, never write such functionality manually. Your form code will usually look something like this: $form = new \IPS\Helpers\Form; $form->add( ... ); $form->add( ... ); if ( $values = $form->values() ) { // Form submitted } \IPS\Output::i()->output = $form; Adding form elements Adding an element a form is done by the $form->add() method. You pass it an object of the element you want - for example, to add a text input to your form, you can do: $form->add( new \IPS\Helpers\Form\Text('name') ); Some of the classes available are: \IPS\Helpers\Form\Text for normal text input \IPS\Helpers\Form\Editor for WYSIWG text input \IPS\Helpers\Form\Upload for file uploads \IPS\Helpers\Form\Date for dates \IPS\Helpers\Form\Select for a select box \IPS\Helpers\Form\YesNo for yes/no radio buttons The constructor for all of these classes is: /** * Constructor * * @param string $name Name * @param mixed $defaultValue Default value * @param bool|NULL $required Required? (NULL for not required, but appears to be so) * @param array $options Type-specific options * @param callback $customValidationCode Custom validation code * @param string $prefix HTML to show before input field * @param string $suffix HTML to show after input field * @param string $id The ID to add to the row * @return void */ public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL ) For all of the available classes, look at the files in the system/Helpers/Form/ directory. The values acceptable for $options are documented in the source code for each. Be aware that some extend others (for example CheckboxSet extends Select, and has the same $options). For example, to create a multi-select box you would do something like: $form->add( new \IPS\Helpers\Form\Select( 'my_select_box', NULL, TRUE, array( 'options' => array( 0 => 'Foo', 1 => 'Bar', 2=> 'Baz' ), 'multiple' => TRUE ) ); Some classes, due to their complexity have further documentation available: \IPS\Helpers\Form\Editor \IPS\Helpers\Form\Upload \IPS\Helpers\Form\Translatable Labels and Descriptions The $name property, in addition to being the name used for the HTML field, is also used for the label to display. The form helper will automatically look for a language string with the same key to use as the label. It will also look for a language string appended with "_desc" for the description. For example, if the $name for your field is "my_field", it will use the language string "my_field_desc" as the description. If a language string with that key doesn't exist, no description will be used. It will also look for a language string appended with "_warning" for a warning block (again if it doesn't exist none is shown). This is normally only ever used with toggles (see below) for example to display a warning when the user selects a particularly dangerous option. Validation Most classes will provide automatic validation, and their $options provide ways of customising this. For example, if you create an \IPS\Helpers\Form\Number element - it will automatically check if the value is a number, and you can use $options to control the maximum and minimum along with the number of allowed decimal points. The system will automatically display the form again with an inline error message if any of the elements don't validate with no extra code required from you. If however, you want to include custom validation, you can do this with the $customValidationCode property - you simply provide a callback method which throws a DomainException if there's an error. For example, if you wanted a number field where the number 7 is specifically not allowed you could do this like so: $form->add( new \IPS\Helpers\Form\Number( 'my_field', NULL, TRUE, array(), function( $val ) { if ( $val == 7 ) { throw new \DomainException('form_bad_value'); } } ) ); Toggles Some fields like radios, select boxes and yes/no fields provide a feature called "toggles" which allow you to show or hide other elements depending on the selected value. For example, you might have a yes/no field to turn a feature on, and only when it is set to "yes" do other settings related to it show. The options available for this depends on the field type. For example, YesNo has two options: togglesOn (which controls which elements to show when the setting is set to "Yes") and togglesOff (which controls which elements to show when the setting is set to "No"). Select has one toggles option which accepts an array, specifying which elements should show for each of the available values. Number has an unlimitedToggles which specifies which elements show when the "Unlimited" checkbox is checked and a unlimitedToggleOn option to reverse that behaviour to when the checkbox is unchecked. For more information, see the source code for each element type. All of these options accept the HTML ID for what they should show/hide. To make other form elements show/hide you will need to provide IDs for them in the constructor. For example, this form has a YesNo field which when set to "Yes" shows a text input field: $form->add( new \IPS\Helpers\Form\YesNo( 'yes_no_field', NULL, TRUE, array( 'togglesOn' => array( 'text_field_container' ) ) ) ); $form->add( new \IPS\Helpers\Form\Text( 'text_field', NULL, TRUE, array(), NULL, NULL, NULL, 'text_field_container' ) ); Handling Submissions When your form is submitted $form->values() will return an array with the values of each element (if the form has not been submitted or validation fails, it returns FALSE). The value returned for each element depends on the type, and sometimes the options. For example, an \IPS\Helpers\Form\Text element always returns a string as it's value. However, \IPS\Helpers\Form\Number might return an integer or a float. \IPS\Helpers\Form\Upload, on the other hand, returns an \IPS\File object (or even an array of them if it's a multiple file upload field). If you prefer to only receive string values (for example, you want to save the values as a JSON object), you can pass TRUE to the $form->values() method. Tabs, Headers and Separators The \IPS\Helpers\Form object provides a number of other methods to create tabbed forms, include headers, etc. For example: $form->addTab('Tab1'); $form->addHeader('Header'); $form->add( new \IPS\Helpers\Form\YesNo( 'yes_no_field' ) ); $form->add( new \IPS\Helpers\Form\Text( 'text_field' ) ); $form->addHeader('Header'); $form->add( new \IPS\Helpers\Form\Text( 'another_text_field' ) ); $form->addTab('Tab2'); $form->add( new \IPS\Helpers\Form\Select( 'select_field', NULL, FALSE, array( 'options' => array( 0, 1, 2, 3 ) ) ) ); For more information on the available methods, see the phpDocs in \IPS\Helpers\Form. Custom Display HTML Casting the $form object to a string returns the HTML to display the form. By default, the form is "horizontal". To use "vertical", or to apply any other classes to the form, you can do: $form->class = 'ipsForm_vertical'; For further customisation, you can call $form->customTemplate() passing a callback with a template to use. This allows you to totally customise the look of the form. A common use of this is to use a template that looks better in modals: \IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) ); If you want to create a custom template, you could use the popupTemplate as an example. Advice and Best Practices Forms make up a large portion of the UI within the IPS Community. It is important to remember to present a UI that is consistent with other areas of the suite. To this end, we recommend the following best practices: Always phrase settings in the positive. For example, say "Enable feature?", don't say "Disable feature?". "Yes" should always mean something is "On". Make labels short and concise and use descriptions only if necessary. For example, don't have a field where the label is "Enable feature?" and the description is "Set this to yes to enable the feature" - that description isn't necessary. Use prefixes and suffixes rather than adding information to the label or description where possible. For example, don't have a label that says "Number of days before deleting" - make the label "Delete after" and the suffix that appears after the field say "days". Never refer to other settings in labels or descriptions. For example, do not have a description that says "Only applies if the above setting is on". Use toggles to indicate this to the user. Never make entering a particular value do something special. For example, do not have a description that says "Leave blank for unlimited" - use an unlimited checkbox or a separate setting which toggles other settings.
  4. 10 points
    Mark

    Commerce: Custom Actions (*)

    IP.Nexus for IP.Board allowed each Package to specify a "Custom module" - a PHP script which could specify custom code to run when purchases of that package were made, expired, cancelled, etc. In IPS Community Suite 4, it is possible to overload any class in the suite, so this is no longer a specific option for packages. But it is easy to recreate. Step 1: Create a plugin You will need to create a plugin, which requires you to have developer mode enabled. We strongly advise against installing developer mode on a live installation so do this, and your development on a test installation. Once developer mode is enabled, a "Create Plugin" button will appear in the Admin CP under System --> Site Features --> Plugin. Use this tool to create your plugin, after which you'll be taken into the Plugin Developer Center. Once in the Plugin Developer Center you will create a Code Hook on the \IPS\nexus\Package class (for more information about Code Hooks and other Plugin features see http://community.invisionpower.com/4docs/advanced-usage/development/plugins-r71/). Step 2: Write your code The content of the plugin will be very similar to the custom module from before, however there are some key differences: The parameters for the methods are different. Rather than arrays, you will be passed objects for purchase and other data. Since your code is running on an instance of a package, you will not be passed $package and should use $this instead. A full list of the available methods and their signatures are below. Because your code will run for every package, you must first check the ID number ($this->id) to see if it is a package you want to run the code for. You are overloading a method which may be doing things itself, so you need to call the parent:: method within your code. If you were using the IP.Board 3 API, you will of course need to update your calls to use the IPS Community Suite 4 API. For example, if this was your custom module on IP.Nexus for IP.Board 3: <?php class custom_actions_FOO { /** * Purchase record generated (run after onPaid) * * @param array The member purchasing * @param array Package data (combined array with row from nexus_packages and nexus_packages_*, depending on the package type) * @param invoice Invoice Model * @param array Row from nexus_purchases [since Nexus 1.5] * @return void */ public function onPurchaseGenerated( $member, $package, $invoice, $purchase ) { ipsRegistry::DB()->insert( 'purchase_log', array( 'member' => $member['member_id'], 'package' => $package['p_id'], 'purchase' => $purchase['ps_id'], 'time' => time() ) ); } } Then your code hook for IPS4 will look something like: <?php class hook { /** * On Purchase Generated * * @param \IPS\nexus\Purchase $purchase The purchase * @param \IPS\nexus\Invoice $invoice The invoice * @return void */ public function onPurchaseGenerated( \IPS\nexus\Purchase $purchase, \IPS\nexus\Invoice $invoice ) { if ( in_array( $this->id, array( 1, 2, 3 ) ) ) { \IPS\Db::i()->insert( 'purchase_log', array( 'member' => $purchase->member->member_id, 'package' => $this->id, 'purchase' => $purchase->id, 'time' => time() ) ); } return parent::onPurchaseGenerated( $purchase, $invoice ); } } Step 3: Download the plugin and install Once you're ready, download your plugin from the Plugin Developer Center which will generate an xml file. Go to your live installation and install this plugin there. Appendix: Available Methods You are overloading \IPS\nexus\Package, so you can specify any methods which are part of that. The most common use-cases however are: /** * On Purchase Generated * * @param \IPS\nexus\Purchase $purchase The purchase * @param \IPS\nexus\Invoice $invoice The invoice * @return void */ public function onPurchaseGenerated( \IPS\nexus\Purchase $purchase, \IPS\nexus\Invoice $invoice ) { } /** * On Renew (Renewal invoice paid. Is not called if expiry data is manually changed) * * @param \IPS\nexus\Purchase $purchase The purchase * @param int $cycles Cycles * @return void */ public function onRenew( \IPS\nexus\Purchase $purchase, $cycles ) { // This method is only available since 4.0.8 } /** * On Expiration Date Change * * @param \IPS\nexus\Purchase $purchase The purchase * @return void */ public function onExpirationDateChange( \IPS\nexus\Purchase $purchase ) { } /** * On Purchase Expired * * @param \IPS\nexus\Purchase $purchase The purchase * @return void */ public function onExpire( \IPS\nexus\Purchase $purchase ) { } /** * On Purchase Canceled * * @param \IPS\nexus\Purchase $purchase The purchase * @return void */ public function onCancel( \IPS\nexus\Purchase $purchase ) { } /** * On Purchase Deleted * * @param \IPS\nexus\Purchase $purchase The purchase * @return void */ public function onDelete( \IPS\nexus\Purchase $purchase ) { } /** * On Purchase Reactivated (renewed after being expired or reactivated after being canceled) * * @param \IPS\nexus\Purchase $purchase The purchase * @return void */ public function onReactivate( \IPS\nexus\Purchase $purchase ) { } /** * On Transfer (is ran before transferring) * * @param \IPS\nexus\Purchase $purchase The purchase * @param \IPS\Member $newCustomer New Customer * @return void */ public function onTransfer( \IPS\nexus\Purchase $purchase, \IPS\Member $newCustomer ) { } /** * On Upgrade/Downgrade * * @param \IPS\nexus\Purchase $purchase The purchase * @param \IPS\nexus\Package $newPackage The package to upgrade to * @param int|NULL|\IPS\nexus\Purchase\RenewalTerm $chosenRenewalOption The chosen renewal option * @return void */ public function onChange( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage, $chosenRenewalOption = NULL ) { }
  5. 7 points
    Mark

    Custom Login Handlers (*)

    IPS Community Suite comes with a number of different methods to allow users to log in to the community, called "login handlers". Generally speaking, there are two types of login handlers: "Standard" login handlers which take a username and/or email address and password. For example, the default login handler, LDAP and IPS Connect. Login handlers which use a custom form, which are usually (though not necessarily) OAuth based. For example, Facebook, Twitter and LinkedIn login. Getting Started Both types are implemented by creating a PHP file in the system/Login folder containing class that extends \IPS\Login\LoginAbstract and inserting a record into the core_login_handlers database table. If you are creating a 3rd party login handler for distribution, you will need to create a plugin to insert that record, and distribute it with your login class. When inserting the record into core_login_handlers, set login_key to the name of the class without the namespace (for example, if your class is \IPS\Login\Example, set login_key to "Example"). Note that your PHP class will be prefixed with an underscore. This is a technicality in how code hooks are facilitated in the IPS Community Suite. Standard Login Handlers Here is a basic skeleton for a standard login handler: namespace IPS\Login; class _Example extends LoginAbstract { /** * @brief Authentication types */ public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME; /** * Authenticate * * @param array $values Values from from * @return \IPS\Member * @throws \IPS\Login\Exception */ public function authenticate( $values ) { /* Init */ $username = $values['auth']; // Depending on the value of $authTypes this may be an email instead $password = $values['password']; /* Find member */ try { $member = \IPS\Member::load( $username ); } catch ( \OutOfRangeException $e ) { throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('username') ) ) ), \IPS\Login\Exception::NO_ACCOUNT ); } /* Check password */ if ( $password !== 'the-correct-password' ) // Implement correct check here { throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member ); } /* Return member */ return $member; } /** * ACP Settings Form * * @param string $url URL to redirect user to after successful submission * @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field * @code return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... ); * @endcode */ public function acpForm() { return array(); } /** * Can a member change their email/password with this login handler? * * @param string $type 'username' or 'email' or 'password' * @param \IPS\Member $member The member * @return bool */ public function canChange( $type, \IPS\Member $member ) { return TRUE; } } The $authTypes property defines whether your login handler expects a username or email address or either. It is a bitwise field, and the acceptable values are: public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME; // Username public $authTypes = \IPS\Login::AUTH_TYPE_EMAIL; // Email address public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL; // Username or email address If you want to base this off a setting, or do any other setup for your login handler, you can implement an init() method. The authenticate() function receives the values from the form (the username/email address and password) and can either return an \IPS\Member object if the login was successful, or throw an \IPS\Login\Exception object if it wasn't. If throwing an \IPS\Login\Exception object, the message is displayed to the user, and the code should be one of the following values: throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::INTERNAL_ERROR ); // Something went wrong with the login handler which wasn't the user's fault. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::BAD_PASSWORD ); // The password the user provided was incorrect. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::NO_ACCOUNT ); // The username or email address the user provided did not match any account. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT ); // The username or email address matches an existing account but which has not been used by this login handler before and an account merge is required (see below) If your login handler needs to create an account for a user, and it is appropriate to do that, you can do that in the authenticate() method. For example: public function authenticate( $values ) { /* Init */ $username = $values['auth']; // Depending on the value of $authTypes this may be an email instead $password = $values['password']; /* Find member */ try { $member = \IPS\Member::load( $username ); } catch ( \OutOfRangeException $e ) { $member = new \IPS\Member; $member->member_group_id = \IPS\Settings::i()->member_group; $member->name = $username; $member->email = '...'; // You'll need to get the email from your login handler's database // You may want to set additional properties here $member->save(); } /* Check password */ if ( $password !== 'the-correct-password' ) // Implement correct check here { throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member ); } /* Return member */ return $member; } The acpForm() and canChange() methods are discussed below. Other Login Handlers Here is a basic skeleton for an OAuth-based login handler: namespace IPS\Login; class _Example extends LoginAbstract { /** * @brief Icon */ public static $icon = 'lock'; /** * Get Form * * @param \IPS\Http\Url $url The URL for the login page * @param bool $ucp If this is being done from the User CP * @return string */ public function loginForm( $url, $ucp=FALSE ) { $redirectUrl = \IPS\Http\Url::internal( 'applications/core/interface/example/auth.php', 'none' ); $oauthUrl = \IPS\Http\Url::external( "https://www.example.com/oauth" )->setQueryString( array( 'client_id' => 'xxx', 'redirect_uri' => (string) $redirectUrl ) ); return "<a href='{$oauthUrl}'>Login</a>"; } /** * Authenticate * * @param string $url The URL for the login page * @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object * @return \IPS\Member * @throws \IPS\Login\Exception */ public function authenticate( $url, $member=NULL ) { /* Get user details from service */ $userData = \IPS\Http\Url::external( "https://www.example.com/userData" )->setQueryString( 'token', \IPS\Request::i()->token )->request()->get()->decodeJson(); /* Get or create member */ if ( $member === NULL ) { /* Try to find member */ $member = \IPS\Member::load( $userData['id'], 'my_custom_id' ); /* If we don't have one, create one */ if ( !$member->member_id ) { /* If a member already exists with this email, prompt them to merge */ $existingEmail = \IPS\Member::load( $userData['email'], 'email' ); if ( $existingEmail->member_id ) { $exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT ); $exception->handler = 'Example'; $exception->member = $existingEmail; $exception->details = \IPS\Request::i()->token; throw $exception; } /* Create member */ $member = new \IPS\Member; $member->member_group_id = \IPS\Settings::i()->member_group; /* Is a user doesn't exist with this username, set it (if it does, the user will automatically be prompted) */ $existingUsername = \IPS\Member::load( $userData['name'], 'name' ); if ( !$existingUsername->member_id ) { $member->name = $userData['name']; } /* Set validating if necessary */ if ( \IPS\Settings::i()->reg_auth_type == 'admin' or \IPS\Settings::i()->reg_auth_type == 'admin_user' ) { $member->members_bitoptions['validating'] = TRUE; } } } /* Set service ID */ $member->my_custom_id = $userData['id']; $member->save(); /* Return */ return $member; } /** * Link Account * * @param \IPS\Member $member The member * @param mixed $details Details as they were passed to the exception thrown in authenticate() * @return void */ public static function link( \IPS\Member $member, $details ) { $userData = \IPS\Http\Url::external( "https://www.example.com/userData" )->setQueryString( 'token', $details )->request()->get()->decodeJson(); $member->my_custom_id = $userData['id']; $member->save(); } /** * ACP Settings Form * * @param string $url URL to redirect user to after successful submission * @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field * @code return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... ); * @endcode */ public function acpForm() { return array(); } /** * Can a member change their email/password with this login handler? * * @param string $type 'email' or 'password' * @param \IPS\Member $member The member * @return bool */ public function canChange( $type, \IPS\Member $member ) { return FALSE; } } The $icon parameter should be the name of a FontAwesome icon which is used on some login screens. The loginForm() method is used to display the HTML you need for the form. For an OAuth-based handler, this will usually just return the appropriate login button. You can alternatively return an \IPS\Helpers\Form object. The authenticate() method is where the bulk of your login code will go. If your loginForm() method returns an \IPS\Helpers\Form object it will be passed an array of values from that form (just like standard login handlers). If your loginForm() method returns raw HTML, it is your responsibility to ultimately redirect the user back to the same URL that was passed as $url to loginForm with the "loginProcess" set to the key for your login handler. Most OAuth providers do this with a gateway script in the interface directory. Your authenticate() method needs to return an \IPS\Member object or throw an \IPS\Login\Exception object, just as described above for standard login handlers. The acpForm(), link() and changeSettings() methods are described below. Creating settings for your login handler You will likely need to create settings for your login handler so when an admin sets it up they can provide keys, etc. There are two methods to assist with this: acpForm() can return an array of form fields allowing you to specify these settings, and testSettings() allows you to check the settings are correct. For example, to define a client ID setting you might do something like this: /** * ACP Settings Form * * @param string $url URL to redirect user to after successful submission * @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field * @code return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... ); * @endcode */ public function acpForm() { return array( 'example_client_id' => new \IPS\Helpers\Form\Text( 'example_client_id', ( isset( $this->settings['example_client_id'] ) ) ? $this->settings['example_client_id'] : '', TRUE ) ); } /** * Test Settings * * @return bool * @throws \InvalidArgumentException */ public function testSettings() { if ( $this->settings['example_client_id'] == 'invalid id' ) { throw new \InvalidArgumentException("The Client ID isn't correct."); } return TRUE; } And then you can simply access it's value elsewhere using $this->settings['example_client_id']. You can of course use custom validation callbacks for fields if appropriate, but often you will need testSettings() where there are multiple settings which work together. Merging Accounts With some login handlers, particularly those which are OAuth-based, you may need to merge accounts. For example, imagine a user is registered on your community, and then they try to log in using Facebook. In this situation, you don't want to create a new account, but rather prompt the user to link their Facebook account with their existing account. In this case, throw an exception in your authenticate() method: $exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT ); $exception->handler = 'Example'; $exception->member = $existingAccount; $exception->details = $token; throw $exception; Set $handler to the key for your login handler, $member to the existing account and $details to any details you need to link the accounts together, such as the access token. Then implement a link() method: /** * Link Account * * @param \IPS\Member $member The member * @param mixed $details Details as they were passed to the exception thrown in authenticate() * @return void */ public static function link( \IPS\Member $member, $details ) { $userData = \IPS\Http\Url::external( "https://www.example.com/userData" )->setQueryString( 'token', $details )->request()->get()->decodeJson(); $member->my_custom_id = $userData['id']; $member->save(); } Your link() method is called after the user has provided their password and it is safe to link the accounts together. Do whatever is necessary so that on subsequent logins, you can log the user in without intervention. Note that link() is static and cannot use any settings from the login handler. Checking if email/username is in use When a user registers an account on the community, your handler can check if the email address or username is acceptable. This is useful if you want your login handler to provide close integration such as is provided by the LDAP and IPS Connect handlers. The methods are emailIsInUse() and usernameIsInUse() - see the signatures in LoginAbstract for information on how to override these. Changing Details and additional callbacks When a user changes their email, password or username on the community, your handler can be notified of these changes and update their databases. You need to implement the canChange() method to let the User CP controllers know you support this functionality, and then the methods are changeEmail(), changePassword() and changeUsername() - see the signatures in LoginAbstract for information on how to override these. Additional callbacks are also available - logoutAccount(), createAccount(), validateAccount(), deleteAccount(), banAccount() and mergeAccounts() - see the signatures in LoginAbstract for information on how to override these.
  6. 5 points
    bfarber

    Single Sign On (SSO) (*)

    SSO, or Single Sign On, is a technique where-by one (or more) applications can automatically recognize a user as logged in when that user has logged in elsewhere. You can implement single sign on with IPS Community Suite by letting a remote application recognize a user who has already logged in to the Community Suite as being logged in within the remote application, or by using a plugin with the Community Suite to have it check for users logged in already through an external application. IPS Community Suite as the "Master" If you want to let external applications recognize users who are logged in to the Community Suite as being logged in on those external applications, there are 2 primary methods to accomplish this task, depending upon your configuration and specific needs. 1) Using IPS Connect One method of implementing single sign on with third party applications is through IPS Connect. IPS Connect is the only reliable cross-domain method of supporting SSO - this means that if your Community Suite and your external application are located on different top level domains (TLDs), for instance site.com and othersite.com, you should read more about IPS Connect and how to implement it in third party applications. Be aware that IPS Connect is natively supported by all IPS Community Suite installations alerady. 2) Reading the Community Suite session If your external application is on the same domain or subdomain as your Community Suite and you can easily include the Community Suite PHP files, you can easily include the Community Suite framework and use the objects and methods made available to validate a user as being logged in. The smallest code example to verify a user as being logged in is /* Require the init.php file from the Community Suite root directory */ require '/path/to/suite/init.php'; /* Initiate the session to verify who this user is */ \IPS\Session\Front::i(); /* Print the user's name */ print \IPS\Member::loggedIn()->name; Here we load the init.php file located in the Community Suite root directory, then we initiate the session, and finally we print the member's name. It is also possible to load the values stored in the user's cookies (if any) and validate them against the framework directly, however for the purposes of SSO it is simpler and more accurate to simply check the user's session as per the code snippet above. If using this approach, you will likely need to modify how cookies are set by IPS Community Suite. By default, it will set cookies to the the path and domain your community is installed into - for example, if your site is at mysite.com/community, it will set cookies to the domain "mysite.com" and the path "/community" - but then if you put the above SSO code at "mysite.com", it will not be able to read those cookies and will not work reliably. You can modify the domain and path used by using the COOKIE_DOMAIN and COOKIE_PATH constants. In the example situation given here, you would need to set the cookie path to "/" (so that it can be read both by the root directory where your SSO code is and by the subdirectory where the community is actually installed) which you would do by adding this line to your constants.php file: define( 'COOKIE_PATH', '/' ); Warning Generally, when you are checking if a user is logged in through the Community Suite in an external application you should redirect registration, login, logout, change email and change password requests back to the Community Suite. This means, if you wish to show users a link in order to perform any of these actions that link should direct the user back to the Community Suite in order to perform the action. This allows you to centralize all account activities IPS Community Suite as the "Slave" If you want to allow users who have logged in to a third party application to be automatically logged in to the Community Suite, you will need to create a plugin. The plugin should contain a code hook that extends the class \IPS\Session\Front, and then you will want to override the read() method. TIP: You can also create a code hook to override \IPS\Session\Admin if you have a need to implement SSO for the ACP (this is NOT recommended in most cases, as it may allow for unforeseen access to your admin control panel through your own custom code if you are not careful). /** * Read Session * * @param string $sessionId Session ID * @return string */ public function read( $sessionId ) Within the read method, generally speaking you will want to first check if the user is logged in locally by calling the parent read method. This saves you from processing checking your remote application on every page load once the user has been authenticated. You will want to check against the local cache as the standard session handler does, and you will want to ensure you only check against a remote application if you have not already done so. One way to accomplish this is to add a database column to your core_sessions table and then only check the remote application if the column has not been set yet. Your hook will vary based upon your needs, but a general outline is as follows /** * Read Session * * @param string $sessionId Session ID * @return string * @note We will want to first add a TINYINT(1) column named `already_checked` for this example */ public function read( $sessionId ) { /* Let normal class do its thing */ $result = call_user_func_array( 'parent::read', func_get_args() ); /* Fetch the session data again */ $key = "session_{$sessionId}"; $session = NULL; if ( isset( \IPS\Data\Cache::i()->$key ) ) { $session = \IPS\Data\Cache::i()->$key; } else { try { $session = \IPS\Db::i()->select( '*', 'core_sessions', array( 'id=?', $sessionId ) )->first(); } catch ( \UnderflowException $e ) { } } /* Only use sessions with matching IP address */ if( \IPS\Settings::i()->match_ipaddress and $session['ip_address'] != \IPS\Request::i()->ipAddress() ) { $session = NULL; } $this->member->language()->words['login_handler_ipscom'] = "Invisionpower.com"; /* Only check the remote application if we are not logged in */ if( !$this->member->member_id AND !$session['already_checked'] ) { /* Check the remote application here - this may involve looking for custom cookies your application sets and then passing them to a callback script on the remote server, or including a custom PHP script you have written */ /* If our check was successful, we create the member if he does not exist, otherwise we load the member. We then set $this->member to the member object we've loaded */ /* We then set the $this->save property to TRUE and we set our flag so that it will save */ $this->save = TRUE; $this->data['already_checked'] = 1; } /* Finally, return our session result as normal */ return $result; } The above example should illustrate the basic principles of overloading our default session class, checking your remote application, and then handling the response. Warning As with running the Community Suite as a "Master", when the Community Suite is configured as the "Slave" you may wish to redirect registration, login, logout, change password and change email requests for the Community Suite to your remote application. If so, you will need to either create hooks to redirect these requests to your remote application, or you will need to create a custom login handler that can propagate the requests to your remote application. Every SSO implementation is different, so you will need to determine the best approach for your specific needs.
  7. 5 points
    For the IPS4.0.5 release, we changed how files are stored and loaded within the suite. Previously we stored the entire URL which was convenient but it made it problematic when you moved your community or tried to move images to another system such as Amazon S3. From 4.0.5 onwards, just the container and file name is stored in the database. This is much better for the database as it means you do not need to update your database when you change your URL and it's easier to move the files as there are no database changes to update. However, it does mean that you need to change how you display images in many cases. For example, if you had something like this: <strong>{$member->group['icon']}</strong>You'll notice that now it is just a broken image. You can use the new {file} tag to display it, like this: <strong>{file="$member->group['icon']" extension="core_Theme"}</strong>This will now display correctly. The extension parameter is mapped to the FileStorage extension found in /applications/{app}/extensions/core/FileStorage. So, for example, if you are managing gallery Images, the extension is gallery_Images. If you're managing Pages records, the extension is cms_Records. When writing code, you will need to be mindful of the changes. From IPS4.0.5 on newer, you will need to use code similar to this below: # Create and save (this is largely the same) $file = \IPS\File::create( 'core_Theme', 'file.txt', $contents ); # Calling (string) on $file now returns just the container/file.txt e.g. (monthly_04_2015/file.txt) \IPS\Db::i()->insert( 'table', array( 'url' => (string) $file ) ); # Load and show $row = \IPS\Db::i()->select( '*', 'table', array( 'file=?', 'file.txt' ) ); # We now have the container/file.txt, but we need the full URL $url = \IPS\File::get( 'app_Extension', $row['url'] )->url; print "<a href='{$url}'>";The most important part to keep in mind is that (string) $file will now store just container/file.txt and you need to call \IPS\File::get( 'app_Extension', $row['url'] )->url to get the full URL.
  8. 4 points
    bfarber

    IPS Connect (*)

    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 document outlines how to use IPS Connect within the Community Suite, as well as how to develop both "master" and "slave" IPS Connect applications. While all changes propagate to all installations that are connected with IPS Connect, one installation will always act as the "master" and all of the remaining installations in the network act as the "slave". 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. Requests that are propagated with IPS Connect 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. TIP: 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 ); Warning Be aware that IPS Connect in IP.Board 3.x and IPS Connect in the 4.0 Community Suite are not compatible. 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. Using IPS Connect in the IPS Community Suite To use IPS Connect with two or more IPS Community Suite installations, you must first choose which installation will be the master and which installation(s) will be the slave(s). Visit the Admin Control Panel of the master application and navigate to the System tab -> Login Handlers. At the top of the page you will see a notice that tells you credentials you must use in order to notify other IPS Community Suite installations that this is the master. There is no "setup" required to make a Community Suite installation the master IPS Connect installation. To set up slave applications in the network, visit the Admin Control Panel of each slave individually and navigate to the same area (System -> Login Handlers). Next to the IPS Connect login method, click on the pencil icon. Here you will be asked for the Master IPS Connect URL and Master IPS Connect Key that you will have retrieved from the master installation previously. You will also be asked if you wish to accept display names or email addresses for the login identifier, or both, and if you want to allow IPS Connect authentications to provide access to the admin control panel if the local user account is marked as an administrator. After providing the required information and saving this form, you should click the "Disabled" badge in order to enable the IPS Connect login handler, and then you should drag-n-drop the IPS Connect handler to the top of the list to make it the primary login method. This ensures that IPS Connect is checked first before the Community Suite falls back to the local database to validate a login attempt. Setting up IPS Connect with your Community Suite is complete at this point. You can repeat the above steps on any other installations you wish to connect to your network. Creating "master" IPS Connect applications Your master IPS Connect application will be required to accept API calls from slave IPS Connect applications and respond to them accordingly, and to then propagate changes from those calls to any other slaves in the network. You must supply a unique key for your master application (which can be anything you want but must be unique to this installation) and the URL to the gateway API file that you will create to all slave applications. The master application secret key should be kept private and never be made public. You must create a gateway file that slave applications will call to. These applications will send requests to your gateway file and will expect a response in JSON format to those requests (except for certain requests which result in redirection as outlined later in this document). Your master application will need to implement the API requests outlined below, accepting the expected GET parameters and responding with the outlined Response status code and parameters. Finally, the master application will also need to propagate the requests to all slave applications in its network (and thus needs to maintain a database of all slave applications that have connected with it). The IPS Community Suite when acting as a master IPS connect application implements a queue system for requests to slave applications. If a request that should be propagated to a slave fails, it is inserted into a queue and reattempted at a later time. Any repeated failures are reported to the administrator via an ACP dashboard plugin. The requests will be automatically reattempted, or you can manually reattempt the requests from the dashboard. While this system is not necessary in a master application, it can help to ensure data consistency across an IPS Connect network. If the master application ignores failures to a slave application, then email changes, name changes, password changes and more may not be propagated if a temporary error occurs. Note that as the IPS Connect API is a public API, while the master key is required to communicate with it, you may wish to implement brute force protection against multiple subsequent login requests. This can help protect against brute force attempts made against slave applications that do not natively support such protection. If a slave application no longer wishes to communicate with a master application, it should respond with a status of DISABLED when a request is propagated to it. If this happens, the master application should disassociate the slave application and cease sending further requests to it. Creating "slave" IPS Connect applications Slave IPS Connect applications will need to send requests for specific actions via an HTTP REST API call to the IPS Connect master installation. This means that you will need to obtain the URL and API key from your IPS Connect master installation and supply this to your slave application, and then when a user attempts to perform one of the actions outlined at the top of this article your login routines should make API calls to the master application in order to perform and validate those actions. Be aware: Some requests result in redirecting the user to one or more websites before returning the user to a URL you supply. See the crossLogin method outlined below for an example. For most requests, however, you will receive a JSON response which will need to be decoded and examined. If you issue a login request, for instance, you will receive a response with a status code and then some additional information such as the email address, username and unique ID on the network. Most slave applications will then create a local user record with this data if the user account does not exist, or update the local user record if the account does exist (using the unique ID to match up user accounts if possible, or falling back to matching accounts by email address). This is not, however, a requirement. Note: After making a login request to the master installation, an IPS Connect slave should then call the crossLogin request at the master installation in order to ensure the user is logged in to all websites in the IPS Connect network. This method will result in the user being redirected to all installations in the network, and then finally back to a URL supplied by the local slave application. All account changes must be sent to the master installation to ensure data consistency across the network. If a user changes their email address and this account change is not sent to the master application, for instance, the user will no longer be able to login correctly on other sites in the network. Additionally, all slave applications must accept the same requests and respond accordingly just as the master would, as the master application must propagate requests to each slave in the network. For instance, consider the following work flow: User changes their email address on SLAVE 1. SLAVE 1 sends this request to the MASTER. The MASTER, in turn, sends the request to SLAVE 2 and then to SLAVE 3. In this case, even though SLAVE 2 and SLAVE 3 are slave applications, they must accept the same requests the master accepted in order to allow them to update their local databases. API Request Details Master IPS Connect applications will need to accept the following requests and respond accordingly with the response parameters outlined for each request. It will also need to propagate the requests to each slave in its network, differentiating the requests with a "slaveCall=1" request parameter. Slave IPS Connect applications will need to call the following API endpoints against the master IPS Connect installation it is communicating with, and additionally accept the same API request calls (differentiated with a slaveCall=1) in order to accept and make changes to the local database when changes are made at a remote website in the Connect network. The following GET request parameters will always be included: do: This is the action to perform, as outlined below. key: This is the secure key and should be validated to ensure it matches the secure key supplied to the slave application. url: This is the URL to the slave application. Other GET request parameters will be sent and will vary depending upon the request. All requests from the master installation to slave installations will also include slaveCall: Always set to '1', this allows the slave application to know that the request is coming from a master application and is intended to result in the local database being updated. Important Note: If 'id' is passed in the request to make a change to a specific user account, the 'key' value will be an MD5 hash of the master application's key concatenated with the id. For instance $key = md5( $masterKey . $id ); All responses will include a 'status' key in the JSON response. Some responses may include additional information. You should verify the 'status' response is SUCCESS to ensure the action completed successfully. Invalid requests will have a status of INVALID_ACTION. Slave applications that no longer wish to be a part of the network (i.e. if IPS Connect is disabled at this installation) should respond with a status of DISABLED. Example: print json_encode( array( 'status' => 'SUCCESS' ) ); exit; Method: verifySettings This method is intended to allow a slave application to verify the settings of the master (i.e. when the master key is first provided) and to "register" with the master installation. This allows the master installation to propagate requests to slave applications later. GET parameters: ourKey: [Required] This is a unique key associated with the slave Response status codes: SUCCESS Response parameters: None Method: fetchSalt Call this method in order to fetch a user's password salt - necessary for allowing the local application to hash the user's credentials properly before sending them to the master. GET parameters: idType: [Required] What type of ID is being passed (a value of 1 indicates the id is a display name, a value of 2 indicates the id is an email address and a value of 3 indicates the value could be either a display name OR an email address) id: [Required] The user ID Response status codes: SUCCESS REQUEST_MISSING_DATA: This indicates either idType or id was not provided ACCOUNT_NOT_FOUND: No account was found based on the supplied value Response parameters: pass_salt: The salt applied to the password when hashing it Method: login This method authenticates a user and logs the user into all applications on the IPS Connect network. GET parameters: idType: [Required] What type of ID is being passed (a value of 1 indicates the id is a display name, a value of 2 indicates the id is an email address and a value of 3 indicates the value could be either a display name OR an email address) id: [Required] The user ID password: [Required] The encrypted password NOTE: The 'password' parameter must be encrypted in the same manner as the IPS Community Suite. A request should first be sent to fetch the user's salt (see fetchSalt above), and then the password should be hashed in the following manner: /* $password is the raw password and $salt is the salt returned from fetchSalt */ if ( strlen( $salt ) == 22 ) { $encrytedPassword = crypt( $password, '$2a$13$' . $salt ); } else { $password = str_replace( "&", "&amp;", $password ); $password = str_replace( "<!--", "&#60;&#33;--", $password ); $password = str_replace( "-->", "--&#62;", $password ); $password = str_ireplace( "<script", "&#60;script", $password ); $password = str_replace( ">", "&gt;", $password ); $password = str_replace( "<", "&lt;", $password ); $password = str_replace( '"', "&quot;", $password ); $password = str_replace( "\n", "<br />", $password ); $password = str_replace( "$", "&#036;", $password ); $password = str_replace( "!", "&#33;", $password ); $password = str_replace( "'", "&#39;", $password ); $password = str_replace( "\\", "&#092;", $password ); $encrytedPassword = md5( md5( $salt ) . md5( $password ) ); } There are two different ways of encrypting the password because of a difference in how passwords were hashed between IP.Board 3 and IPS Community Suite 4. If your community did not upgrade from IP.Board 3, the salt will always have a length of 22 and only the first algorithm is needed. $2a$13$ refers to the salt prefix and a pre-determined cost factor that should not be altered. Response status codes: SUCCESS REQUEST_MISSING_DATA: This indicates either idType or id was not provided WRONG_AUTH: This indicates that the provided credentials could not be authenticated. This may mean that no account exists with the id provided or that the password is not valid. Response parameters: connect_status: VALIDATING if the account is still validating or SUCCESS otherwise email: The member's email address name: The member's display name connect_id: The member's unique integer ID on the master installation connect_revalidate_url: If the member is VALIDATING, the URL that any slave application's should send the user to in order to complete their validation Method: crossLogin When a user logs in to a slave application successfully, they will be redirected to the crossLogin method of the master application in order to be logged in to it and all other slave applications on the network. This is necessary to work around cross-domain cookie restrictions. The master install will need to redirect the user to each slave's crossLogin method, and will also need to log the user in to the master application, before returning the user to the originating URL (the original slave application the user logged in to). GET parameters: id: The member's unique integer ID on the master installation returnTo: a URL to return the user to once the user has been logged on. NOTE: When the master application redirects the user to slave applications to log the user on, the returnTo URL should be compared to the url parameter in order to ensure the user is not sent to the originating slave. When sending the user to another slave in the network, the returnTo parameter should be set to the master URL. Further, the master application should set a 'slaveCall' parameter to 1 when calling slave applications to prevent them from performing extra work (this allows slave applications to know that the request is from the master and to perform specific duties). Response status codes: None, the user will be redirected to the returnTo URL Response parameters: None Method: logout API calls to the logout method are designed to log the user out of the master application as well as all of the slave installations. GET parameters: returnTo: The URL to return the user to once they have been logged out id: The member's unique integer ID on the master installation NOTE: Much like the crossLogin method, the logout method should redirect the user to all slave applications to log the user out and then log the user out of the master application before returning the user to the originating installation. slaveCall is passed in the URL when the master calls slave applications to differentiate requests between master and slave applications. Response status codes: None, the user will be redirected to the returnTo URL Response parameters: None Method: register Register the user on all installations in the Connect network GET parameters: name: The member's name email: The member's email address pass_hash: The member's password hash pass_salt: The member's password salt revalidateUrl: The URL to send the user to if they are validating and attempt to login to any other site in the connect network NOTE: The 'pass_hash' parameter must be encrypted in the same manner as the IPS Community Suite. The password should be hashed in the following manner: /* $password is the raw password and $salt is the salt returned from fetchSalt */ crypt( $password, '$2a$13$' . $salt ); Response status codes: SUCCESS REQUEST_MISSING_DATA: This indicates email, name, pass_salt or pass_hash was not passed Response parameters: connect_id: The member's unique integer ID on the connect network Method: validate Call this method in order to mark a user's account as validated. If a user account is marked as awaiting validation and the user validates, this should be called to ensure the user account is marked as validated across the entire network. GET parameters: id: [Required] The unique user ID of the user account Response status codes: SUCCESS Response parameters: None Method: delete Call this method in order to delete a user account. THERE IS NO UNDOING THIS ACTION. GET parameters: id: [Required] The unique user ID of the user account Response status codes: SUCCESS Response parameters: None Method: ban Call this method in order to ban or unban a user account GET parameters: id: [Required] The unique user ID of the user account status: [Required] A value of 1 will ban the user account while a value of 0 will unban the user account Response status codes: SUCCESS Response parameters: None Method: merge Call this method in order to merge two distinct user accounts into one. THERE IS NO UNDOING THIS ACTION. GET parameters: id: [Required] The unique user ID of the account you wish to keep remote: [Required] The unique user ID of the account you wish to remove Response status codes: SUCCESS Response parameters: None Method: checkEmail Call this method in order to check if an email exists at the master application. This can be useful to prevent a user who has already registered elsewhere on the Connect network from registering again on a local site, when they should instead login. GET parameters: email: [Required] The email address to check Response status codes: SUCCESS Response parameters: used: 1 if the email address is in use or 0 if not Method: checkName Call this method in order to check if a username exists at the master application. This can be useful to prevent a user who has already registered elsewhere on the Connect network from registering again on a local site, when they should instead login. It is not necessary to enforce uniqueness of display names in your application if your application has a need to allow multiple user accounts with the same display name to exist, however you should never allow logging in by 'display name' if this is the case. GET parameters: name: [Required] The name to check Response status codes: SUCCESS Response parameters: used: 1 if the name is in use or 0 if not Method: changeEmail This method is called when an existing user's email address should be updated to a new value. GET parameters: email: [Required] The new email address to use id: [Required] Unique user ID provided by the master application to a previous login or registration call Response status codes: SUCCESS REQUEST_MISSING_DATA: The new email address to use was not supplied EMAIL_IN_USE: The new email address is already being used by another account Response parameters: None Method: changePassword This method is called when a user has updated their password GET parameters: pass_salt: [Required] Password salt pass_hash: [Required] Password hash id: [Required] Unique user ID provided by the master application to a previous login or registration call NOTE: The 'pass_hash' parameter must be encrypted in the same manner as the IPS Community Suite. The password should be hashed in the following manner: /* $password is the raw password and $salt is the salt to be passed with pass_salt */ crypt( $password, '$2a$13$' . $salt ); Response status codes: SUCCESS REQUEST_MISSING_DATA: The password salt or password hash was not supplied Response parameters: None Method: changeName This method is called when an existing user has changed their display name at a local installation TIP: 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 ); If this is done on a slave IPS Community Suite application, that slave (only) will ignore username change requests and will not send username changes to the master application. If the above constant is set on a master IPS Community Suite application, username change requests will not be propagated to any slaves in the network. You should carefully consider your intentions if you decide to make the change above. GET parameters: name: [Required] The new name to use id: [Required] Unique user ID provided by the master application to a previous login or registration call Response status codes: SUCCESS REQUEST_MISSING_DATA: The new name to use was not supplied USERNAME_IN_USE: The new name is already being used by another account Response parameters: None Plugging in to your existing master application If you are using IPS Community Suite as a master application and wish to extend or change its functionality, you can easily do so without having to edit any PHP files, ensuring your customizations are retained through future upgrades. The master IPS Connect gateway with the IPS Community Suite is found at /applications/core/interface/ipsconnect/ipsconnect.php. You can create a new file in this same location called "custom.php" with a class inside this file called ipsConnect_custom, extending ipsConnect. This file will be automatically loaded if it exists, and the ipsConnect_custom class will be instantiated instead of ipsConnect. From there, you can override any methods you need to. As an extension to this, the following methods can be defined which will be called automatically: _postCrossLogin(): Because cross login requests result in redirection instead of returning a response to output in JSON format, this method is supported which allows you to perform any custom actions you require before the redirection occurs. _postCrossLogout(): Because logout requests result in redirection instead of returning a response to output in JSON format, this method is supported which allows you to perform any custom actions you require before the redirection occurs. Example: <?php // My custom.php file class ipsConnect_custom extends ipsConnect { public function register() { $result = parent::register(); // A user just registered, do something custom now return $result; } protected function _postCrossLogin() { // Do something before the request is redirected to a different site } protected function _postCrossLogout() { // Do something before the request is redirected to a different site } }
  9. 3 points
    Mark

    Plugins (*)

    Introduction Plugins provide a way for developers to modify or extend the behaviour of applications within the IPS Community Suite. This guide will cover in detail the different features available within Plugins. Many of the features describes are accessed from the Plugin Developer Center - to access this, first place IPS Community Suite in developer mode. Once developer mode is enabled, a "Create Plugin" button will appear in the Admin CP under System --> Site Features --> Plugin. Use this tool to create your plugin, after which you'll be taken into the Plugin Developer Center. Useful links for further reading: Plugins: An Example - A step-by-step guide to creating your first plugin. The Autoloader - An overview of how to locate classes. Plugin Features Code Hooks Code Hooks allow you to extend any class within the IPS Community Suite or an application installed. You can create Code Hooks in the Plugin Developer Center. You'll notice that throughout the code, classes are defined beginning with an underscore, but called without. For example, in /system/Member/Member.php, the declaration for is: namespace IPS; class _Member extends \IPS\Patterns\ActiveRecord Yet throughout the code, \IPS\Member is called rather than \IPS\_Member. This is a technicality in how code hooks are facilitated. When creating your code hook, specify the class you want to extend without the underscore. When you're actually editing the code, it will say "extends _HOOK_CLASS_" - this is because the actual class it will extend will vary if more than one hook is overloading the same class. You can edit code hooks either in the Plugin Developer Center or by manually editing the file which will have been created in /plugins/<your plugin>/hooks. Using the Plugin Developer Center has the benefit that all the properties and methods of the class you're extending (and classes that class extends) are shown in the sidebar, and clicking on any one will insert the declaration with the doc-comment into the editor. Important things to remember when creating code hooks: The first line in the class that is created is //<?php - this is so if you want to edit the file in your own editor that syntax highlighting works properly, however the <?php tag should not be used uncommented. Your hook will specify that it extends _HOOK_CLASS_ - though this is syntaxually wrong, this should not be changed. The system will automatically change it to the correct class which may vary if there is more than one hook on a single class. If your code causes a PHP error or throws a RuntimeException, your hook will be ignored and the system will revert to the default class. If you need to deliberately throw an exception, throw an object of an appropriate exception class. When overriding a method, you MUST call the parent method so that ultimately you are inserting your code at the beginning or end of the method. This is necessary for allowing hooks on the same method to work. When overriding a method, you MUST NOT copy code from the original method into your hook. This is necessary to ensure that your hook does not interfere with any bug fixes or changes made to the original class in future versions. Also, it is against the terms of the IPS Community Suite license to distribute any code within it. Theme Hooks Theme hooks allow you to modify the HTML templates. The HTML templates within the IPS Community Suite are structured into groups, locations and applications. Each application will have at least 3 locations: front (templates used for the front-end) admin (templates used for the Admin CP) global (templates which are used for both) Though some applications may have more for specific purposes (for example, the core application has another location for installer/upgrader called setup). Each location can contain any number of groups - groups are generic collections of templates, for example, the core application has one group (in the front location) called messaging which contains all the templates for composing and viewing personal conversations. One Theme Hook acts on one template group. You can create Theme Hooks in the Plugin Developer Center. Once created there are two ways to use Theme Hooks - each of which work differently and so will be appropriate for different circumstances: CSS-Selector Mode Editing your Theme Hook in the Plugin Developer Center will display a panel showing all of the templates in that group: Selecting any template will bring up a tabbed interface showing the modifications your Theme Hook is making to that template, and allow to create more: To make a modification to the selected template, you will provide a CSS selector - you can use any of the selectors supported by jQuery (it's worth mentioning that jQuery isn't involved in making your Theme Hook work, it is simply that the supported selectors are the same). The easiest way to choose a selector is with the interactive "Select Element" feature, which will launch a model displaying the template allowing you to simply click on the element you want to use: It is important to note that when using the "Select Element" feature, it will use the most specific CSS selector it can for the element selected, however, that selector may also match other elements so you may need to adjust it. When providing the content, in addition to the variables that are available to the template (which are shown next to the editor) you can also use any variables available at that point in the template (for example, if you're inserting code within a foreach loop, you can use the variables created by it). You can also use template logic and template tags. PHP Mode Under the hood, each template group is compiled into a PHP class, with a method for each template. You can extend this class by manually editing the file which will have been created in /plugins/<your plugin>/hooks. It is important to note that when using this mode, you are overloading the compiled template group so the return value will be the HTML that will be displayed, without any template logic or template tags.] All of the same considerations as for Code Hooks (see "Important things to remember when creating code hooks") also apply here. Settings Allocating space in IPS\Settings IPS\Settings is a repository for scalar key/value data pairs - it is shared amongst all applications and plugins and is accessible from anywhere as a Singleton, there are also shortcuts to access it's contents within templates. It's primary intention is for storing settings the administrator provides in the Admin CP. In order to use this, you must enter the keys you want to be allocated to your plugin and the default values in the Plugin Developer Center. The keys are not namespaced, so it is recommended that you choose keys that are unlikely to be used by other applications or plugins. Creating a settings page Your plugin is allowed to have one settings page which will be displayed with the administrator clicks the "edit" button next to your plugin. In most cases, this will be just a regular form using the form helper, however, though the form helper is very flexible (it can handle tabs, sidebars and a wide variety of input types), you are not restricted to this and can display your settings however you like. In the /plugins/<your plugin> directory, you'll find a file called "settings.rename.php" - rename this to "settings.php" and this will be the code that runs when the user accesses your settings page. The example file is written with the form helper in mind, and to use that, all you need to do is add additional input fields. If you want to use a custom interface though, you just need to return either a string (with the content to display) or TRUE to dismiss the settings page. Your code will be eval'd and so you should not include an opening <?php tag - one is included in the example file so that syntax highlighting will work in most code editors, however it is commented out. Tasks Tasks are scripts which run at specific intervals. They are useful for performing any maintenance that must be ran on a regular basis. It is important to note however, that they cannot be relied on - though IPS Community Suite provides a way for administrators to run tasks using a cron, this may not be enabled, in which case tasks can only be triggered when users are active on the site - if the site is inactive at the time your task is scheduled to run, it will run at the soonest opportunity after. You can create tasks in the Plugin Developer Center. When you create a task, a file will automatically be created in the /plugins/<your plugin>/tasks directory for your task. The doc-comments in this file explain how to implement the task. Database changes (Versions) Installs To run code such as database changes when your plugin is installed, open the /plugins/<your plugin>/dev/setup/install.php file and follow the instructions given by the doc-comments. Upgrades Plugins support version management. Whenever you release a new version of your plugin you should add a new version in the Plugin Developer Center. When you add a version, you must specify an ID number to represent it (e.g. "10000", "10001", etc.) as well as a human-readable string (e.g. "1.0.0", "1.0.1", etc.). There is no particular standard to how the ID number should be formatted other than it must increase the newer the version. The human-readable string should be in the format "x.y.z". For each version, a file will be added to the /plugins/<your plugin>/dev/setup folder where you can specify code that will be ran when upgrading to that version (see the doc-comments within them for more details). It is important to note that at install, only install will be ran, so when you make a change you will usually need to add it both to your installer, and the appropriate upgrade file for that version. HTML, CSS, JavaScript and Images You can place HTML, CSS, JavaScript and image files in the appropriate folders in /plugins/<your plugin directory>/dev. HTML HTML files will become templates within the core application > global location > plugins group. In other words, you will get the content of the HTML files you create using this code in controllers: \IPS\Theme::i()->getTemplate( 'plugins', 'core', 'global' )->FILENAME( ... ); Or the following code in other templates: {template="FILENAME" group="plugins" location="global" app="core"} You should create your files using the extension ".phtml". The first line of the file should be the following tag: <ips:template parameters="$example1, $example2" /> Replacing $example1, $example2 with the names of whatever variables you intend to pass in. It is fine to have no parameters. After that opening line, the remainder of the file should just be what you want the template contents to be. You can use template logic and template tags. Note that if any of the code in your template causes an exception to be thrown (which could happen if you're using particularly complicated template logic/tags without proper try/catch statements) the contents of the template will be ignored and will return an empty string. CSS and JavaScript Any CSS and JavaScript files you create will be compiled with the rest of the CSS and JavaScript automatically, so you do not need to do anything other than create the files. Images Images will be placed in the core application > global location, in a folder called plugins. In other words, you will render the images you place in the "img" directory by using this code in templates: <img src='{resource="plugins/example.jpg" app="core" location="global"}' /> You must put all images directly in the "img" directory, do not create subdirectories. Language Strings IPS Community Suite supports multiple languages and translation tools such as the Visual Language Editor. Any text your plugin uses should be abstracted to language strings so that your plugin also supports these features. Language strings are defined as key/value pairs. To add a language string, just open the /plugins/<your plugin>/dev/lang.php file and add an element to the array. Then to use the language string, use this in controllers: \IPS\Member::loggedIn()->language()->get( 'KEY' ); Or this in templates: {lang="KEY"} Widgets Widgets are blocks which can be added to the sidebar, header or footer on any page in the suite and the Pages application. You can create and widgets in the Plugin Developer Center. After creating the widget, a PHP file will be created in the widgets folder of your application or plugin with a skeleton to get you started. Simply follow the instructions in the source code to make your widget work. Distribution You can download your plugin from the Plugin Developer Center which will download a single XML file. This will automatically handle building all of the above features.
  10. 2 points
    Mark

    Commerce License Key API (*)

    Products in commerce can generate license keys. A simple HTTP-based API is available to interact with license keys. Basics For each of the endpoints, you will send a HTTP POST request, and a JSON object will be returned. The URL you will be posting to is: http://www.example.com/applications/nexus/interface/licenses/?ENDPOINT Of course, replace "example.com" with the correct domain for your community and "?ENDPOINT" with one of the endpoints listed below. Configuration Before you can use the API, you must define some settings. In the root directory of your community, create a constants.php file if you don't already have one (be sure to start the file with <?php) and add this line: define('NEXUS_LKEY_API_DISABLE', FALSE); You can also add two other constants: NEXUS_LKEY_API_CHECK_IP controls whether the IP address of the server making the request needs to be the same when checking or updating data as when the key was activated. Defaults to TRUE. NEXUS_LKEY_API_ALLOW_IP_OVERRIDE controls whether you can specify an "ip" parameter for this check, overriding what IP actually sent the request. Defaults to FALSE. Important Information Two concepts you need to understand before you can dive in: License keys may be able to be used multiple times (you can set how many times when creating your product). When a license key is activated it is assigned a usage ID for that activation which refers to that usage. You will need to log and send this even if you only allow 1 usage. License keys can be given an identifier which is sort of like a password. If a key has an identifier, you will need to send it with all requests. It is strongly recommended you do this, as otherwise someone could brute force the API to get a list of all your license keys. For example, you might use the customer's email address as an identifier and if you do this, the API will only respond to requests which include a valid license key and the correct email. An identifier can be information about the customer, or any custom fields on the product and can be set in your product settings. Endpoint: activate Call this to activate a license key, for example when a user first enters their key into your application. Parameters to send: key The license key identifier The identifier setIdentifier If "1", and the license key does not currently have a value for its identifier, the provided identifier will be set - if it has already been set and the provided value is not the same, an error will be raised extra JSON-encoded additional data to note. This should be a simple object with key/value pairs where both the keys and values are strings. You will be able to view the data in the Admin CP when viewing the purchase the license key is associated with. Response: response will be "OKAY" usage_id The usage ID that has been assigned. Be sure to save this. Error codes in addition to those common to all endpoints: 201 (MAX_USES) - The key has already been activated the maximum number of times and cannot be activated again. Endpoint: check Call this to check a license key is still active. Must be from the same IP address that activate was unless overridden by configuration. Parameters to send: key The license key identifier The identifier usage_id The usage ID Response: status - One of the following values: "ACTIVE" - The license key is active. "INACTIVE" - The license key, or purchase associated with the license key, has been deactivated. "EXPIRED" - The purchase associated with the license key has expired. uses - The number of times this license key has been activated. Only provided if "STATUS" is "ACTIVE". max_uses - The maximum number of times this license key can been activated. Only provided if "STATUS" is "ACTIVE". Error codes in addition to those common to all endpoints: 303 (BAD_USAGE_ID) - The usage ID provided is invalid. 304 (BAD_IP) - The IP address making the request does not match the one used to activate the license key. Endpoint: info Call this to get information about a key, not specific to any usage ID. Parameters to send: key The license key identifier The identifier Response: key - The license key. identifier - The identifier. generated - A unix timestamp of the date that the key was generated. expires - A unix timestamp of the date that the purchase the key is associated with will expire. usage_data - A multi-dimensional array containing information about the license key's "uses". The array keys are the usage IDs and the value is an array with the following elements: activated - A unix timestamp of when this usage was activated. ip - The IP address that activated this usage. last_checked - A unix timestamp of the date the last "check" API call was received for this usage. extra - Additional information as provided to the "activate" API method. purchase_id - The ID number of the purchase associated with the license key. purchase_name - The name of the purchase associated with the license key. purchase_pkg - The ID number of the package in the store of the purchase associated with the license key. purchase_active - A boolean value indicating if the purchase associated with the license key is active. purchase_start - A unix timestamp of the date that the purchase associated with the license key was purchased. purchase_expire - A unix timestamp of the date that the purchase associated with the license key will expire. purchase_children - An array containing child purchases of the purchase associated with the license key. The values will be an array of data about the child purchase with the following elements: id - The ID number of the purchase. name - The name of the purchase. app - The application key that the purchases was made from (will be "nexus" for purchases from the store). type - The type of purchase (e.g. "product", "hosting", "ad", etc.). item_id - The ID number of the package in the store of the purchase. active - A boolean value indicating if the purchase is active. start - A unix timestamp of the date that the purchase was purchased. expire - A unix timestamp of the date that the purchase will expire. lkey - The license key associated with the purchase, if it has it's own license key. customer_name - The customer's name. customer_email - The customer's Email address. uses - The number of times this license key has been used. max_uses - The maximum number of times this license key can be used. There are no error codes in addition to those common to all endpoints. Endpoint: updateExtra Call this to update the "extra" information for a key that was originally sent in the "activate" request. Must be from the same IP address that activate was unless overridden by configuration. Parameters to send: key The license key identifier The identifier usage_id The usage ID extra JSON-encoded new data Response: status will be "OKAY" Error codes in addition to those common to all endpoints: 303 (BAD_USAGE_ID) - The usage ID provided is invalid. 304 (BAD_IP) - The IP address making the request does not match the one used to activate the license key.
  11. 2 points
    Mark

    Developer Mode (*)

    Developer Mode is required to develop Applications and Plugins in IPS Community Suite. 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 To enable Developer Mode: 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: 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.
  12. 1 point
    Requirements The Model Skeleton namespace IPS\yourApp; class _Comment extends \IPS\Content\Comment { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief Default Values */ protected static $defaultValues = NULL; /** * @brief [Content\Comment] Item Class */ public static $itemClass = 'IPS\yourApp\YourClass'; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'yourapp_comments'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = ‘comment_'; /** * @brief Title */ public static $title = ‘thing_comments’; /** * @brief Database Column Map */ public static $databaseColumnMap = array( ); 'item' 'author' 'author_name' 'content' 'date' 'ip_address' => 'fid', => 'mid', => 'author', => 'text', => 'date', => 'ip_address', 61 } Introduction Just like Content Items themselves, Comments also need a model which follow the Active Record design pattern. The inheritance is similar to Content Items (though \IPS\Content\Comment is instead of \IPS\Content\Item, though both extend \IPS\Content. The required properties are the same as Content Items, with the addition of $itemClass. The elements required for $databaseColumnMap are: item should be the column that contains the ID number of the item the column belongs to. author, like for item, should be the column that contains the ID number of the user who posted the comment. content should be the column that contains the actual comment text. date should be the column that contains the timestamp of when the comment was posted. ip_address should be the column that contains the IP address of the user who posted the comment. The optional elements are: author_name is optional, but if specified should be the column to contain the username of the user who posted the comment. first should be a column that contains a boolean value indicating if this is the first comment on the item. Available Methods In addition to those provided by \IPS\Patterns\ActiveRecord (which work exactly the same as for Content Item models) a number of additional methods are available: url( $action=NULL ) Gets the URL directly to that comment (will automatically work out the correct page number and anchor). Unlike for Content Items, this method is already defined for you. item() Returns the \IPS\Content\Item object of the item the comment belongs to. author() truncated( bool $oneLine=FALSE ) canView(...) canEdit(...) canDelete(...) modPermission(...) modAction(...) All behave the same as content items. isFirst() Returns a boolean value indicating if this is the first comment on the item. 62 isIgnored( \IPS\Member $member=NULL ) Returns a boolean value indicating if this comment should be ignored by $member (currently logged in member for NULL). dateLine() Returns a string that can be used in templates saying something along the lines “Posted 2 days ago”. html() Returns the HTML to display the comment (in it’s template). Optional Properties A number of optional properties can be specified: static $commentTemplate A callback specifying the template used to display the comment (a standard template is used by default). static $formTemplate A callback specifying the template used to display the form for posting the comment (a standard template is used by default). static $incrementPostCount A boolean value controlling if post counts should be incremented for comments of this type. static $formLangPrefix Behaves the same as for content items allowing you to change the language strings used on the submission form. Changes to make to Content Item model Add a new property to your Content Item model: public static $commentClass = ‘IPS\yourapp\YourClass'; If your Content Item requires a first comment - for example, IP.Board topics need at least 1 post, so this is true for topics, but IP.Downloads files are posted with no comments to start with, so it is not true for files - add an additional property: protected static $firstCommentRequired = TRUE; Add new elements to $databaseColumnMap: 63 Element Description Required? num_comments The number of comments Yes last_comment Unix timestamp of when last comment was made No last_comment_by ID of member who made last comment No last_comment_name Username of the member who made the last comment No Create an EditorLocations extension To display the comment text editor, it will automatically look for an editor extension using the $application and $module properties of your content item, so you’ll need to create an editor extension that matches that. Displaying You can display the comments however you like, however, to ensure that the JavaScript features such as AJAX replying and editing works, you must apply certain attributes. Here is example markup: <div data-controller='core.commentFeed' data-feedID='messages- {$item->id}'> <br> {{if $item->commentPageCount() > 1}} {$item->pagination()|raw} <br><br><br> {{endif}} <div data-role='commentFeed'> {{foreach $item->comments() as $comment}} {$comment->html()|raw} {{endforeach}} </div> <div data-role='replyArea'> {$item->commentForm()|raw} </div> </div> These methods will use generic templates. To customise the template used, add either or both of these methods to your Comment model (the values shown here are their default values): /** * @brief [Content\Comment] Comment Template */ public static $commentTemplate = array( array( 'global', 'core', 'front' ), 'commentContainer' ); 64 /** * @brief [Content\Comment] Form Template */ public static $formTemplate = array( array( 'forms', 'core', 'front' ), 'commentTemplate' ); 65 Content Item Changes Additional properties and methods in \IPS\Content\Item static $commentsPerPage Controls the number of comments per page. Defaults to 25. commentPageCount() Returns the number of pages of comments. commentPagination() Returns the HTML for pagination links comments( int $limit=NULL, int $offset=NULL, string $order=‘date’, string $orderDirection=‘asc’, \IPS\Member $member=NULL ) Returns either an \IPS\Content\Comment object (if $limit is 1) or an array of comments. See phpdocs for full details. commentForm() Returns the HTML for the reply form. lastCommentPageUrl() Returns the URL for the last page of comments. canComment() Returns a boolean value indicating if the member logged in can comment. Behaviour Changes • stats() will now return an additional element including the number of comments.
 66 Searching Introduction Comments can be included in search results and the activity stream alongside content items. Other Requirements Searching must be implemented on the content items. Behaviour that changes once implemented Comments will be indexed and included in search results and the activity stream. The index will automatically be updated when comments are created, edited, moved, hidden, etc. and works automatically with both the MySQL index and Sphinx. Additional methods available once implemented None. How to implement 1. Add an interface: implements \IPS\Content\Searchable
 67 Reports Introduction Comments can be reported to moderators. Other Requirements None. Behaviour that changes once implemented • A “Report” button will automatically appear next to the comment for members that can report the comment. Additional methods available once implemented canReport( [$member] ) Checks if the member (either the given member or the currently logged in member) can report the content, taking into consideration if their group is allowed to submit reports, if they can view the content, if they are the author and if they have already reported that item. report( $reportContent ) Submits a report. How to implement 1. Add an interface: implements \IPS\Content\ReportCenter 2. Add a new properties to define a CSS class name to represent your content item when viewing reports. public static $icon = 'icon'; 
 68 Edit History Introduction Comments can log who has edited the comment and the changes that have been made. Other Requirements None. Behaviour that changes once implemented • When editing, the details of the edit will be logged and displayed on the comment. The exact behaviour depends on the site’s configuration. Additional methods available once implemented editLine() Returns the “Edited x days ago by User” line to display next to the comment. editHistory( bool $staff ) Returns an Iterator with the edit history. How to implement 1. Add an interface: implements \IPS\Content\EditHistory 2. Add the following elements to $databaseColumnMap Element Description Required? edit_time Unix timestamp of when edit was made Yes edit_show Boolean indicating if edit message should show Yes edit_member_name Username of member making the edit Yes edit_reason Reason for the edit No, but recommended edit_member_id ID of member making the edit No 69 Hiding / Approving Introduction Comments can be hidden to non-staff members. This can be used to require staff approval before comments can be viewed, and as a way for staff members to hide undesirable comments. Other Requirements Requirements: None. Optional: • Moderator permissions Behaviour that changes once implemented loadAndCheckPerms(...) will now throw an OutOfRange exception if a hidden content item is loaded and the currently logged in user does not have permission to view is hidden content. Hidden comments will only show to users who have permission to view hidden comments. Users with appropriate moderator permission will see buttons to hide/unhide in comments. Users who are set to have all new content moderated will have their comments hidden until it is manually approved. Additional methods available once implemented hidden() Returns a number indicating the hidden state: • -1 means the content is hidden, but was previously unhidden. • 0 means the content is unhidden (default, normal status). • 1 means the content is hidden and was marked hidden at the time of posting. canHide( \IPS\Member $member=NULL ) Returns a boolean value indicating if $member (currently logged in member if NULL) can hide the item. Takes into consideration if it is already locked. canUnhide( \IPS\Member $member=NULL ) Returns a boolean value indicating if $member (currently logged in member if NULL) can unhide the item. Takes into consideration if it is already locked. A new method is also available in the content item class: 70 canViewHiddenComments( \IPS\Member $member=NULL ) Returns a boolean value indicating if $member (currently logged in member if NULL) can view hidden comments on the item. How to implement Add an interface: implements \IPS\Content\Hideable Add either a hidden or an approved element to $databaseColumnMap pointing to the column in the database which contains the status of the content. 3. Optionally,youcanalsoaddaunapproved_commentselementtothe $databaseColumnMap in the content item class pointing to the column containing the number of comments pending approval. 4. If a user is configured to have their content approved, their comments will be hidden by default automatically. You may want to override the method that handles this so, for example you can make certain nodes have all comments posted to items within them require approval. To do this, override the moderateNewComments(...) method in the content item class - for example: public static function moderateNewComments( \IPS\Member $member ) { if ( $this->item()->container()->bitoptions[‘moderation’] and !static::modPermission( 'unhide', $member, $this->item()- >container() ) ) { return TRUE; } return parent::moderateNewComments( $member ); } Element Value if unhidden (normal, default) Value if hidden (was previously unhidden) Value if pending approval hidden 0 -1 1 approved 1 -1 0 71 Reputation Introduction Users can “like” or give reputation on comments. A member’s total reputation displays on their profile. Other Requirements None. Behaviour that changes once implemented • Reputation buttons will automatically appear next to comments. Additional methods available once implemented reputation() Returns an integer indicating the current reputation for the item. canGiveReputation( int $type, \IPS\Member $member=NULL ) Returns a boolean value indicating if $member (currently logged in member if NULL) can give $type (1 for positive, -1 for negative) reputation on the comment. giveReputation( $type, \IPS\Member $member=NULL ) Gives reputation on the comment. Will throw DomainException if $member is not allowed to give reputation on the comment. repGiven( \IPS\Member $member=NULL ) Returns an integer (1 for positive, -1 for negative, 0 for no reputation given) indicating what reputation $member (currently logged in member if NULL) has given to the comment. How to implement 1. Add an interface: implements \IPS\Content\Reputation 2. Addapropertyspecifyingakeytodistinguishreputationgivenonthistypeofcontent item as opposed to another. It can be anything you like, though convention is to use the name of the column in your database table (with prefix) which stores the ID. public static $reputationType = ‘comment_id’; 72 Following Introduction Members can follow content items and receive a notification when new comments are posted in it. Other Requirements • The content items must be followable. Behaviour that changes once implemented • After posting a new comment, members who are following the content item it was posted in will receive a notification. If the comment needs to be approved by a moderator, the notifications will be delayed until the comment has been approved. Additional methods available once implemented These methods are available to the content item class, not the comment class: followers( int $privacy=3, array $frequencyTypes=array( ‘immediate’, ‘daily’, ‘weekly ), array $limit=array( 0, 25 ), string $order=‘name’ ) Returns an \IPS\Db\Select object which can be iterated on to get details about which members are following the item. See phpDocs for available options. $privacy is a bitwise value using the \IPS\Content\FOLLOW_* constants - the default value, 3, includes public and anonymous follows. How to implement 1. Addafollowbuttononthepagewhereyourcontentitemcanbeviewed: {template="follow" app="core" group="global" params=“'yourApp', ‘YourContentItemClass’, $item->id, $item->followers()->count( TRUE )"}
 Example: http://pastebin.com/BGnDQU7t
  13. 1 point
    Most of the classes you will work with while using Nodes and Content Items will extend \IPS\Patterns\ActiveRecord. Objects of this class represent a row in the database table. It is important that you use the methods provided by this class rather than calling the database directly, as when you add additional features to your content item, the methods will perform more complicated tasks. static load( int $id ) A factory method that retrieves record from the database and returns an object of your Content Item class. Results are cached so if you load the same item twice, a second database query will not be made and both calls will return the same object (the same object by reference - not a copy of the object). Example: $item = YourClass::load( 1 ); static constructFromData( array $data ) If, for some reason, you do need to select data directly from the database (for example, if you want to get the most recent record), and you have an array containing a row from the database, the constructFromData method allows you to create an object out of that data, without having to call load which would make another (unnecessary) database query. Example: $item = YourClass::constructFromData( \IPS\Db::i()->select(...)- >first() ); __get( mixed $key ) __set( mixed $key, mixed $value ) Though you do not call these methods directly - magic getters and setters allow you to get and set the values in the database row. A static property, $databasePrefix, can be set if all the columns in your database table start with the same prefix, then you do not need to included that. For example, let’s say your database table looks like this: item_id item_title item_author 1 Foo 1 2 Bar 1 6 item_id item_title item_author 3 Baz 1 You would set $databasePrefix: static $databasePrefix = 'item_'; You would load the first row like so (it will automatically look for columns called “id”): $item = YourClass::load( 1 ); And could then get the title like so: echo $item->title; If you want to override the behaviour for any particular column (for example, if you have a database column which stores a timestamp, and you want the getter/setter to handle \IPS \DateTime objects) you can define methods called get_<key> and set_<key>. In these methods the $_data property stores the raw values. For example: public function get_date() { return \IPS\DateTime::ts( $this->_data[‘date’] ); } public function set_date( $value ) { $this->data[‘date’] = $value->getTimestamp(); } save() After changing any properties, you must call the save method to actually save those changes to the database. Example: $item = YourClass::load( 1 ); $item->title = 'New Title'; $item->save(); delete() The delete method deletes the item from the database. Example: $item = YourClass::load( 1 ); $item->delete(); 7 __clone() A magic method exists to automatically adjust the primary key when you clone an item; Example: $item = YourClass::load( 1 ); $copy = clone $item; $copy->save(); echo $item->id; // 4 Bitwise Flags The ActiveRecord class provides special features to facilitate bitwise operation, allowing you to use one INT column in your database to store multiple binary values. To do this, you can define a static $bitOptions property in your class like so: public static $bitOptions = array( 'bitwise_column' => array( 'bitwise_column' => array( ), ), ); 'property_1' 'property_2' 'property_3' => 1, => 2, => 4, In this example, there is a database column (bitwise_column) storing bitwise data (if you needed to store more values than could be stored in a single INT field, you could add additional columns). This column stores 3 boolean values (property_1, property_2 and property_3). When defining properties, you must define them with the numeric value they will be represented by, so the number must double each time (1, 2, 4, 8, 16, etc.). The ActiveRecord will automatically provide an \IPS\Patterns\Bitwise object for this column, which implements \ArrayAccess. You can get and set values as if it was an array: /* Getting a value */ if ( $object->bitwise_column[‘property_1’] ) { // . . . } /* Setting a value */ $object->bitwise_column[‘property_2’] = FALSE; $object->save(); /* Getting database rows */ $rowsWithPropery1AsTrue = \IPS\Db::i()->select( ‘*’, ‘table’, \IPS \Db::i()->bitwiseWhere( \IPS\YourClass:: $bitOptions['bitwise_column'], 'property_1' ));
 8 Notes Class Definition When you create classes ion the IPS Community Suite, you will always define your class with a preceding underscore. Even though you do this, you do not call it with the preceding underscore. This is a technicality of how autoloading works. loadAndCheckPerms There is a method, loadAndCheckPerms( int $id ) which is common to all the classes worked with in this document, though it is not part of \IPS\Patterns\ActiveRecord. By default, it behaves the same as the load( int $id ) method provided by \IPS \Patterns\ActiveRecord, however, as you add additional features, it will check appropriate permissions. For example, once you implement front-end permissions, it will throw an OutOfRangeException if the currently logged in user does not have permission to view the object. Similarly, once you implement hiding content, it will throw an OutOfRangeException if used to load a hidden object and the currently logged in user does not have permission to view hidden objects. You should therefore always use this method over load( int $id ) when loading nodes, content items, comments and reviews on the front-end, or in any code called from the front-end.
  14. 1 point
    Mark

    File Uploads (*)

    To allow file uploads in your code, you use the \IPS\Helpers\Form\Upload class within the Form Helper. The administrator has the ability to control how to store different types of file - due to this, using an Upload field is slightly more complicated than most other form types. The FileStorage Extension You are required to create an FileStorage extension within your application which is mostly used to provide callbacks to locate files uploaded by your field. To get started, create an FileStorage extension file through the developer center for your application. A skeleton file will be created in the applications/app/extensions/core/FileStorage folder with example code. You will need to provide code for all the methods. For example, if you are storing each file in a row in a database table, the code might look something like this: <?php namespace IPS\forums\extensions\core\FileStorage; class _Key { /** * Count stored files * * @return int */ public function count() { return \IPS\Settings::i()->setting_key ? 1 : 0; } /** * Move stored files * * @param int $offset This will be sent starting with 0, increasing to get all files stored by this extension * @param int $storageConfiguration New storage configuration ID * @param int|NULL $oldConfiguration Old storage configuration ID * @throws \Underflowexception When file record doesn't exist. Indicating there are no more files to move * @return void */ public function move( $offset, $storageConfiguration, $oldConfiguration=NULL ) { $thing = \IPS\Db::i()->select( '*', 'my_table', 'image IS NOT NULL', 'id', array( $offset, 1 ) )->first(); \IPS\Db::i()->update( 'my_table', array( 'image' => (string) \IPS\File::get( $oldConfiguration ?: 'app_Key', $thing['image'] )->move( $storageConfiguration ) ), array( 'id=?', $thing['id'] ) ); } /** * Check if a file is valid * * @param \IPS\Http\Url $file The file to check * @return bool */ public function isValidFile( $file ) { try { \IPS\Db::i()->select( 'id', 'my_table', array( 'image=?', $file ) )->first(); return TRUE; } catch ( \UnderflowException $e ) { return FALSE; } } /** * Delete all stored files * * @return void */ public function delete() { foreach( \IPS\Db::i()->select( '*', 'my_table', "image IS NOT NULL" ) as $forum ) { try { \IPS\File::get( 'app_Key', $forum['image'] )->delete(); } catch( \Exception $e ){} } } } However the appropriate code to use will depend on the nature of how the content created by your files are stored. Creating the element When creating the element you must provide an $options parameter specifying the extension you just created. For example, the code to create your element will look something like: $form->add( new \IPS\Helpers\Form\Upload( 'my_upload_field', NULL, TRUE, array( 'storageExtension' => 'app_Key' ) ) ); Additional options are available to allow multiple file uploads, to restrict the allowed extensions, the maximum file size and more. See the source code for all the available options. Handling Submissions The value returned will be an object of \IPS\File (or an array of \IPS\File objects if the field allows multiple file uploads). You do not need to do anything with the file itself, as it has already been stored according to the administrators preference. You do however, have to save the URL to it (which you can get by casting the \IPS\File object to a string) as that is what you will need to get and manipulate the file later, and use within the extension you created earlier. For example, your code might look like: $form = new \IPS\Helpers\Form; $form->add( new \IPS\Helpers\Form\Upload( 'my_upload_field', NULL, TRUE, array( 'storageExtension' => 'app_Key' ) ) ); if ( $values = $form->values() ) { \IPS\Db::i()->insert( 'my_table', array( 'image' => (string) $values['my_upload_field'] ) ); } Manipulating the file later To get the \IPS\File object back, you simply call: $file = \IPS\File::get( 'app_Key', $url ); The first parameter being your extension, and the second being the URL you obtained when saving the form. You can then use this object to get the contents of the file, delete it, etc. See the phpDocs in \IPS\File for more information on what you can do with files. If it is an image file, you can also create an \IPS\Image object to resize, add a watermark, etc. To do this you call: $image = \IPS\Image::create( $file->contents() ); See the phpDocs in \IPS\Image for more information on what you can do with images.
  15. 1 point
    This article covers how to define versions for your application and use the upgrader to run queries to upgrade between versions. Coming from an IP.Board 3 app? If you have an application which was originally developed for IP.Board 3.x, you will need to do the following steps. It doesn't matter if you installed a fresh 4.0 install or upgraded from 3.x. Create a new application as normal in the AdminCP If you installed a fresh 4.x rather than upgraded, manually create the database tables in your SQL database. Under the Database Schema tab in the Developer Center for your application, import the database tables. It is important that you use the "Import From Database" tab as the other options will assume these are new tables. Under the Versions tab, add all of your previous versions and the queries/code needed to upgrade between those steps - this is discussed in detail below. There is an option to upload your old versions.xml file to import them quickly, but you will need to specify the upgrade for each version queries/code manually. How to define versions In the Developer Center for your application, there is a Versions tab which shows all the versions there has been for your application. It is important that you always have the latest, unreleased version of your application in here. For example, for a brand new application, you will have version "1.0.0" under the versions tab. When you build and release your application (version 1.0.0), you should straight afterwards add the next version you will be working on (1.0.1 for example) to the Versions list. This is because while you're working on your application, the system will automatically add any changes you make to the database schema to the upgrade routine for the latest version you have defined. Database Schema The Database Schema tab is where you define all of the tables that your application uses. When your application is installed for the first time, all of the tables you have defined or created. As you make changes to the database schema, the system will automatically add the appropriate queries to make those changes to the upgrade routine for the latest version. For example: say the current latest version of your application under the Versions tab is 1.0.0 and you have a table under the Database Schema tab. You release that version, and then add version 1.0.1 to the Versions tab. Later, you add a column to the table under the Database Schema tab. When you release version 1.0.1, the system will automatically: For new installs: Just create the table as it is defined, including the added column For upgrades: Add the column You do not need to manually add a statement to add the column to your upgrade routine. Custom Version Queries and Code Though the Database Schema system is good at automatically handling tables owned by your application, you may find you need to run other queries (for example UPDATE queries or queries to add columns to tables not owned by your application). This is done under the Versions tab. Simply click the "+" icon for the version that the query need to be ran for (for example, if the query needs to be ran when upgrading to 1.0.1, you would click the "+" button for version 1.0.1). You will notice there is a special "install" version which can be used to specify queries which should be ran on install. Since on a fresh install, those are the only queries that are ran, it may be necessary to add a query both to the "install" special version, and to the version you're working with. For example, if you're working on version 1.0.1 and decide you want to add a column to the core_groups table that you didn't add in version 1.0.1, you would need to specify the query in both the 1.0.1 version (for people upgrading from 1.0.0) and the special "install" version (for new installs). If you need to run code which cannot be expressed as a single query, you can also define custom code. To do this, click the "</>" button for the version that needs custom code. This will write a file to the applications/<your_app>/setup/<version>/ directory with a skeleton to get you started. Simply open that file and follow the instructions within it to add your code. You can also do this for the special "install" version. If you are coming from an IP.Board 3 app, you can manually create a queries.php file in the applications/<your_app>/setup/<version>/ directory with the contents of the old mysql_updates.php file (setting an $SQL variable). Because this functionality is only to accommodate legacy upgrades, the file must be created manually and cannot be done from the developer center. If there is only a small number of queries, it may be easier to rewrite them as a call to an \IPS\Db::i() method and add them using the normal method (clicking "+" for the version in the developer center). Uninstall Code The system will automatically delete any tables defined by your application's Database Schema when uninstalling. If you need to run code in addition to this, you can do this with the Uninstall extension. In the Developer Center for your application, under the Extensions tab, click the "+" button for core > Uninstall and create an extension (the name of it isn't important). This will write a file to the applications/<your_app>/extensions/core/Uninstall/ directory with a skeleton to get you started. Simply open that file and follow the instructions within it to add your code.
  16. 1 point
    Mark

    Update Checking (*)

    IPS Community Suite can periodically check for updates to your application, plugin or theme and present a message in the Admin CP if an update is available. When creating your application, plugin or theme you can specify a URL. IPS Community Suite will send a request to the specified URL periodically with a "version" parameter specifying the current version ID number. For example, if you specify the URL as: http://www.example.com/myapp/check.php And someone is using a version of your application with the version ID "10002", then the request will be sent to: http://www.example.com/myapp/check.php?version=10002 This URL should return with a JSON-encoded object with the following properties: version specifying the human-readable version number (e.g. "2.0.0") of the latest version available longversion specifying the version ID number (e.g. "20000") of the latest version available released specifying the unix timestamp of the date that version was released updateurl specifying the URL where the administrator can download the update For example, you might return something like this: { "version": "2.0.0", "longversion": 20000, "released": 1423841958, "updateurl": "http://www.example.com/myapp/download" } For a live example, the URL for checking for updates to the IPS Community Suite itself is: http://remoteservices.invisionpower.com/updateCheck You can visit this URL to see what is returned for the IPS Community Suite.
×
×
  • Create New...

Important Information

We use technologies, such as cookies, to customise content and advertising, to provide social media features and to analyse traffic to the site. We also share information about your use of our site with our trusted social media, advertising and analytics partners. See more about cookies and our Privacy Policy