Jump to content

Rikki

Members
  • Posts

    24,413
  • Joined

  • Last visited

  • Days Won

    84

 Content Type 

Downloads

Release Notes

IPS4 Guides

IPS4 Developer Documentation

Invision Community Blog

Development Blog

Deprecation Tracker

Providers Directory

Projects

Release Notes v5

Invision Community 5 Bug Tracker

Forums

Events

Store

Gallery

Everything posted by Rikki

  1. In most cases, your content items will exist inside container node structures that categorize them - for example. topics (item) in a forum (node), or images (item) in an album (node). A number of methods are available within the content item model to make working with these relationships easier. Adding support for container nodes The first step you need to take is add a new static property to your content item model. protected static $containerNodeClass = 'string'; This property value should be the name of your node's model class, allowing IPS4 to identify the relationship. Next, you need to add a container key to the $databaseColumnMap array in your model (see the content item model guide for more information). The value should be the name of the database column that contains the ID number of the container. Both parts are illustrated in this example: protected static $containerNodeClass = "IPS\yourapp\YourNodeClass"; public static $databaseColumnMap = array( //... Other values defined in your column map 'container' => 'item_parent_id' ); Additional methods available to content item models After adding support for containers, two methods become available: boolean canMove( \IPS\Member $member=NULL ) Returns a boolean indicating whether the provided user can move the item to a different container. $member (\IPS\Member, optional) The member whose permissions should be used. By default, the currently-logged in member is used. void move( \IPS\Node\Model $container [, boolean $keepLink=FALSE ] ) Moves the item into a different container. Note: this method does not check permissions; use canMove() first. $container (\IPS\Node\Model, required) The container node object into which the item will be moved. $keepLink (boolean, optional, default FALSE) If true, a dummy content item will be added that links to the new location of the old content item. Additional methods are also available to the container node model to help you work with collections of items; consult the container node model documentation for more details.
  2. Simply by being defined, a controller that extends \IPS\Content\Controller will inherit a lot of functionality. Like all kinds of controllers, your content item controller belongs in the <app>/modules/<location> directory, such as yourapp/modules/front/somemodule/somecontroller.php. If you haven't already read about standard controllers in IPS4, I recommend you go there now and grasp the fundamentals before continuing. Content item controllers are simply a more specialized version of the standard dispatcher controller, and so most of the functionality remains the same, but with methods specific to content items also available. Basic skeleton At its most basic, a content controller is simply: namespace IPS\yourapp\modules\front\yourmodule; class _yourcontroller extends \IPS\Content\Controller { } The key thing to note is that your controller extends \IPS\Content\Controller, rather than \IPS\Dispatcher\Controller directly. Just by doing this, your controller already has the ability to delete content - if you append &do=delete&id=X (where X is the item ID) to the item URL, then the controller will automatically check if the user has permission to delete the item (which, by default, is only if the user is a moderator with permission to delete any content). As you progress through the other steps of this guide, you'll implement other interfaces in your controller which will add new functionality (both automatic, and new methods you can call).
  3. Inheritance Chain Your content model extends several classes. In turn, these are: \IPS\Content\Item Provides the features of Content Items. It contains all the code for the various optional features of content items which you will activate by adding properties and interfaces to your model. The rest of the guides in this section cover implementing these features. \IPS\Content Provides a small number of features which are common to both Content Item models and Content Comment models (explained later) such as getting the author and working with $databaseColumnMap. \IPS\Patterns\ActiveRecord Provides the functionality to load items from the database, work with their properties, save and delete them. See the Active Records guide for more information on this class. Basic Skeleton <?php namespace IPS\yourapp; class _YourClass extends \IPS\Content\Item { /** * @brief Multiton Store */ protected static $multitons; /** * @brief Default Values */ protected static $defaultValues = NULL; /** * @brief Application */ public static $application = 'yourapp'; /** * @brief Module */ public static $module = 'yourmodule'; /** * @brief Database Table */ public static $databaseTable = 'table'; /** * @brief Database Prefix */ public static $databasePrefix = 'prefix_'; /** * @brief Database Column Map */ public static $databaseColumnMap = array( 'author' => 'author' ); /** * @brief Title */ public static $title = ‘thing’; /** * Get URL * * @param string|NULL * @return \IPS\Http\Url */ public function url( $action=NULL ) { $url = \IPS\Http\Url::internal( ... ); if ( $action ) { $url = $url->setQueryString( 'do', $action ); } return $url; } } Specifying your class properties Content item models require a few static properties to configure their behavior. Many come from the Active Record class. public static $application = 'string'; Required. The application key that the content item belongs to. public static $module = 'string'; Required. The module key that the content item belongs to. public static $multitons = array(); public static $defaultValues = NULL; Required. Inherited from \IPS\Patterns\ActiveRecord. These two properties are requirements of \IPS\Patterns\ActiveRecord. They don't need a value assigned; they simply need to be defined. public static $databaseTable = 'string'; Required. Inherited from \IPS\Patterns\ActiveRecord. The name of the database tablet that stores these content items. public static $databasePrefix = 'string'; Optional. Inherited from \IPS\Patterns\ActiveRecord. Specifies the field prefix this table uses. public static $databaseColumnMap = array(); Required. Features provided by the higher classes your model extends will examine this array to find out what columns certain things are stored as in your database. The following elements are required, and in all cases the value is with the $databasePrefix omitted: author Should contain the name of the column which contains the ID number of the member who posted the content. title Should contain the name of the column which contains the title of the content. date Should contain the name of the column which contains a unix timestamp of when the content item was created. ip_address Should contain the name of the column (without the $databasePrefix) which contains the IP address of the user who posted the item. public string $title = 'string'; The key for the language string that describes what your content item is (for example, 'Topic', 'File', 'Image', etc.) Available methods on Content Item models In addition to those provided by \IPS\Patterns\ActiveRecord (which work exactly the same as for nodes), a number of additional methods are available. \IPS\Patterns\ActiveRecordIterator static getItemsWithPermission( ... ) Gets content items that the current user has permission to access. See the phpDoc in /system/Content/Item.php for all supported arguments. \IPS\Member author() Returns the \IPS\Member object for the user that posted the content item. For example: $item = YourClass::load( 1 ); $user = $item->author(); echo $user->name; boolean canView( [ \IPS\Member $member=NULL ] ) Returns a boolean value indicating if the provided member can view the content item. By default will always return TRUE, but is affected if Permissions is enabled and if Hiding is enabled. $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member will be used. boolean canEdit( [ \IPS\Member $member=NULL ] ) Returns a boolean value indicating if the provided member can edit the content item. $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member will be used. boolean canDelete( [ \IPS\Member $member=NULL ] ) Returns a boolean value indicating if the provided member can delete the content item $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member will be used. boolean static modPermission( string $type [, \IPS\Member $member=NULL [, \IPS\Node\Model $container=NULL ] ] ) Returns a boolean value indicating if the provided member has permission to perform the action specified by the $type param in the specified $container (if provided) $type (string, required) The type of permission being checked. Acceptable values are: edit delete move feature (if Featuring is enabled) unfeature (if Featuring is enabled) pin (if Pinning is enabled) unpin (if Pinning is enabled) lock (if Locking is enabled) unlock (if Locking is enabled) hide (if Hiding/Approving is enabled) unhide (if Hiding/Approving is enabled) view_hidden (if Hiding/Approving is enabled) $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member will be used. $container (\IPS\Node\Model, optional) If provided, checks the permission specifically in this container node. void modAction( string $type [, \IPS\Member $member=NULL [, string $reason=NULL ] ] ) Performs the specified moderation action. Throws OutOfRangeException if the member does not have permission to perform this action. $type (string, required) The type of moderation action being performed. Consult the list in the previous method for acceptable values. $member (\IPS\Member, optional) If provided, uses this member's permissions when performing the check. By default, the currently-logged in member will be used. $reason (string, optional) Used only for hide/unhide actions; specifies the reason the action is being taken.
  4. 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 ...where example.com is your own URL, and ENDPOINT is one of the calls 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); The license key API is disabled by default, and this line enables it. There are two other relevant constants too: 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 concepts There are two important concepts to understand before getting started with the license key API: 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. Endpoints This section covers the available endpoints and the data they return. ?activate Call this to activate a license key, for example when a user first enters their key into your application. Request Parameters key The license key identifier The identifier (see 'important concepts', above) 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 Object response "OKAY" usage_id The usage ID that has been assigned. Be sure to save this in your application; it will be required for future calls (see 'important concepts', above) Error Codes (in addition to standard errors thrown by the API) 201 (MAX_USES) The key has already been activated the maximum number of times and cannot be activated again. ?check Call this to check a license key is still active. Must be from the same IP address that called activate was unless overridden by configuration. Request Parameters key The license key identifier The identifier associated with this license key (see 'important concepts', above) usage_id The unique usage ID for this license key (see 'important concepts', above) Response Object 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 standard errors thrown by the API) 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. ?info Call this to get information about a key, not specific to any usage ID. Request Parameters key The license key identifier The identifier associated with this license key (see 'important concepts', above) Response Object key The license key identifier The identifier associated with this license key 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. Error Codes (in addition to standard errors thrown by the API) There are no errors thrown by this endpoint other than the standard ones. ?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. Request Parameters key The license key identifier The identifier associated with this license key usage_id The unique usage ID associated with this license key extra JSON-encoded new 'extra' data Response Object status "OKAY" Error Codes (in addition to standard errors thrown by the API) 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.
  5. 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 this guide). Step 2: Write the 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 like this: <?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. 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 shown below. void onPurchaseGenerated( \IPS\nexus\Purchase $purchase, \IPS\nexus\Invoice $invoice ) Called when a purchase is generated; that is, when the invoice is marked as paid (either automatically, or by an admin). $purchase Purchase object representing a line item on an invoice. $invoice The invoice to which this purchase belongs void onRenew( \IPS\nexus\Purchase $purchase, int $cycles ) Called when a renewal invoice for a purchase is paid. $purchase Purchase object representing a line item on an invoice $cycles The number of cycles this renewal covers. When a user renews their purchase, they sometimes have the ability to pay for future renewals in one go, and $cycles represents the number of renewal periods that have been paid for. void onExpirationDateChange( \IPS\nexus\Purchase $purchase ) Called when the expiration date of a purchase changes, e.g. when the purchase is renewed. $purchase Purchase object representing a line item on an invoice void onExpire( \IPS\nexus\Purchase $purchase ) Called when a purchase expires. $purchase Purchase object representing a line item on an invoice void onCancel( \IPS\nexus\Purchase $purchase ) Called when a purchase is canceled. $purchase Purchase object representing a line item on an invoice void onDelete( \IPS\nexus\Purchase $purchase ) Called when a purchase is deleted. $purchase Purchase object representing a line item on an invoice void onReactivate( \IPS\nexus\Purchase $purchase ) Called when a purchase is reactivated after having expired. $purchase Purchase object representing a line item on an invoice void onTransfer( \IPS\nexus\Purchase $purchase, \IPS\Member $newCustomer ) Called just before a purchase is transferred to another customer account. $purchase Purchase object representing a line item on an invoice $newCustomer Member object for the customer account to which this purchase is being transferred void onChange( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage [, mixed $chosenRenewalOption = NULL] ) Called when a package in a purchase is upgraded or downgraded. $purchase Purchase object representing a line item on an invoice $newPackage The new package to which the purchase is being upgraded or downgraded $chosenRenewalTerm The renewal term chosen for the new package; either an integer or a \IPS\nexus\RenewalTerm object (or NULL)
  6. Handling account changes 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, then implement the methods changeEmail(), changePassword() and changeUsername() as appropriate. boolean canChange( string $type, \IPS\Member $member ) Indicates whether this login handler will support the user changing their username, email address or password. $type (string, required) The type of property that is changing (one of username, password or email) $member (\IPS\Member, required) The relevant member whose details are changing void changeEmail( \IPS\Member $member, string $oldEmail, string $newEmail ) Called when the user is changing their email address. $member (\IPS\Member, required) The member changing their email address $oldEmail (string, required) The user's current email address $newEmail (string, required) The user's new email address Other callbacks void logoutAccount( \IPS\Member $member, \IPS\Http\Url $redirectUrl ) Called when a user logs out. Note this is not called if the administrator uses the 'force log-out all users' tool in the AdminCP. $member (\IPS\Member, required) The member that is logging out $redirectUrl (\IPS\Http\Url, required) A URL as an \IPS\Http\Url object to which the user will be redirected after logging out void createAccount( \IPS\Member $member ) Called when a user's account is created. $member (\IPS\Member, required) The member object for the new account. void validateAccount( \IPS\Member $member ) Called when a user validates their account. Never called if account validation is disabled. If email validation is enabled, this is called after a user validates their email address or if the admin manually validates the account. If admin validation is enabled, this is called after the admin has validated the account. $member (\IPS\Member, required) The member object for the account that has been validated. void deleteAccount( \IPS\Member $member ) Called when the user account is deleted by an administrator. $member (\IPS\Member, required) The member being deleted. void banAccount( \IPS\Member $member, boolean $ban=TRUE ) Called when a user account is banned or unbanned. $member (\IPS\Member, required) The member account being banned or unbanned. $ban (boolean, optional, default TRUE) If true, indicates this account is being banned (false means unbanned). void mergeAccounts( \IPS\Member $member, \IPS\Member $member2 ) Called when two member accounts are merged. $member (\IPS\Member, required) The first member account, and the one that is being retained. At point of calling, this object has the original member data. $member2 (\IPS\Member, required) The second member account, and the one that will ultimately be deleted after the merge.
  7. 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(). boolean emailIsInUse( string $email [, \IPS\Member $exclude=NULL ] ) $email (string, required) The email address to check $exclude (\IPS\Member, optional) If provided, excludes the supplied member from the check for uniqueness boolean usernameIsInUse( string $username ) $username (string, required) The username to check
  8. 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; $handler The key for your login handler $member The existing account (as an \IPS\Member object) $details 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.
  9. 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 its 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.
  10. Basic skeleton 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 classname (without the fa- prefix) 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, as follows: // Something went wrong with the login handler which wasn't the user's fault. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::INTERNAL_ERROR ); // The password the user provided was incorrect. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::BAD_PASSWORD ); // The username or email address the user provided did not match any account. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::NO_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) throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT ); The acpForm(), link() and changeSettings() methods are described in the following sections of this guide.
  11. Basic skeleton 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; } } Auth types 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: // Username public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME; // Email address public $authTypes = \IPS\Login::AUTH_TYPE_EMAIL; // Username or email address public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL; If you want to base this off a setting, or do any other setup for your login handler, you can implement an init() method in your class, like so: public function init() { if( $SOME_SETTING ) { $this->authTypes = \IPS\Login::AUTH_TYPE_USERNAME; } } The authenticate() 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: // Something went wrong with the login handler which wasn't the user's fault. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::INTERNAL_ERROR ); // The password the user provided was incorrect. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::BAD_PASSWORD ); // The username or email address the user provided did not match any account. throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::NO_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) throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT ); 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.
  12. 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. See autoloading for more information.
  13. 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 IPS Community Suite as being logged in within the remote application, or by using a plugin with the IPS 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 IPS Community Suite as being logged in on those external applications, you will essentially need to load the Invision Community software and validate if the user is logged in. Reading the IPS Community Suite session If your external application is on the same domain or subdomain as your IPS Community Suite you can easily include the IPS 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 IPS 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. Warning Generally, when you are checking if a user is logged in through the IPS Community Suite in an external application you should redirect registration, login, logout, change email and change password requests back to the community. 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, 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 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). 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 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: /** * Guess if the user is logged in * * This is a lightweight check that does not rely on other classes. It is only intended * to be used by the guest caching mechanism so that it can check if the user is logged * in before other classes are initiated. * * This method MUST NOT be used for other purposes as it IS NOT COMPLETELY ACCURATE. * * @return bool */ public static function loggedIn() { /* Check to guess if the user might be logged in. We can do this, for instance, by checking if a cookie that might indicate the user is logged in has been set. Otherwise, guest page caching will kick in and a user who logs in may not see they are logged in initially because a cached guest page is served until they refresh. */ return parent::loggedIn(); } /** * Read Session * * @param string $sessionId Session ID * @return string */ public function read( $sessionId ) { /* Let normal class do its thing */ $result = call_user_func_array( 'parent::read', func_get_args() ); /* If we're already logged in, return */ if( $this->member->member_id ) { return $result; } /* Here we would check if the user is logged in according to the front end */ $this->checkIfUserIsLoggedIn(); /* If the user is logged in, we should now either create their account if it does not exist, or update their details if appropriate. For instance if the user's email address has changed, we should update their local account */ $this->createOrUpdateUserAccount(); /* If we're logged in now, set the cookies */ if( $this->member->member_id ) { /* At this point, we're logged in. Have a cookie. */ /* Calling setMember() automatically checks that a member_login_key exists and the expiration is still valid */ $this->setMember( $this->member ); /* Is this a known device? */ $device = \IPS\Member\Device::loadOrCreate( $this->member ); $device->anonymous = 0; $device->updateAfterAuthentication( true, \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\Standard' ) ); } 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. Note You should always use a globally unique user ID to match users in your external service to Invision Community. When you validate that a user is logged in, your service should return the user's ID, which you should then store in Invision Community. When later attempting to check if a user exists, you should use this ID to do so. To do this you will generally need to Add a column to core_members to store the ID, such as remote_id Add a hook on \IPS\Member like so /** * Add our own column to default fields */ public function __construct() { static::$databaseIdFields = array_merge( static::$databaseIdFields, array( 'remote_id' ) ); } parent::__construct(); When loading the member, pass the appropriate parameter like so $this->member = \IPS\Member::load( $remoteIdValue, 'remote_id' ); It may be tempting to try to just match users up by email address, however if a user changes their email address on the front end and then attempts to visit the community, there will be no way to match the accounts up, typically resulting in a new account being created for the (existing) user. Note that the OAuth support in Invision Community facilitates matching remote user IDs to local user IDs, so if you will be creating a custom login handler in addition to an SSO plugin you may wish to (1) map that custom login handler in the updateAfterAuthentication() call above and (2) use the built in login handler linking tables to map the remote ID, instead of modifying the core_members database table. Warning As with running the IPS Community Suite as a "Master", when IPS4 is configured as the "Slave" you may wish to redirect registration, login, logout, change password and change email requests for the community 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.
  14. What is an extension? One of the fundamental ways of implementing cross-application integration with the IPS4 framework is the concept of extensions. Put simply, an extension is a way for an application to implement specific behavior in specific parts of the suite. For example, there's an extension that allows an application to specify how its content is handled by the search system, and another that allows the application to build elements to be displayed on the "Edit Member" screen in the AdminCP. If your application provides an extension, it will be automatically loaded in the appropriate places. Creating an extension Extensions are created in the Developer Center for your application in the AdminCP. IPS4 will automatically generate the class file in the appropriate extension directory within your application; you'll be prompted to enter a classname for your extension when creating it. Each extension requires different methods, depending on what the extension does. Refer to this documentation or the individual source files in the /extensions directory of your application for more information. Types of extension core AdminNotifications Send admin notifications and display banners to administrators AdvertisementLocations Create additional locations for advertisements within the application. Announcements Provide control over where attachments should display within the application (for example: in which categories). BBCode Add a BBCode tag to the parsing engine. Build Run custom code when building the application. BulkMail Provide additional replacement tags within bulk mails. CommunityEnhancements Add third-party APIs and services to the Community Enhancements section of the AdminCP. ContactUs Add additional fields to the contact us form and provide control over where submissions to the Contact Us form should be sent ContentModeratorPermissions Add content moderator permissions. ContentRouter Specify the content items used within the application. CreateMenu Add elements to the '+Create' menu which displays in the header of every page. Dashboard Add content to the AdminCP Dashboard. DesignersMode Run code when designers mode is enabled and disabled. EditorLocations Specify the WYSIWYG editor locations used in the application. EditorMedia Add additional media sources to the 'Insert existing attachment' button on the editor. FileStorage Specify the types of files used by the application FrontNavigation Add tabs and menus to the global navigation bar. GroupForm Add group settings. GroupLimits Control how group settings should be handled for secondary groups. IncomingEmail Handle incoming emails. IpAddresses Specify areas where IP addresses are logged in the application. LiveSearch Add to the AdminCP live search feature. MFAArea Define an area to be protected by muti-factor authentication. MemberACPProfileBlocks Add a block to the AdminCP member view. MemberACPProfileContentTab Add a tab to the Content Statistics block within the AdminCP member view. MemberACPProfileTabs Add a tab to the AdminCP member view. MemberExportPersonalInformation Add additional data to the personal information XML file. MemberFilter Provide ways of filtering members for bulk mail, etc. MemberForm Add settings to AdminCP Edit Member form. MemberHistory Add data parsers for Member History display MemberRestrictions Show content and add fields to the associated form for the Warnings & Restrictions block within the AdminCP member view. MemberSync Run code for when a member account is created, edited, merged or deleted. MetaData Add content meta data types MobileNavigation Add items to the mobile app navigation bar. ModCp Add to the ModCP. ModCpMemberManagement Add filters to the ModCP member management page. ModeratorPermissions Add moderator permissions. Notifications Specify the types of notifications sent by the application. OutputPlugins Add template tags. OverviewStatistics Add a block to the user or activity overview statistics page Permissions Specify the nodes with permissions integration used within the application. Profile Add content to users' profiles. ProfileSteps Suggested steps to complete profiles Queue Specify background queue tasks used by the application. RssImport Import content from an RSS feed Sitemap Add to the sitemap. StreamItems Add extra items to activity stream Uninstall Run code when an application is uninstalled. nexus Item Sell items with Commerce integration.
  15. IPS4's theme system has a feature called template plugins, which are special tags that do something to the values you pass in. You'll see them throughout the templates - they look like this: {lang="..."} This tag displays the language string for the key you pass into it, and is probably the most commonly used one. But there's many others too, so let's review some of the useful ones you can use in your themes and addons. {member} If you need to show any data about a member, the {member} tag is very useful. It's a shorthand that can display properties and call methods on a member object, so it's much neater than the manual approach. It's used like this: // Get a property, like 'name' {member="name"} // Call a method, like 'link()' {member="link()"} By default, it will work with the currently logged-in member, but you can pass an id attribute to show data about any member: // Show the name of member #67 {member="name" id="67"} {expression} The expression tag allows you insert simple one-line PHP expressions into your templates. For example, if a variable is an array of values and you want to show one per line, instead of writing a loop, you could do: {expression="implode( '<br>', $myArray )"} {prefix} The prefix tag is unusual in that it's designed specifically for use in CSS files. It prefixes CSS styles with the various vendor prefixes, meaning instead of writing: .myClass { -webkit-transform: scale(3) rotate(45deg); -moz-transform: scale(3) rotate(45deg); -o-transform: scale(3) rotate(45deg); transform: scale(3) rotate(45deg); } You can write: .myClass { {prefix="transform" value="scale(3) rotate(45deg)"} } {hextorgb} Continuing with the CSS theme, next there's the "Hex to RGB" tag. If you're a theme designer and want to use a theme setting value but apply some transparency, this tag will be particularly useful to you. Color theme settings are simple hex values, e.g. #000000. To apply some transparency, you need to use the rgba notation however (the 'a' meaning 'alpha channel', otherwise known as transparency). The {hextorgb} tag does this for you. It accepts either a hex color, or a theme setting key. By default it outputs the same color in rgb notation, but if you want to add transparency, you can add an opacity parameter which will represent the alpha channel value. {hextorgb="#ff0000"} --> rgb(255,0,0) {hextorgb="page_background" opacity="0.6"} --> rgba(235,238,242,0.6) {truncate} Finally, there's the truncate tag. This tag takes some text (usually as a variable), and truncates it to the length you specify. By default it appends an ellipsis (...) to the end of the content, although this is configurable via the append parameter. {truncate="$someLongText" length="300"} Note that this isn't designed to be used on HTML markup; you may break your page if HTML tags are included in the text. For those cases, consider using the javascript ipsTruncate widget instead. I hope this overview of 5 lesser-known template tags will help you as you build themes or applications! Share your related tips in the comments.
  16. Translatable text fields can be used to allow the user to provide a different value for all of the different languages they have on their community. If only one language is installed, then a single text field is shown instead. Translatable fields are commonly used for when an administrator has to provide the name for something which may need to be different depending on the language. Tip Translatable fields are designed to be used inside the AdminCP only. They are not intended for use by users on the front-end. The Translatable class works with the three text field types available to IPS4 forms: text inputs, textareas and WYSIWYG editors. The class takes care of creating the underlying text fields for you. Values are saved into the language system, and retrieved later for use. To use translatable fields in your code, you use the \IPS\Helpers\Form\Translatable class within the Form Helper. Creating the element When creating the element, your $options parameter requires at least two values: app The key of the application that owns the language string key A unique key for this language string (as with standard language strings, alphanumeric only). If you are displaying a "create" form for something which hasn't been created yet, you can pass NULL as the key. Unlike other fields, the $defaultValue must be NULL. The class will automatically fill in the current value for the key by fetching it from the language system. For example, the code to create your element will look something like: $form->add( new \IPS\Helpers\Form\Translatable( 'my_translatable_field', NULL, TRUE, array( 'app' => 'app', 'key' => 'my_language_string' ) ) ); This adds a standard single-line text input to the form. To create a textarea instead, add a textArea key to your $options array, set to TRUE. To create a WYSIWYG editor, add an editor key to your $options array. The value of editor should be an array that contains the options accepted by the \IPS\Helpers\Form\Editor class. Handling submissions When processing a form containing Translatable fields, you must save the returned values to the language system manually, like so: if ( $values = $form->values() ) { \IPS\Lang::saveCustom( 'app', 'my_language_string', $values['my_translatable_field'] ); // Other field processing... }
  17. To use the WYSIWG editor in your code, you use the \IPS\Helpers\Form\Editor class within the Form Helper. Editors automatically have the ability to support attachments, and the administrator can customize the editor to make certain features available in some areas and not others. Because of this, using an Editor is slightly more complicated than most other form types. Creating an EditorLocations extension You are required to create an EditorLocations extension within your application which is mostly used to provide callbacks to locate attachments uploaded to that editor. To get started, create an EditorLocations extension file through the developer center for your application. A skeleton file will be created in the applications/app/extensions/core/EditorLocations folder with example code. You will need to provide code for the attachmentPermissionCheck() and attachmentLookup() methods. For example, if you are using this editor for an ACP setting, the value of which will be displayed on a certain page which is visible to all members who have access to the application, the code might look something like this: <?php namespace IPS\app\extensions\core\EditorLocations; class _Key { /** * Permission check for attachments * * @param \IPS\Member $member The member * @param int|null $id1 Primary ID * @param int|null $id2 Secondary ID * @param string|null $id3 Arbitrary data * @return bool */ public function attachmentPermissionCheck( $member, $id1, $id2, $id3 ) { return $member->canAccessModule( \IPS\Application\Module::get( 'app', 'module' ) ); } /** * Attachment lookup * * @param int|null $id1 Primary ID * @param int|null $id2 Secondary ID * @param string|null $id3 Arbitrary data * @return \IPS\Http\Url|\IPS\Content|\IPS\Node\Model * @throws \LogicException */ public function attachmentLookup( $id1, $id2, $id3 ) { return \IPS\Http\Url::internal( 'app=app&module=module&controller=controller', 'front' ); } } Or if you are using this editor for the description for Content Items that members create, the code might look something like: <?php namespace IPS\app\extensions\core\EditorLocations; class _Key { /** * Permission check for attachments * * @param \IPS\Member $member The member * @param int|null $id1 Primary ID * @param int|null $id2 Secondary ID * @param string|null $id3 Arbitrary data * @return bool */ public function attachmentPermissionCheck( $member, $id1, $id2, $id3 ) { try { return \IPS\app\Thing::load( $id1 )->canView( $member ); } catch ( \OutOfRangeException $e ) { return FALSE; } } /** * Attachment lookup * * @param int|null $id1 Primary ID * @param int|null $id2 Secondary ID * @param string|null $id3 Arbitrary data * @return \IPS\Http\Url|\IPS\Content|\IPS\Node\Model * @throws \LogicException */ public function attachmentLookup( $id1, $id2, $id3 ) { return \IPS\app\Thing::load( $id1 )->url(); } } However the appropriate code to use will depend on the nature of how the content created by your editor will be used. Note that you do not have to (and shouldn't) create a separate extension for every single editor. It is common practice for example, to use one extension for every setting field within your application. The $id parameters allow you to know specifically what piece of content is referenced, as explained below. You must also create a language string which identifies your editor with the key editor__<app>_<Key>. This is used to display to the admin when they are configuring which buttons show up in which areas. For example, in the core application, the key editor__core_Contact is defined as "Contact Form". Creating the form element When creating the element you must provide an $options parameter specifying the extension you just created, along with some additional information: autoSaveKey is a string which identifies this editor's purpose. For example, if the editor is for replying to a topic with ID 5, you could use "topic-reply-5". Make sure you pass the same key every time, but a different key for different editors. attachIds are discussed below. Can contain up to 3 elements, and the first 2 must be numeric, but the last can be a string For example, the code to create your element will look something like: $form->add( new \IPS\Helpers\Form\Editor( 'my_editor', NULL, TRUE, array( 'app' => 'app', 'key' => 'Key', 'autoSaveKey' => 'my-editor-field', 'attachIds' => array( ... ) ) ); Claiming attachments Generally speaking, there are two types of content: values which always exist (like settings) and content which is created and deleted. Attachments are handled differently for each: Values which always exist Pass an identifier to the attachIds parameter. For example, you might do: 'attachIds' => array( 1 ) And then in your extension you will look at $id 1 and know that 1 is for this instance of the editor. Then you would use different numbers for other editors using the same extension. Even if there is only one editor using this extension, you must provide a value for attachIds. The system will then automatically handle claiming attachments. Content which is created and deleted When displaying the editor on the "create" screen you will pass NULL for attachIds (because of course at this point you don't know what ID you will save it with). Then, in the code for your form which handles creating the content, after you have created the content and therefore have an ID for it, you call this code: \IPS\File::claimAttachments( $autoSaveKey, $id1, $id2, $id3 ); $autoSaveKey is the same value you used for the autoSaveKey parameter when creating the editor. Each of the $id fields are optional but you must provide at least one. They are what will be passed to the method in your extension. When displaying the editor on the "edit" screen, you pass the ID values to attachIds and do not call claimAttachments. For example: $editing = NULL; if ( \IPS\Request::i()->id ) { try { $editing = \IPS\app\Thing::load( \IPS\Request::i()->id ); } catch ( \OutOfRangeException $e ) { \IPS\Output::i()->error( ... ); } } $form = new \IPS\Helpers\Form; $form->add( new \IPS\Helpers\Form\Editor( 'my_editor', NULL, TRUE, array( 'app' => 'app', 'key' => 'Key', 'autoSaveKey' => !$editing ? 'creating-thing' : "editing-thing-{$editing->id}", 'attachIds' => $editing ? array( $editing->id ) : NULL ) ) ); if ( $values = $form->values() ) { if ( !$editing ) { $item = new \IPS\app\Thing; $item->content = $values['my_editor']; $item->save(); \IPS\File::claimAttachments( 'creating-thing', $item->id ); } else { $editing->content = $values['my_editor']; $editing->save(); } } Displaying the contents of an Editor field The value is automatically parsed including replacing profanity and other settings configured by the administrator, and sanitized of any security concerns. You can safely store the display value and display it without any further parsing. If you simply output the value of the editor in your template, you would notice that entities are double-escaped. This is because by default, the IPS4 template system escapes all output to prevent XSS vulnerabilities. In the case of editor content, however, you can safely use the unescaped version of the content because the form helper will have already taken care of sanitization for you. Therefore, to use editor content in a template, you would use this: {$val|raw} The |raw modifier disables automatic escaping for this variable by the template system.
  18. What are toggles? Frequently in form design you'll want to show or hide groups of fields depending on the values of other fields. For example, assume you have a Yes/No field that enables some feature, and some other fields that customize how the feature works. When the user checks 'No' to disable the feature, you can hide the other fields using toggles (and show them automatically when 'Yes' is checked). By doing so, you simplify your form design, and make it easier for users to understand each field they see, with the correct context. IPS4 has built-in support for this kind of behavior on forms using the toggles functionality. It's automatic - you simply specify the relevant element IDs when building your form. Using form toggles 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 behavior to when the checkbox is unchecked. For more information, see the source code for each element type in the system/Helpers/Form directory. All toggle options accept an array of HTML element IDs that should be toggled. For example, to toggle an element with the ID 'myElement' from a YesNo field, you'd do: $form->add( new \IPS\Helpers\Form\YesNo( 'yes_no_field', NULL, TRUE, array( 'togglesOn' => array( 'myElement' ) ) ) ); Note that if you plan to toggle other form elements, you will need to manually specify an element ID (the last parameter of the field constructor). For example, to have our YesNo field toggle a text field, we must also specify an ID on the text field like so: $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' ) );
  19. For more complex forms, the \IPS\Helpers\Form helper provides a number of methods to help you organize and group your form fields: Tabs (including determining which tab a field with an error exists in) Headers (to group related fields under a title) Sidebars (to enable you to add contextual content to a form) Separators (to break a form into logical pieces) Using them is simple: $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\Text( 'another_text_field' ) ); The form is built sequentially, so you should call addTab, addHeader etc. followed by the fields that should appear in that tab or under that header, before adding another tab, and so on. Tabs $form->addTab( string $lang [, string $icon=NULL [, string $blurbLang=NULL [, string $css=NULL ] ] ] ) $lang (string, required) The key of the language string to be used on this tab. $icon (string, optional) If provided, will display an icon on the tab. Should be a FontAwesome icon classname, without the fa-. For example, cloud-download or envelope-o $blurbLang (string, optional) If provided, will add some description text to the top of the tab contents, above the fields. Should be the key of the language string to use. $css (string, optional) A string of space-separated CSS classnames to add to the tab content wrapper (note, not the tab itself). Headers $form->addHeader( string $lang ) $lang (string, required) The key of the language string to use for this header Separators $form->addSeparator() This method doesn't take any parameters. Sidebar Each tab can have its own separate sidebar (but a maximum one sidebar per tab, of course). $form->addSidebar( string $html ) $html (string, required) HTML to show as the sidebar. You'd typically call a template here, for example: $form->addSidebar( \IPS\Theme::i()->getTemplate( 'group', 'app', 'location' )->myTemplate() );
  20. Rikki

    Displaying a form

    Outputting the form Casting the $form object to a string returns the HTML to display the form. You don't usually need to cast the form explicitly, simply adding it to the output buffer (e.g. \IPS\Output::i()->output) will be fine. e.g. \IPS\Output::i()->output .= $form; Adding custom CSS to a 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'; The class property is a simple string (not an array), so simply use a space-separated list of CSS classnames. If you use your own CSS styles rather than built-in classnames, don't forget to include your CSS file too! Using custom form templates By default, standard built-in form templates are used when displaying a form. For further customization though, you can call $form->customTemplate() passing a callback with a template to use. This allows you to totally customize 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' ) ); The template you create must contain the following template header, including these parameters: <ips:template parameters="$id, $action, $elements, $hiddenValues, $actionButtons, $uploadField, $class='', $attributes=array(), $sidebar, $form=NULL" /> Passing custom parameters If you need to pass additional custom parameters to your form template, you can do that in the second parameter of the customTemplate method, like so: $templateToUse = array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ); $form->customTemplate( $templateToUse, $myCustomParameter, $anotherParameter ); Your parameters are added to the front of the template parameter list, meaning in this case, your template header would be: <ips:template parameters="$myCustomParameter, $anotherParameter, $id, $action, $elements, $hiddenValues, $actionButtons, $uploadField, $class='', $attributes=array(), $sidebar, $form=NULL" />
  21. Purpose of the Form Helper Much of the UI in IPS4 revolves around forms, whether it's the numerous settings screens in the AdminCP, replying to content on the front end, configuring your profile, and so on. The IPS Community Suite has a powerful Form Helper class that simplifies implementing forms in your applications and plugins. It provides features such as automatic validation and built-in security, and (in the case of more complex field types such as matrices and node/tree selectors) takes care of generating the UI and dynamic behavior for you. Forms created with the Form Helper support a wide range of field types, have built-in tab support, use HTML5 features where applicable, and more. If you require input from users in your application or plugin, you should always use the Form Helper; never implement such functionality manually and bypass the framework. Here's a basic stub showing how your form code will eventually look: // Create the form instance $form = new \IPS\Helpers\Form; // Add our form fields $form->add( ... ); $form->add( ... ); if ( $values = $form->values() ) { // Form submitted; handle the form values here } // Send the form for output \IPS\Output::i()->output = $form; Creating a form To create a form, you simply create an instance from the Form Helper. To the returned object you can add fields, tabs, and more (see the following sections). // Create a form using defaults $form = new \IPS\Helpers\Form; No parameters are required, but there's a few optional parameters you can pass: \IPS\Helpers\Form( [ string $formID='form' [, string $submitLang='save' [, \IPS\Http\Url $actionURL=NULL [, array $attributes=array() ]]]] ); $formID (string, optional) The ID to use in the HTML id attribute for this form $submitLang (string, optional) The language key of the string to use in the primary 'submit' button for this form $actionURL (\IPS\Http\Url, optional) A URL object representing the URL the form should post to (by default, forms post to the same page that displays them, for easy processing) $attributes (array, optional) An array of key/values of other HTML attributes you might want to include in the <form> tag, such as for javascript functionality. 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') ); There's a wide range of built-in form field types for you to use. 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 All form field classes have the same parameter signature. Each is customized by the values you pass in. Here are the parameters you can pass to form field classes: __construct( string $name [, mixed $defaultValue=NULL [, boolean $required=FALSE [, array $options=array() [, function $customValidationCode=NULL [, string $prefix=NULL [, string $suffix=NULL [, string $id=NULL ]]]]]]] ); $name (string, required) The field name (used in the HTML name attribute. $defaultValue (mixed, optional) The default value of the field, in whatever is the appropriate type for this field $required (boolean, optional) Is this field required? Note: passing true sets the HTML5 required attribute, meaning the form can't be submitted unless this is filled. Passing NULL instead disables the HTML5 required attribute, allowing you to validate it manually on the backend instead. $options (array, optional) An array of options. The acceptable values vary depending on the field type; consult the individual classes for more information. $customValidationCode (function, optional) A closure that validates the value of the field $prefix (string, optional) HTML string to show before the field HTML $suffix (string, optional) HTML string to show after the field HTML $id (string, optional) The ID to be used in the HTML id attribute 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 ) ) ); 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. In addition to labels and descriptions using language string's automatically, it is also possible to override this behaviour and set your own values in the controller. For example: $input = new \IPS\Helpers\Form\Text( 'my_select_box', NULL, TRUE ); $input->description = "My Description"; $input->label = "My Label"; It is important to note that when setting descriptions in this way they will be added to the for exactly as provided with no additional markup. If markup is required then you should set the property using a template. Validation Most classes will provide automatic validation, and their $options provide ways of customizing 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'); } } ) ); 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). Be aware that CSRF protection is handled automatically when you use the centralized \IPS\Helpers\Form class and an error message will be shown if the CSRF key does not match the expected value. 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. 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.
  22. Rikki

    Handling 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 a FileStorage extension within your application which is mostly used to provide callbacks to locate files uploaded by your field. To get started, create a 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 of 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 form 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 at a later date, 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.
  23. 1. Create the Email Template The content for emails is stored in the dev/email folder of the application which will send the email. For example, if you look in the applications/core/dev/email folder (you will need to be in developer mode to see that folder) you will see all the emails the core application sends. All emails sent from the IPS Community Suite are sent in two formats: HTML (for most email clients) and plain text (for older email clients or users who specifically do not want to see HTML emails). For each type of email that can be sent, there are two files: key.phtml (which contains the HTML version) and key.txt (which contains the plaintext version). Note that both types are sent to the user, and their individual email client will determine the most appropriate one to display. To send your own type of email, you will therefore need to create both of these. They key you use can be anything so long as it is unique. The first line of both files should be a tag like this: <ips:template parameters="$parameter1, $parameter2, $email" /> Later (in step 3) when you call the code to send your email you can pass whatever parameters you want which can then be used in your templates. A final parameter, $email, will also always be passed which contains the \IPS\Email object you're working with. Within the templates, you can include template logic and most template tags, however be careful: the email you're sending will probably be to a different user to who is logged in when the email is sent, so do not use anything which is dependent on the currently logged in member. most importantly, do not use {lang=""} template tag (which is always based on the currently logged in member). Instead, you can access $email->language which is an \IPS\Lang object for the correct language of the recipient. There's also a special tag, {dir}, that you can use in your template. This is either ltr or rtl depending on the recipient's language, and is used (most notably with <table>s) to ensure the email layout is appropriate for the language. Usage example: <td dir='{dir}' valign='middle' align='center' style="..."> When creating your HTML template, be aware that email clients use very different standards to browsers and many normal techniques cannot be used. We recommend you use a template from one of the IPS applications as your base, and you may want to use a tool such as Litmus. When creating your plaintext template, be aware that you cannot use any HTML tags, and whitespace is significant; that is, any whitespace in your template will be displayed in the email. 2. Add Subject Language Strings The system will automatically look for a language string with the key mailsub__<app>_<key> to use as the subject, so add that to your dev/lang.php file. Note that you can use the parameters you pass to the template - for example, you will notice in applications/core/dev/lang.php: 'mailsub__core_notification_new_likes' => '{$member->name|raw} liked your post', 3. Send the Email The actual code to send the email is very simple: \IPS\Email::buildFromTemplate( 'app', 'key', $params )->send( $member ); The send() method can be passed either an \IPS\Member object (this is best as the system will automatically customize the email for the user, including choosing the correct language) or an email address as a string (which you should only do if you need to send an email to unregistered users) or an array of either of these to send to multiple recipients. You can also pass second and third parameters for users to CC and BCC respectively. If you want to customize the content of the message depending on the member you can include variables in your template with the format *|key|* and then send like so: Template: <td> *|some_key|*<br> </td> PHP: \IPS\Email::buildFromTemplate( 'app', 'key', $params, TRUE )->mergeAndSend( array( 'user1@example.com' => array( 'some_key' => 'value for user 1', ), 'user2@example.com' => array( 'some_key' => 'value for user 2', ) ) );
  24. 1. Create an extension The first step is to create a Notifications extension. In the Developer Center for your application go to Extensions > core and click the "+" button on the "Notifications" row. A file will be created in your application's extensions/core/Notifications folder with a skeleton to get you started. Open that file and follow the instructions within it to add your notification types to the Notification Options screen and create the method(s) which will control what gets displayed when the user clicks on the notification icon in the header. 2. Create an Email Template You must create the email templates for what will get sent to users who have opted to receive emails for your notification. Follow the instructions from Steps 1-2 of the Emails guide. For your templates, use notification_key as the key (where key is the notification key you specify in getConfiguration()). For example: the notification type new_status (in the core application) uses the email templates notification_new_status which for it's subject uses the language string mailsub__core_notification_new_status. 3. Send the Notification The actual code to send the notification is very simple: $notification = new \IPS\Notification( \IPS\Application::load('app'), 'key', $item, $params ); $notification->recipients->attach( \IPS\Member::load(1) ); $notification->send(); $item Should be an object of the thing the notification is about, which is used to prevent the same notification being sent more than once or NULL if not applicable. If it is a content item (it doesn't have to be, but if it is) the notification will automatically remove the recipient if they don't have permission to read the item or are ignoring the user who posted it. $params Any parameters that you want to pass to the email template. You call $notification->recipients->attach() for each member you want to send the notification to, passing an \IPS\Member object.
  25. Notifications or Email? You have a number of options available for you for sending notifications to members: Following Members can follow other members, content containers (nodes like categories) and content items (like topics) to be notified about content that interests them. For information on how to implement this, see the Following sections for the Content Items and Comments documentation. Notifications Members can choose how to be notified about certain events (if they want to receive an inline notification, an email, both or neither) and you can send them notifications honoring those settings. For example, notifications are sent when someone posts on another user's profile. For information on how to implement this, see below. Email You can also manually send an email to a member. For information on how to implement this, see below. Though all 3 systems interact with each other, it is important not to confuse them: the follow system sends a notification which may be an email. You can also send a notification which may be an email, or you can just send an email. If you need to send a notification to a user, you should think carefully about whether a notification or an email is appropriate. Generally notifications are best for telling a user about something that has happened that they might be interested in (for example, notifications are sent when someone posts on another user's profile) and emails are best for things which are in response to a specific action, or require action from the user (for example, an email is sent when a product a user has purchased from Commerce is shipped).
×
×
  • Create New...