IPS Staff
  • Content count

  • Joined

  • Last visited

About Mark

  • Rank
    Meet Jay
  • Birthday March 04

IPS Marketplace

  • Resources Contributor Total file submissions: 5

Profile Information

  • Gender Male
  • Location Colchester, UK

Recent Profile Visitors

126,289 profile views

Mark's Activity

  1. Mark added a article in Development   

    Versions and Upgrading your application
    This article covers how to define versions for your application and use the upgrader to run queries to upgrade between versions.
    Coming from an IP.Board 3 app?
    If you have an application which was originally developed for IP.Board 3.x, you will need to do the following steps. It doesn't matter if you installed a fresh 4.0 install or upgraded from 3.x.
    Create a new application as normal in the AdminCP If you installed a fresh 4.x rather than upgraded, manually create the database tables in your SQL database. Under the Database Schema tab in the Developer Center for your application,  import  the database tables. It is important that you use the "Import From Database" tab as the other options will assume these are new tables. Under the Versions tab, add all of your previous versions and the queries/code needed to upgrade between those steps - this is discussed in detail below. There is an option to upload your old versions.xml file to import them quickly, but you will need to specify the upgrade for each version queries/code manually.  
    How to define versions
    In the Developer Center for your application, there is a Versions tab which shows all the versions there has been for your application. It is important that you always have the latest, unreleased version of your application in here. For example, for a brand new application, you will have version "1.0.0" under the versions tab. When you build and release your application (version 1.0.0), you should straight afterwards add the next version you will be working on (1.0.1 for example) to the Versions list. This is because while you're working on your application, the system will automatically add any changes you make to the database schema to the upgrade routine for the latest version you have defined.
    Database Schema
    The Database Schema tab is where you define all of the tables that your application uses. When your application is installed for the first time, all of the tables you have defined or created. As you make changes to the database schema, the system will automatically add the appropriate queries to make those changes to the upgrade routine for the latest version.
    For example: say the current latest version of your application under the Versions tab is 1.0.0 and you have a table under the Database Schema tab. You release that version, and then add version 1.0.1 to the Versions tab. Later, you add a column to the table under the Database Schema tab. When you release version 1.0.1, the system will automatically:
    For new installs: Just create the table as it is defined, including the added column For upgrades: Add the column You do  not  need to manually add a statement to add the column to your upgrade routine.
    Custom Version Queries and Code
    Though the Database Schema system is good at automatically handling tables owned by your application, you may find you need to run other queries (for example UPDATE queries or queries to add columns to tables not owned by your application). This is done under the Versions tab. Simply click the "+" icon for the version that the query need to be ran for (for example, if the query needs to be ran when upgrading to 1.0.1, you would click the "+" button for version 1.0.1).
    You will notice there is a special "install" version which can be used to specify queries which should be ran on install. Since on a fresh install, those are the only queries that are ran, it may be necessary to add a query both to the "install" special version, and to the version you're working with. For example, if you're working on version 1.0.1 and decide you want to add a column to the core_groups table that you didn't add in version 1.0.1, you would need to specify the query in  both  the 1.0.1 version (for people upgrading from 1.0.0) and the special "install" version (for new installs).
    If you need to run code which cannot be expressed as a single query, you can also define custom code. To do this, click the "</>" button for the version that needs custom code. This will write a file to the applications/<your_app>/setup/<version>/ directory with a skeleton to get you started. Simply open that file and follow the instructions within it to add your code. You can also do this for the special "install" version.
    Uninstall Code
    The system will automatically delete any tables defined by your application's Database Schema when uninstalling. If you need to run code in addition to this, you can do this with the Uninstall extension. In the Developer Center for your application, under the Extensions tab, click the "+" button for core > Uninstall and create an extension (the name of it isn't important). This will write a file to the  applications/<your_app>/extensions/core/Uninstall/  directory with a skeleton to get you started. Simply open that file and follow the instructions within it to add your code.
    • 0 replies
  2. Mark added a bug in IPS4 Bug Tracker   

    Commerce Upgrader
    Commerce's Upgrader doesn't go back further than the current latest 3.x version.
    • 0 replies
  3. Mark added a post in a topic: IP. Chat 4.0?   

    It will come. It's not ready yet though  
  4. Mark added a article in Development   

    Pluralized Language Strings
    Some language strings in IPS Community Suite have a format like this:
    {# [1:post][?:posts]} This is to accommodate how words can change depending on the number of things they are referring to. In this example, the string may display "1 post" or "2 post s " depending on the number passed to it. Although in English there is usually only 2 forms (singular and plural), the syntax is designed to be flexible so that any language can be accommodated.
    Basic Format
    The basic format is:
    {#[x:value]} The meaning of each section is:
    The curly braces mark the boundaries and are required. The # at the start is where the number will go and is also required and must be at the front. Each set of square brackets represents a possible value. It contains the value, followed by a colon, followed by the word(s) to display if that is the value. At the start of the square brackets, the value (represented here by "x") can be a number, or "?" for all other values not specified.  
    Hiding or moving the number
    If you do not want to display the number, you can add an ! before the #. For example:
    %s {!#[1:likes][?:like]} this Will display "%s likes this" if the value passed is 1 or "%s like this" for any other value.
    If you do this, you can display the number elsewhere by placing another # where you want the number to display. For example:
    {!#[1:week][?:# weeks]} Will display "week" if the value passed is 1, or " x  weeks" for any other value.
    Including additional content
    You can include additional characters where desired within the syntax. For example:
    {# anonymous [1:member][?:members]} Will display "1 anonymous member" is the value passed is 1 or " x  anonymous members" for any other value.
    Rather than provide a specific number or "?" for all other values, you can also use wildcards. * will match anything which ends with the following number and % will match anything that begins with it. Though these are never used in English, they are necessary in some languages.
    For example:
    {#[*1:posts][*2:posts][?:posts]} Though all display the same value, the first block will match any number ending with 1 (for example: 1, 21, 171), the second block any number ending with 2, and the final block matching any number.
    {#[%1:posts][%2:posts][?:posts]} Though all display the same value, the first block will match any number beginning with 1 (for example: 1, 12, 108), the second block any number beginning with 2, and the final block matching any number.
    Multiple Numbers
    You can create strings which accept multiple numbers, and prefix any #s with an index.
    For example:
    {0# [1:post][*2:posts]} and {1# [1:view][*2:views]} Will expect two values. If passed 1 and 2, it will return "1 post and 2 views".
    • 0 replies
  5. Mark added a post in a topic: External tools for advanced users   

    Deleting the stored files should cause them to be rebuilt. There was an issue with how this is handled for theme-related files which will be fixed in the next beta  
    And yeah, isn't it awesome? Just include init.php and you're away  
  6. Mark added a article in Development   

    Translatable Text Fields
    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 they have only one, they will appear to be a regular text field). They are commonly used for when an administrator has to provide the name for something which may need to be different depending on the language. They are not designed to be used outside of the ACP .
    To use translatable fields in your code, you use the \IPS\Helpers\Form\Translatable class within the  Form Helper . U sing a Translatable field is slightly more complicated than most other form types .
    Creating the element
    When creating the element you must provide an $options parameter specifying an application which "owns" the language string and a key. 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, $defaultValue must be NULL. The system will automatically fill in the current value for the key.
    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' ) ) );  
    Handling Submissions
    You must save the returned value manually like so: \IPS\Lang::saveCustom( 'app', 'my_language_string', $values['my_translatable_field'] );  
    Text area and editors
    You can make the field a texture rather than single-line textbook by setting the "textArea" element in $options to TRUE. To use a full WYSIWG editor, you must make  all the normal considerations for an editor field . Then set the "editor" element in $options to what you would normally set as $options in the \IPS\Helpers\Form\Editor object.
    • 0 replies
  7. Mark added a bug in IPS4 Bug Tracker   

    Content databases auto save
    When I create a new database record, I get the autosaved content of the record I was last editing. I'm guessing it's mistakenly using the same autoSaveKey for edits as for creates.
    • 0 replies
  8. Mark added a article in Development   

    File Uploads
    To allow file uploads in your code, you use the \IPS\Helpers\Form\Upload class within the  Form Helper .
    The administrator has the ability to control how to store different types of file - due to this, using an Upload field is slightly more complicated than most other form types.
    The FileStorage Extension
    You are required to create an FileStorage extension within your application which is mostly used to provide callbacks to locate files uploaded by your field.
    To get started, create an FileStorage extension file through the developer center for your application. A skeleton file will be created in the applications/app/extensions/core/ FileStorage folder with example code. You will need to provide code for all the methods.
    For example, if you are storing each file in a row in a database table, the code might look something like this:
    <?php namespace IPS\forums\extensions\core\FileStorage; class _Key { /** * Count stored files * * @return int */ public function count() { return \IPS\Settings::i()->setting_key ? 1 : 0; } /** * Move stored files * * @param int $offset This will be sent starting with 0, increasing to get all files stored by this extension * @param int $storageConfiguration New storage configuration ID * @param int|NULL $oldConfiguration Old storage configuration ID * @throws \Underflowexception When file record doesn't exist. Indicating there are no more files to move * @return void */ public function move( $offset, $storageConfiguration, $oldConfiguration=NULL ) { $thing = \IPS\Db::i()->select( '*', 'my_table', 'image IS NOT NULL', 'id', array( $offset, 1 ) )->first(); \IPS\Db::i()->update( 'my_table', array( 'image' => (string) \IPS\File::get( $oldConfiguration ?: 'app_Key', $thing['image'] )->move( $storageConfiguration ) ), array( 'id=?', $thing['id'] ) ); } /** * Check if a file is valid * * @param \IPS\Http\Url $file The file to check * @return bool */ public function isValidFile( $file ) { try { \IPS\Db::i()->select( 'id', 'my_table', array( 'image=?', $file ) )->first(); return TRUE; } catch ( \UnderflowException $e ) { return FALSE; } } /** * Delete all stored files * * @return void */ public function delete() { foreach( \IPS\Db::i()->select( '*', 'my_table', "image IS NOT NULL" ) as $forum ) { try { \IPS\File::get( 'app_Key', $forum['image'] )->delete(); } catch( \Exception $e ){} } } } However the appropriate code to use will depend on the nature of how the content created by your files are stored.
    Creating the element
    When creating the element you must provide an $options parameter specifying the extension you just created.  For example, the code to create your element will look something like:
    $form->add( new \IPS\Helpers\Form\Upload( 'my_upload_field', NULL, TRUE, array( 'storageExtension' => 'app_Key' ) ) ); Additional options are available to allow multiple file uploads, to restrict the allowed extensions, the maximum file size and more. See the source code for all the available options.
    Handling Submissions
    The value returned will be an object of \IPS\File (or an array of \IPS\File objects if the field allows multiple file uploads). You do not need to do anything with the file itself, as it has already been stored according to the administrators preference. You do however, have to save the URL to it (which you can get by casting the \IPS\File object to a string) as that is what you will need to get and manipulate the file later, and use within the extension you created earlier. For example, your code might look like:
    $form = new \IPS\Helpers\Form; $form->add( new \IPS\Helpers\Form\Upload( 'my_upload_field', NULL, TRUE, array( 'storageExtension' => 'app_Key' ) ) ); if ( $values = $form->values() ) { \IPS\Db::i()->insert( 'my_table', array( 'image' => (string) $values['my_upload_field'] ) ); }  
    Manipulating the file later
    To get the \IPS\File object back, you simply call:
    $file = \IPS\File::get( 'app_Key', $url ); The first parameter being your extension, and the second being the URL you obtained when saving the form. You can then use this object to get the contents of the file, delete it, etc. See the phpDocs in \IPS\File for more information on what you can do with files.
    If it is an image file, you can also create an \IPS\Image object to resize, add a watermark, etc. To do this you call:
    $image = \IPS\Image::create( $file->contents() );  See the phpDocs in \IPS\Image for more information on what you can do with images. 
    • 0 replies
  9. Mark added a article in Development   

    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 customise the editor to make certain features available in some areas and not others - due to these concerns, using an Editor is slightly more complicated than most other form types.
    The 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 identfies 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 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: things which always exist (like settings) and content which is created and deleted. Attachments are handled differently for each:
    Things 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 $id1 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.
    Things which are 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 used for the autoSaveKey - 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 value
    The value is automatically parsed including replacing profanity and other settings configured by the administrator, and sanitised of any security concerns. You can safely store the display value and display it without any further parsing.
    When sending any variable to a template, the system will automatically escape it to prevent XSS vulnerabilities. Since editor content is allowed to contain HTML, and has been sanitised, you can override this escaping by doing:
    • 0 replies
  10. Mark added a article in Development   

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

    Template Tags
    Template tags are short tags that can be used in templates. Some can also be used in CSS files.
    Template tags look like this:
    {tag="value"} Some template tags accept options:
    {tag="value" option="option_value" other_option="other_value"} Useful links for further reading :
    Template Logic  
    Language Strings
    {lang="key"} Returns the language string for the language being used by the member that is currently logged in.
    Available options:
    sprintf  can be used to pass values to be replaced using sprint . The values will be escaped to prevent XSS; if this is undesired,  htmlsprintf  does the same without the escaping (be careful not to introduce XSS vulnerabilities in this way). pluralize  can be used to pass values if the string uses pluralizing logic . wordbreak  inserts <wbr> tags throughout the returned value and should be used on user-supplied values which might cause the layout to break  
    Dates & Times
    {date="699753360"} Returns a value for displaying a date. By default is automatically relative (e.g. may display "5 minutes ago"), uses HTML5 <time> tags, will automatically condense on smaller displays and is locale-aware. It can be passed either a unix timestamp or an instance of \IPS\DateTime.
    Available options:
    norelative  just displays the date and time in a locale-aware format, without using <time> tags or the other features. You only need to specify norelative="true". dateonly  works like norelative but displays just the date and not the time.  
    Member Data
    {member="name"} Returns the value of the specified parameter for the currently logged in member. In this example, returns the value of \IPS\Member::loggedIn()->name.
    Available options:
    group  will return the value from the member's group information i.e. \IPS\Member::loggedIn()->group[ $key ] - you only need to specify group="true". id  will make it use the member with the specified ID rather than the currently logged in member. raw  will prevent the returned value from being escaped to prevent XSS vulnerabilities. You only need to specify raw="true". Be careful when doing this that you do not introduce XSS vulnerabilities.  
    {setting="board_name"} Returns the value of the setting with the specified key.
    Theme Settings
    {theme="selected"} Returns the value of the theme setting with the specified key.
    URLs  (can be used in CSS)
    {url="app=core&module=system&controller=login"} Returns a URL.
    Available options:
    seoTemplate should be the FURL template to use. seoTitle should be the title to use in the friendly URL, if applicable. If there needs to be more than one,  seoTitle s can be provided with comma-delimited values. csrf  if provided, will add the CSRF key to the URL. You only need to specify csrf="true". fragment  if provided, will add a #fragment to the end of the URL with the provided value. noprotocol  if provided, will return the URL without a protocol. You only need to specify noprotocol="true". plain  will provide the URL in plaintext rather than HTML-entity encoded. If usually only used by plaintext email templates. You only need to specify plain="true".  
    {number="1000"} Returns the number formatted according to the user's locale. For example "1,000".
    File Size
    {filesize="1000000"} Returns a human-readable representation of a number of bytes. For example, "1MB".
    Available options:
    decimal  will make it use decimal (1000 bytes = 1kB) rather than binary (1024 bytes = 1kB).  
    CSS Prefixes  (can be used in CSS)
    {prefix="transition" value="0.1s all linear"} Returns a block of CSS code with all the browser-specific prefixes ("-webkit-", "-moz-", "-ms-" and "-o-"). The option is required.
    {template="userPhoto" group="global" app="core" params="$entry->author(), 'small'"} Includes another template. All options are required.
    Images  (can be used in CSS)
    {image="image.png" app="core" location="front"} Returns the URL to the image. All options are required.
    Expression  (can be used in CSS)
    {expression="1+1"} Executes the provided value as PHP and displays the return value.
    Available options:
    raw  will prevent the returned value from being escaped to prevent XSS vulnerabilities. You only need to specify raw="true". Be careful when doing this that you do not introduce XSS vulnerabilities.  
    Wordbreak  (can be used in CSS)
    {wordbreak="This is some very long text..."} Returns the value with <wbr> tags inserted throughout it so as to avoid breaking the layout for user-provided content which may be very long.
    Truncate  (can be used in CSS)
    {truncate="Some very long text..." length="10"} Returns the value truncated to the specified length. Option is required.
    Additional available options:
    start  can be provided as an offset. append  can be provided to provide an alternate value to use instead of &hellip; to add to the end of truncated text.  
    {advertisement="location"} Displays whatever advertisement is configured to display at the specified location.
    • 0 replies
  12. Mark added a article in Development   

    The Autoloader
    Classes in IPS Community Suite 4 are "autoloaded". This means you never have to include or require an IPS4 source file.
    For reference, the autoload method is \IPS\IPS::autoloader() which is located in the init.php file of the root directory.
    Locating Classes
    Classes must be located in the correct location and be named properly so the autoloader can find them. There are generally three locations:
    Framework classes
    Classname structure: \IPS\Namespace\Class
    Location on disk: system/Namespace/Class.php
      Application classes
    Classname structure: \IPS\app\Namespace\Class  (note that the application key is lowercase, but the parts after are PascalCase)
    Location on disk: applications/app/system/Namespace/Class.php
      Application extensions and modules
    Classname structure: \IPS\app\modules\front\module\controller  (note all the parts are lowercase)
    Location on disk: applications/app/modules/front/module/controller.php For Framework classes and Application classes, the final file must always be in within a folder (not loose in the system directory). If only one-level deep, the system will look for a file in a directory of the same name. For example \IPS\Member is located in system/Member/Member.php while \IPS\Member\Group is located in system/Member/Group.php
    Monkey Patching
    When declared, classes always start with an underscore. For example, throughout the IPS Community Suite, you call \IPS\Member, however, if you look in the source file you will see it declared like this:
    namespace IPS; class _Member { ... This is a technicality of a feature called  monkey patching which allows third party developers to overload any class without any consideration throughout the code and in a way where hooks do not conflict with one another.
    In this example, the system will execute, right after autoloading the Member.php file, code like this:
    namespace IPS; class Member extends \IPS\_Member { }  
    If a third party developer wants to overload \IPS\Member, the system will inject this in between, so you end up with a structure like so:
    \IPS\Member extends hook1 hook1 extends \IPS\_Member   
    Or if two hooks wanted to overload \IPS\Member:
    \IPS\Member extends hook1 hook1 extends hook2 hook2 extends \IPS\_Member This means that the framework, and any third party code only ever has to call \IPS\Member, and the system will not only automatically autoload the source, but create a chain of any hooks wanting to overload the class.
    If the mechanics of this are confusing (it is an unusual practice) - it is not essential to thoroughly understand. You only need to know that classes must be prefixed with an underscore when declared, but that underscore is never used when actually calling the class.
    It also means that rather than calling "self::" within the class, you should call "static::" so you are calling the overloaded class, not the original.
    Third Party Libraries
    If you want to use a third-party PHP library they will of course need to be included manually and cannot be monkey patched.
    If the library follows the PSR-0  standard for naming, you can add it to \IPS\IPS::$PSR0Namespaces like so, and the autoloader will then autoload it:
    \IPS\IPS::$PSR0Namespaces['Libary'] = \IPS\ROOT_PATH . '/applications/app/system/3rd_party/Library'; Otherwise, you will need to include the source files manually.
    • 0 replies
  13. Mark added a article in Development   

    Right-to-Left Language Support
    In the most scripts, including the Latin script used in most Western and Central European languages, writing starts from the left side of the page and continues to the right. This is however, always the case. For example, in the Arabic script (used for the Arabic, Persian and many other languages) and the Hebrew script start at the right side of the page and continue to the left.

    This influences many design decisions. For example, a web page will usually have its logo in the top-left, as this is the first place users usually look. However, speakers of languages which use right-to-left scripts will expect the logo in the top-right. Similarly, the "close" button usually found in the top-right of popups will need to be on the top-left, and progress bars will need to fill the other way around. In fact, when using a right-to-left script, the entire page should be like a mirror image of how it usually is.

    IPS Community Suite fully supports right-to-left scripts. As a designer creating themes, or a developer creating applications and plugins, it is important that you consider right-to-left scripts and maintain full support for them. Fortunately, doing so is very easy. There are just 3 considerations you need to make.

    Step One: Add RTL-Specific Declaration Blocks to your CSS

    In your CSS, almost everywhere where you use the words "left" or "right" or make a declaration which affects either the left or right (such as "margin", "padding" or "border") you must change your CSS to reverse this for RTL scripts. IPS Community Suite will always have a "dir" attribute on the HTML tag specifying "ltr" or "rtl" so it is easy to add these declarations.

    For example, if you have this in your CSS:
    .myClass { color: red; font-size: 2em; float: left; padding: 0px 10px 0px 50px; margin-right: 5px; } You will need to change this to:
    .myClass { color: red; font-size: 2em; } html[dir="ltr"] .myClass { float: left; padding: 0px 10px 0px 50px; margin-right: 5px; } html[dir="rtl"] .myClass { float: right; padding: 0px 50px 0px 10px; margin-left: 5px; } The first block is making declarations which apply regardless of the direction. The next two blocks are declaring styles which will only apply if the text direction is left-to-right or right-to-left, respectively.

    Step Two: Flip Images

    If you use any images which indicate a direction, these will need to be flipped for right-to-left scripts. For example, if you have a right arrow, this will probably need to be changed to a left arrow, otherwise it will be pointing in the wrong direction.

    If you use FontAwesome icons in your HTML, these are usually automatically flipped. However, if you declare them in CSS, you may need to add conditional statements again. You will also need to manually flip any other images you are using.

    For example, if you have this CSS declaration:
    .nextLink:after { font-family: 'FontAwesome'; content: '\\f105'; } You will need to change this to:
    .nextLink:after { font-family: 'FontAwesome'; } html[dir="ltr"] .nextLink:after { content: '\\f105'; } html[dir="rtl"] .nextLink:after { content: '\\f104'; } Or if you have:
    .nextLink { background-image: url('right_arrow.png'); } You will need to change this to:
    html[dir="ltr"] .nextLink { background-image: url('right_arrow.png'); } html[dir="rtl"] .nextLink { background-image: url('left_arrow.png'); } Step Three: Consider JavaScript

    This usually will not require any action. However, if you have any custom JavaScript which adds user interaction, consider if any changes need to be made. For example, if you have a menu which opens from the left, it may need to open from the right. If you are only using the UI widgets already in the IPS Community Suite, these already make all such considerations so no action will be necessary.
    • 0 replies
  14. Mark added a article in Development   

    Template Logic
    This article lists the describes the template logic syntax that can be used in templates.

    Common Control Structures

    For most control structures, the syntax is similar to  PHP's alternative syntax for control structures  with the PHP statements contained within double curly braces. In most cases though, brackets, colons and semi-colons are not required.

    {{if $var}} HTML to display {{endif}} {{if $foo}} HTML to display {{elseif $bar}} HTML to display {{elseif $baz}} HTML to display {{else}} HTML to display {{endif}} {{foreach $foo as $bar}} HTML to display {{endforeach}} Shortcuts

    A number of shortcuts are available for use within control structures. For example rather than doing:
    {{if \IPS\Member::loggedIn()->member_id}} HTML to display {{endif}} You can do:
    {{if member.member_id}} HTML to display {{endif}} The available shortcuts are:
    request. var  is equivalent too  \IPS\Request::i()->var member. var  is equivalent too  \IPS\Member::loggedIn()->var settings. var  is equivalent too  \IPS\Settings::i()->var theme. var  is equivalent too  \IPS\Theme::i()->settings['var'] cookie. var  is equivalent too  \IPS\Request::i()->cookie['var']
    Variables can be used using normal curly braces:
    {$foo} To prevent XSS, variables used in this way are automatically escaped. To prevent the escaping, do:
    {$foo|raw} Be extremely careful when doing this not to introduce XSS vulnerabilities.
    The escaping is by default performed in a way that entities are not double-encoded. To change it to double-encode do:
    Template Tags

    Almost anywhere were template logic can be used,  template tags  can also be used for formatting numbers, dates, getting language strings, etc.

    Raw PHP

    You can execute raw PHP by enclosing it in double-curly braces. Beware of introducing XSS security vulnerabilities if doing this.
    {{$foo = array();}} Display template contents

    When in  developer mode  there is a special tag which can be inserted anywhere in a template which will cause it's compiled content to be displayed. This can be useful for debugging:
    • 0 replies
  15. Mark added a article in Development   

    Plugins: An Example
    This guide will demonstrate step-by-step creating how to create simple plugin making use of most of the features provided. For this guide, you'll be shown how to create a plugin which displays a message at the top of every page on the community.

    Useful links for further reading:
    Plugins - Details information about Plugins and the features provided. Step 1: Creating the Plugin

    To begin, you'll need to have a test installation  in developer mode . Once developer mode is enabled, a "Create Plugin" button will appear in the Admin CP under System --> Site Features --> Plugin. Use this tool to create your plugin, after which you'll be taken into the Plugin Developer Center.

    Step 2: Creating the Theme Hook

    The easiest way to display a message at the top of every page is to create a theme hook. Theme hooks allow you to modify the content of a template. The template we'll be modifying is "globalTemplate" which is in the "global" template in the "front" location in the "core" application.

    Under the "Hooks" tab, create a new hook and choose "core --> front: global" as the template group. Once it has been created, click the edit button, then choose "globalTemplate" from the menu on the left.

    Choose "Select Element" to bring up the contents of the template and choose the point you want to hook on - a good place is to select  <div id="ipsLayout_mainArea">  and then for the content position choose "Insert content inside the chosen element(s), at the start."

    For now, specify the contents manually:
    <div class="ipsMessage ipsMessage_information">This is the global message.</div> After saving you can go to the homepage and you will immediately see the message you've just created. Congratulations, you've just created a simple Plugin!

    The remaining steps will show how to expand this functionality.

    Step 3: Using Templates

    It's good practice to keep all HTML in templates. While the current approach works fine, the HTML content is buried within the code of your plugin, so if someone installing the plugin wanted to modify it in some way (perhaps change the CSS classes on it), it would be difficult to do so.

    Fortunately, creating a HTML template is really easy. If you look into the directory on your computer/server where IPS Community Suite is installed, you'll notice a "plugins" directory - inside this you'll find a directory has been created for your plugin with the name you specified in Step 1. Inside this is a folder navigate to dev/html. Within this folder, files you create will become available as a template.

    Create a file called "globalMessage.phtml" - and set the following as it's contents:
    <ips:template parameters="" /> <div class="ipsMessage ipsMessage_information"> Now using a template! </div> The first line is just to name any parameters that will be passed into the template - which is not needed in this case.

    Once the file has been created, edit your theme hook and change the contents it inserts to use this template by using this code:
    {template="globalMessage" group="plugins" location="global" app="core"} This is a  template tag  which pulls in the contents of a template. All templates created by plugins are created in the "plugins" group in the "global" location in the "core" application.

    Once this is done, you should see the message has changed to "Now using a template!"

    As a side-note, if you wanted to add CSS code, you can just add css files into the dev/css folder and they will automatically be included.

    Step 4: Settings & Language Strings

    Now you have a global message - but currently there's no way to customise what it says. It would be handy if the plugin had a simple setting in the Admin CP that the admin could use to change the contents.

    To create a setting - go to the "Settings" tab in the developer center for your plugin and add a setting - for the key, use "globalMessage_content", and set the default value to whatever you like. It's important to make sure that your plugin starts working straight after it installs so the admin knows it's working properly, so don't leave the default value blank.

    Creating the setting here allocates space for your setting in the database, but you still need to create a form where the admin can edit it. To do this, again look in the directory for your plugin on the filesystem - you'll see a file called "settings.rename.php", first rename this to "settings.php" then open it up. It already contains example code to get you going. Change the first line of code (the  $form->add(...)  call) to this:
    $form->add( new \IPS\Helpers\Form\Editor( 'globalMessage_content', \IPS\Settings::i()->globalMessage_content, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => 'globalMessage_content' ) ) ); This code is using the  form helper .

    Now when you go to the Plugins area of the Admin CP, you'll see a new "edit" button next to your plugin, when clicked, this brings up a form with a place where users can fill in a message.

    There is however, a problem with your form. The label for the setting just says "globalMessage_content" - obviously this needs to be changed to something more useful, for which you'll need a language string. To create one, look in the directory for your plugin again and open up the dev/lang.php file. It will contain just an empty array - add an element to this array like so:
    $lang = array( 'globalMessage_content' => "Message", ); The label will now say "Message".

    Finally, you need to actually make the user-defined message show. To do this, open up the globalMessage.phtml file you created in step 3 and change it's contents to:
    <ips:template parameters="" /> {{if settings.globalMessage_content}} <div class="ipsMessage ipsMessage_information"> {setting="globalMessage_content"} </div> {{endif}} That adds some  template logic  which detects if there is a value for our setting and only displays the message if there is, and another  template tag  which gets the value of our setting.

    If you wanted to add your own CSS, you can create CSS files in the dev/css folder of your plugin directory - any files you create there will be included automatically.

    Step 5: Making changes to the database

    If we wanted to take this plugin even further, you could add a "close" button to the message allowing users to dismiss the message once they have read it. Whether or not any given user has dismissed the message is information can be stored in the database.

    Open up the dev/setup folder of your plugin directory. In here you'll find a file called "install.php" - this file is ran when your plugin is installed. If you need to make subsequent database changes in future versions of your plugin, you can create new versions in the developer center, and a file will be created for each version you create - you need to make sure you add whatever changes you need both to the install.php file and the appropriate upgrade file.

    Open up install.php and add this code inside the step1 method:
    \IPS\Db::i()->addColumn( 'core_members', array( 'name' => 'globalMessage_dismissed', 'type' => 'BIT', 'length' => 1, 'null' => FALSE, 'default' => 0, 'comment' => 'If 1, the user has dismissed the global message' ) ); return TRUE; This code adds a new column to the core_members table in the database, which is the table which contains information on all the members. The column created is a BIT(1) column (which means it can store only 1 or 0) - 0 will indicate the user has not dismissed the message (so it should show) and 1 will indicate they have - the default value is set to 0.

    Though this code will make sure when your plugin gets installed the column will be added, you'll need to run a query on your local database so that  you  have it during development. Run this SQL query using whatever your preferred method is for managing your database:
    ALTER TABLE core_members ADD COLUMN globalMessage_dismissed BIT(1) NOT NULL DEFAULT 0 COMMENT 'If 1, the user has dismissed the global message'; After the user dismisses the message, their preference will need to be reset if the admin changes the contents of the message. To handle this, add this line to your settings.php file in your plugin directory, just after the  $form->saveAsSettings();  call.
    \IPS\Db::i()->update( 'core_members', array( 'globalMessage_dismissed' => 0 ) ); Step 6: Creating a Code Hook

    Now you've allocated space in the database where the flag will be stored, you need to write code to actually set that value, and link it up with the template.

    You'll need to add a method to a controller which will handle the user's click. A generic-use controller intended for this sort of thing is available -  \IPS\core\modules\front\system\plugins  - though theoretically, you could add the method to any controller.

    In the developer center, create a code hook on that class and open it up (either in the Admin CP or by opening the file that has been created in the hooks directory of your plugin directory) and add the following code inside the class:
    public function dismissGlobalMessage() { \IPS\Session::i()->csrfCheck(); if ( \IPS\Member::loggedIn()->member_id ) { \IPS\Member::loggedIn()->globalMessage_dismissed = TRUE; \IPS\Member::loggedIn()->save(); } else { \IPS\Request::i()->setCookie( 'globalMessage_dismissed', TRUE ); } \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); } It's important to understand thoroughly what this function is doing:
    It first performs a  CSRF  check. Because this is a controller method, it is executed automatically on accessing the appropriate URL. Because it does something which affects the user (it modifies a preference) it is essential that it have a CSRF check. If the check fails, execution will be halted automatically. It checks if the current user is a registered member.  \IPS\Member::loggedIn()  returns an  \IPS\Member  object for the current user - if the user is a guest (not logged in) the  member_id  property will be 0. If the user is logged in, it sets the  globalMessage_dismissed  property to  TRUE  and saves the member. Since \IPS\Member is an  Active Record , this will edit the column you created in step 5 for the appropriate row in the database. If the user is not logged in, it sets a cookie so that even users who are not logged in can dismiss the message. It then redirects the user back to the page they were viewing, or if the server cannot provide the HTTP Referer, the home page. Now that the action has been created, you need to adjust the template to honour the preference, and add a button that links to the dismiss action.

    Modify the template you created in step 3 to:
    <ips:template parameters="" /> {{if settings.globalMessage_content and !member.globalMessage_dismissed and !cookie.globalMessage_dismissed}} <div class="ipsMessage ipsMessage_information"> <a href="{url="app=core&module=system&section=plugins&do=dismissGlobalMessage" csrf="1"}" class="ipsMessage_code ipsType_blendlinks"><i class="icon-remove"></i></a></span> {setting="globalMessage_content"} </div> {{endif}} Step 7: JavaScript

    You now have a completely functional plugin which displays a message to all users that they can dismiss. For a little bit of added flair, you can make it so when the user dismisses the message, rather than reloading the page, it performs the action with an AJAX request and then the message fades out. It's really important that you only think about JavaScript enhancements  after  the core functionality has been written, so that users who don't have JavaScript enabled can still use your plugin, and search engines can access any content your plugin provides.

    To do this, you need to create a JavaScript controller. In the dev/js directory of your plugin directory, create a file named "globalMessageDismiss.js" with the following contents:
    ;( function($, _, undefined){ "use strict"; ips.controller.register('plugins.globalMessageDismiss', { initialize: function () { this.on( document, 'click', '[data-action="dismiss"]', this.dismiss ); }, dismiss: function (e) { e.preventDefault(); var url = $( e.currentTarget ).attr('href'); var message = $(this.scope); ips.getAjax()(url).done(function(){ ips.utils.anim.go( 'fadeOut', message ); }).fail(function(){ window.location = url; }); } }); }(jQuery, _)); To explain what this is doing:
    Everything other than the contents of the initialize and dismiss functions is required code for JavaScript controllers. The  ips.controller.register  line specifies the name of the controller. When an element with a JavaScript controller attached to it is loaded (you'll attach it to the global message in a moment) the initialize function is ran - the best practice is to only set up event handlers here and handle events in other functions. An event is being set up here to fire when any elements matching the selector  [data-action="dismiss"]  (you'll add that attribute to the close button in a moment) are clicked, and it calls the dismiss function when this happens. This dismiss function first prevents the event from performing it's default action (as a click will of course take the user to the target URL), then sets the variables needed (the URL that the button points to and the message box. It then sends an AJAX request to the URL that the button points to - if it succeeds, it fades out the box, and if it fails, it redirects the user to it just as if there was no JavaScript functionality. To make it actually work, you need to specify that you want your message to use this controller. In your template, add  data-controller="plugins.globalMessageDismiss"  to the  div  element, and  data-action="dismiss"  to the  a  element.

    For completeness, we should also adjust our action a little so that is aware of AJAX requests - while this is not strictly necessary, if it is not done, it is possible that if the redirect has to redirect to a screen which will display an error message, that the AJAX request will see  this  response and assume the request failed. Open up the action.php file you created in step 6 and change the redirect line to:
    if ( \IPS\Request::i()->isAjax() ) { \IPS\Output::i()->sendOutput( NULL, 200 ); } else { \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); } Once this is done, closing your message will now fade out without a page reload.

    Step 8: Downloading

    Congratulations, you've just created your first plugin! You can download so that you can install it on other sites or distribute it through the IPS Marketplace from the developer center.

    For reference, a downloaded version of the hook, as well as a zip of the plugin directory for development are attached:

      Global Message.xml
    • 0 replies

About Me

Contributes To

  1. News and Announcements    By IPS

    • 500
    • 13820
    • 4815912