Jump to content

bfarber

Clients
  • Posts

    163,911
  • Joined

  • Days Won

    346

 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 bfarber

  1. Google disagrees about allowing search result pages to be indexed https://www.mattcutts.com/blog/search-results-in-search-results/
  2. "No man is an island" wrote John Donne. He wrote that a good 200 years before computers were invented, but it rings true for any well written framework like Invision Community. The included REST API allows developers to fetch data from Invision Community and also allows data to be added. This data can be used to power widgets on your website, or to be used within other applications you are already using in a very simple way. Several enhancements have been made to the REST API for Invision Community 4.3 that we wanted to let you know about. These changes are developer-oriented, so if you do not use the REST API with your community please feel free to skip this update. If you would like to learn more about the REST API available with Invision Community, please see our REST documentation. Search capabilities As previously noted, you can now perform searches through the REST API. You can perform searches based on keywords, tags, or both, and you can limit and filter results with parameters similar to when you perform a regular search on the site (e.g. to specific containers, returning only results over a set number of comments, or searching within clubs). Permission awareness Several REST API endpoints are now permission-aware when combined with Oauth functionality built into Invision Community 4.3. This means that many REST API endpoints can be called using a specific user's access token, and only results that the specific user would normally be able to see will be returned (and/or they will only be able to submit to areas they normally have permission to). Ability to search members While an endpoint has always been available to retrieve (and add/edit/delete) members, the ability to search for members has now been implemented. You can search by name, email address, and (one or more) group(s), and a paginated response will be returned. Private conversations You can now start a new private conversation, reply to an existing private conversation, and delete a private conversation through the REST API. Other REST API changes You can now specify member's secondary groups when adding or updating a member through the REST API. You can specify the member's registration IP address through the REST API when adding or updating a member. You can now specify other member properties not directly exposed through the REST API when adding or updating a member by setting the rawProperties input field. You can now specify other member properties to retrieve through the REST API through the otherFields request parameter. The REST API now better logs changes to member accounts (so you will be able to more easily identify how a user's name, email address, password, etc. has changed when looking at the member history). You can now retrieve all content a member is following through the REST API, as well as follow a new container/content item, and delete an existing follow. You can now validate an account through the REST API You can now specify a 'perPage' parameter for paginated responses to control how many items are returned per page. Most of these changes were directly culled from client feedback and implemented per specific requests. If there are other REST API changes you would like to see implemented please don't hesitate to leave your feedback!
  3. "their" in English is taken to mean "belonging to them". As we do not know the gender of the user, we can't say "his" (as in belonging to him) or "her" (belonging to her), so "their" is an acceptable gender-neutral solution. But, as you use a translation that adjusts the verbiage anyways, I'm not sure why this matters.
  4. The system is more geared towards digital goods being distributed, or site-sponsored physical goods. There are third party addons on the marketplace that facilitate user-to-user sales of items (i.e. "classifieds"), and we have some ideas rolling around internally to open up Commerce further.
  5. With Commerce and Downloads, you can allow users to sell their products and services, and optionally retain a percentage of any commission for the site. We do that here in our marketplace using stock functionality.
  6. "The world’s most valuable resource is no longer oil, but data", the Economist wrote recently. Invision Community software stores a lot of important data that can be leveraged to analyze and improve upon the traffic and interactions with your site. While there are some various statistics tools in the AdminCP already, we spent some time with 4.3 enhancing and improving upon our existing reporting tools, as well as adding some new analytics tools you may find useful. Chart Filters Beginning with 4.3, any dynamically-generated charts in the AdminCP that support filtering will allow you to save those filter combinations for easier access in the future. When you open the Filters menu and toggle any individual filters, the chart will no longer immediately reload until you click out of the menu, and 'All' and 'None' quick links have been added to the filters menu to allow you to quickly toggle all filters on or off. Here is the 'Sales' chart for Commerce, for example. You will see that the interface is now tabbed. Commerce's Sales chart After opening the 'Filters' menu, selecting all of my products named 'test', and saving this filter combination as a new chart, I can quickly come back to this chart in the future. Specific filter configurations allow you to run reports easily Note that each user can save their own chart filter configurations independent of other users. Top income by customer Speaking of Commerce, we have also added a new chart to the 'Income' page, allowing you to view reports of your top customers. As with other dynamic charts, you can save filter configurations here for easy future access, and you can view the results as a table to get a raw list of your top customers' purchases. Further, we have tidied up the table views for the other existing tabs on this page. Looks like brandon is my top customer Reaction statistics We have introduced several statistic pages to expose information about the Reactions/Reputation system and how your users are interacting with it. For instance, you can now view information about usage of each of the reactions set up on your site. Yes, I'm definitely confused a lot You can also see which users give and receive the most reputation (which is the sum of their reaction points, keeping in mind that negative reactions can reduce a user's total reputation score), you can see which content on your community has the most reputation (which might prompt you to promote it to the 'Our Picks' page, promote it to social media, or otherwise continue to encourage interaction with the content), and you can see which applications reactions are given in the most. This could allow you, for instance, to focus more efforts in areas of your site to drive more activity, or to foster activity in areas you did not realize were as active as they are. Some areas of the community aren't as active as they could be Additionally, when viewing user profiles on the front end you can now see a breakdown of which reactions each user has given and received when you click the "See reputation activity" link in the left hand column. Apparently I'm not so much confused, as I am confusing Tag Usage Another useful statistic introduced with 4.3 is the ability to review tag usage on your community. As with other dynamic charts, you can filter however you like and save those filter configurations for easy future access. Not all tags are equal Trend charts for topics and posts When viewing the New Topics and New Posts charts, there are now tabs for "New Topics by Forum" and "New Posts by Forum", allowing you to see which of your forums are the most active. Additionally, you will see a trend line drawn on the chart to show you the trend (e.g. whether activity is increasing or decreasing). You can also filter which forums you wish to review, so you can compare your most active forums, the forums that are most important to your site, or the forums that need the most attention/may not be relevant, for instance. Viewing new topics by forum New posts by forum, but viewing only a subset of my most important forums Other Improvements Some other miscellaneous improvements have been introduced as well, which you may be interested in: When viewing Member Activity reports, you can now filter by group. We have also added the content count column to the table so you can quickly sort by top posters if this is relevant to the report you are running. Device usage is now also tracked (mobile, desktop, etc.) and can be viewed on a new Device Usage page. Developers: Dynamic charts now support database joins
  7. A picture says a thousand words, they say. If getting those pictures online is troublesome, some of those words might be a little choice. Gallery has been an integral part of our community suite for just about as long as T1 Tech Mark Higgins can remember (and he has many years of memories). It has seen many interfaces changes as the years have rolled by. The most recent version received a fair amount to feedback on usability. We've listened. We've re-engineered most of Gallery's key interfaces to make uploading new images to your community frictionless. Lets take a look through the major changes. Improved submission process Submitting images has to be simple or else users will give up and your gallery will be underutilized. We have spent a lot of time simplifying and speeding up the submissions process for your users. The first thing that will be noticed is that the submission process is not presented as a wizard anymore, and the choice to submit to a category or album has been significantly cleaned up and simplified. Choosing a container Here, I have chosen the category I wish to submit to, so now I am asked if I want to submit directly to the category, if I want to create a new album, or if I want to submit to an existing album. Choosing one of those last two options will load the appropriate forms to create an album or select an existing album, respectively. Afterwards, the modal expands to full screen and you will naturally select your images next, and there's a lot to talk about here. Overhauled submission interface First and foremost, the interface has changed significantly to both simplify the UI and to make actually using the interface easier. When you click on an image, the form is loaded to the right immediately without an AJAX request needed to fetch the form. In addition to quickly setting the credit and copyright information for all images at once, you can now set the tags for all images quickly and easily without having to edit each image individually. Images support drag n drop reordering in the uploader here, which means that you can drag n drop images to different positions to control their order. Many users previously would name images "Image 1", "Image 2", and so on, and then set their albums to order images by name in order to control the order the images were displayed in. This is no longer necessary now that you can manually reposition the images. The default description editor is a pared down textarea box, but you can still use the rich text editor if you wish. The ability to enable maps for geo-encoded images and to upload thumbnails for videos is still supported as well, and those options will show up when appropriate in the right hand panel. The 100 image per submission limit has also been lifted. You can now upload many more images in one go with no hard limit imposed. Upon clicking submit images, you will see the typical multiredirector to store all of your images, however you will notice that it processes much faster than it did in 4.2 and below. Better submission control Administrators can now configure categories such that can accept only images, only albums, or both. This means you can now create categories that cannot be submitted to directly, and you can create categories that albums cannot be used with. This is a feature that has been oft-requested since the release of 4.0, and we are happy to report that it will be available in our next release. Additionally, album creators (if permitted) can also now create shared albums. When you create a new album, you can now specify (under the Privacy menu) who can submit to the album, with your available options being: Only me Anyone Only the users I specify Only the groups I specify Prior to 4.3, albums have always been owned by one user and only that user could submit to them. Invision Community 4.3 will open up albums so that anyone can submit to them, dependent upon the album creator's preferences and needs. The choice is yours as to who can submit to your albums New image navigation Another major change with Gallery 4.3 is that clicking an image now launches that image in a lightbox to view it and interact with it. This lightbox is context-aware, allowing you to visit the next and previous images in the listing, whether that is a category or album listing, or the featured images or new images listings on the Gallery homepage, for example. The new image lightbox Firstly, I will note that you are seeing the image here with my mouse cursor over the image area, exposing the title, tags, and some various buttons. When you mouse away from the image those overlays fade away to highlight the image itself better. As you can see, you can navigate left and right here to view the next and previous image in this context, and you can otherwise interact with the image as you would have if you had visited the older-style image view page (including the ability to rate, review and comment). The new Gallery release will introduce a new advertisement location in the right hand column to allow you to show advertisements, even in the lightbox. If you follow a link to a full image view page, the lightbox will automatically launch when the page loads, still allowing you to interact in a familiar manner. Additionally, if you move through enough images in the lightbox to reach a new page (for example, if you click on the last image in the album listing and then click on the next image button), the listing itself behind the lightbox will update for easier usability if the user closes the lightbox. One final thing to note is that the interface has been made more mobile friendly, particularly through the introduction of swiping support. You can swipe left and right in the lightbox, and in image carousels, to see the next and previous images. Notable performance improvements As we mentioned at the beginning, we recognize there is a balance between performance, usability, and attractiveness, particularly with regards to an image Gallery. For that reason, we have made Gallery's performance a major focus in 4.3, and have implemented some changes that bring with them a noticeable performance improvement. Firstly, we have adjusted the software to only store two copies of an image (in addition to the original), instead of four. In previous versions, we stored a thumbnail, a small copy, a medium copy and a large copy of an image, all of which arbitrarily sized and designed to best meet our layout needs without showing an image too large or too small in a given space. We have simplified this vastly by storing a slightly larger "small" image, and storing a large copy. Diskspace usage is reduced dramatically as a result, and bandwidth usage is actually lowered as well since only two copies of an image need to be delivered to the browser instead of four. Next, we have implemented prefetching of the 'next' and 'previous' pages when you launch the lightbox image view. This means that when a user navigates to the next image in the lightbox, it loads immediately instead of waiting for the content to be fetched from the server. From a UX perspective, this provides a much snappier and responsive interface, making users more apt to interact with the site. We have additionally sped up the submission process as previously mentioned. The order of execution for certain events that must happen during submission has been moved around a bit, resulting in a faster experience for the end user actually submitting the images. Because we know the details matter, we have implemented other smaller improvements as well. For example, the link to rebuild images in the AdminCP previously resulted in a redirect process that rebuilt the images while you waited, but now a background task is launched so that you can continue with what you were doing while the images get rebuilt in the background. From start to finish, the Gallery UI and UX has been touched on and improved, and we hope you enjoy these improvements when you start using the new version.
  8. What other custom fields might you develop? Images in Gallery already support overlaid "notes" which you could use to tag users, although this won't act as a 'mention' and notify the user or show that any specific users have been tagged. Tagging users is a bit specific and would probably be handled in a better manner than a basic custom field, so I'm just trying to see what other use cases you might have.
  9. In *most* cases, if you get a generic error message that relates to a specific issue, we need to improve that error at the software level rather than have some sort of public list of every error code ever.
  10. That error code occurs while importing a theme when the file that was uploaded no longer exists, or the md5 checksum of the upload does not match the checksum of the file during the import. This shouldn't cause a 404 error on the front end of your site. If you are experiencing trouble I recommend submitting a ticket so we can take a look.
  11. I'm afraid I can't speak as to whether this will be changed or not, but it's still something on our internal discussion tracker. Sometimes it's a little harder to get things that have no real-world impact (but help third party developers) to the top of the list as you can imagine.
  12. The Invision Community software includes a class to work with request data, including GET, POST, and REQUEST data, cookies, and detecting certain information about the request (such as whether it was submitted via AJAX or not). You will need to use this class to perform some common actions working with web-based software. The class is accessed through \IPS\Requst::i() and implements the Singleton pattern. GET, POST and REQUEST data To access request variables, you simply call them as properties of the class. For instance, you can check if a request variable is set and output it like so if( isset( \IPS\Request::i()->someVariable ) ) { print \IPS\Request::i()->someVariable; } Request data is largely unmodified from the originally submitted data except that NULL bytes and RTL control characters are stripped from the input, and slashes are stripped if magic quotes is enabled. This means that all request data should be considered potentially tainted and you will need to take precautions not to introduce security issues by relying upon "clean" request data from this class. If the request is made using the PUT method (which happens through our REST API in some cases, for instance), this request data is also available through this class. Working with cookies Cookie values are available in the cookies property of the \IPS\Request class. print \IPS\Request::i()->cookie['member_id']; If your site uses a cookie prefix, note that it will be stripped automatically here. To set a cookie, you can use the setCookie method. As with fetching a cookie, you should not include any cookie prefix that is used. The method signature is /** * Set a cookie * * @param string $name Name * @param mixed $value Value * @param \IPS\DateTime|null $expire Expiration date, or NULL for on session end * @param bool $httpOnly When TRUE the cookie will be made accessible only through the HTTP protocol * @param string|null $domain Domain to set to. If NULL, will be detected automatically. * @param string|null $path Path to set to. If NULL, will be detected automatically. * @return bool */ public function setCookie( $name, $value, $expire=NULL, $httpOnly=TRUE, $domain=NULL, $path=NULL ) Typically you should leave $domain and $path as NULL, however these can be overridden if needed, for instance if you are working on integrating with a third party service. You can clear all login cookies through \IPS\Request::i()->clearLoginCookies() if needed, which includes the member_id and pass_hash cookies, as well as any forum password cookies (where a user may have entered a password to access a forum). Other helper methods There are several helper methods in the \IPS\Request class that you can leverage as needed to check various properties of the request \IPS\Request::i()->isAjax() Returns a boolean true or false to indicate if the request was made via AJAX \IPS\Request::i()->isSecure() Returns a boolean true or false to indicate if the request was made over SSL (https) \IPS\Request::i()->url() Returns an \IPS\Http\Url object representing the requested URL. Note that fragments (values after the hash symbol in a URL) are not sent to the server and will not be available to check at the server level. \IPS\Request::i()->ipAddress() Returns the IP address used to make the current request, taking into account proxy servers and forwarding if the administrator has chosen to do so in the AdminCP. \IPS\Request::i()->ipAddressIsBanned() Returns a boolean true or false to denote if the current request IP address is banned in the AdminCP IP address ban filters. \IPS\Request::i()->requestMethod() Returns the current request method, upper-cased. \IPS\Request::i()->isCgi() Returns a boolean true or false to indicate if the current request is being processed by a CGI wrapper in PHP \IPS\Request::i()->floodCheck() Checks the user's search flood control setting to determine if the user has recently searched and is searching again too quickly. If the user has searched recently and this method is called before the flood control time period has completed, an error will be shown to the user, otherwise their "last searched" time is updated to the current time for subsequent checks. \IPS\Request::i()->confirmedDelete( $title, $message, $submit ) When a user deletes data, it is prudent to confirm that the action was intended and not a misclick or similar. To facilitate this you can call the confirmedDelete() method which will verify if the user has confirmed the deletion already, and if not will show a confirmation screen first.
  13. The Invision Community framework provides a powerful helper class to work with URLs, including parsing URLs and making requests to (and reading responses from) URLs. The software automatically uses cURL when available, falling back to standard sockets if not (this behavior can also be overridden by constants if an environment wishes to force cURL or sockets, specifically). Working with URLs The \IPS\Http\Url class is used to work with URLs, and two helper methods form the primary interface to create a URL object with this class: internal(): Use this method when creating an internal URL, such as a URL to a forum or topic. The method handles automatically creating friendly URLs when configured to do so. external(): Pass any full URL to this method to create a URL object out of the URL string. // Create a URL to topic ID 1 $url = \IPS\Http\Url::internal( 'app=forums&module=forums&controller=topic&id=1', 'front', 'forums_topic', array( 'topic-friendly-slug' ) ); $url = \IPS\Http\Url::external( 'https://www.google.com' ); // Create a URL object for a standard URL string The internal method accepts the following parameters: The URL query string The 'base' ('front' or 'admin', depending upon whether the URL is to a page in the AdminCP or not) The SEO template, if applicable An array of SEO slugs if the template calls for one A PROTOCOL_* constant from \IPS\Http\Url to override whether an http or https URL should be generated. You can usually omit this parameter and let the software determine the right protocol to use automatically. The external method simply accepts a URL string. If you are unsure if the URL object you are creating is internal or external, you can alternatively use the createFromString() method (passing the full URL as you would with the external() method), however note that this method is somewhat performance-intensive and the directly internal() and external() methods are preferred. The first parameter for this method is the URL, the second parameter is a boolean flag indicating if the URL may be a friendly internal URL, and the third and final parameter is a boolean flag indicating if you wish to automatically encode any components that are invalid instead of throwing an error (defaulting to FALSE - set this to TRUE if the URL is user-supplied and should not throw an error). You can make adjustments to the URL after creating the URL object by calling various methods. setScheme(): You can pass the scheme in (i.e. http or https), or pass in NULL to use a protocol-relative scheme (i.e. no scheme) setHost(): Accepts a full host name setPath(): Accepts a full valid path setQueryString(): Accepts a query string key as the first parameter and its value as the second parameter OR an array of key => value pairs as the first parameter setFragment(): Accepts a fragment stripQueryString(): Accepts a query string key or an array of query string keys and removes those query string parameters (if present) from the URL When you wish to output the URL, you can cast the \IPS\Http\Url object as a string print (string) $urlObject; The class has a few additional properties and methods you may wish to reference or call, outlined below: isInternal: This property denotes if the URL is internal or not isFriendly: This property denotes if the URL is a friendly (internal) URL or not queryString: This property is an array of key => value query string parameters in the URL hiddenQueryString: This property is an array of key => value query string parameters that would have been present if the URL were not a friendly URL. For instance, if you create an internal friendly URL object, this property will contain the associated query string parameters that are not shown (because a friendly URL is used) csrf(): Call this method to add the current viewing user's CSRF key to the URL as a query string argument. This is then later checked in controllers to prevent CSRF-style attacks. If you have a state-changing request that does not use the \IPS\Helpers\Form class, you should typically check the CSRF key. (static) seoTitle(): You can call this method to create a valid friendly URL slug. Typically this is done when content is submitted and the URL slug is stored for later reference, however you can also call this method on the fly if needed. Making requests Once you have created an \IPS\Http\Url object, you can make requests to it. To do this, you first call request() against the URL object. /** * Make a HTTP Request * * @param int|null $timeout Timeout * @param string $httpVersion HTTP Version * @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects * @return \IPS\Http\Request */ public function request( $timeout=null, $httpVersion=null, $followRedirects=5 ) This returns an \IPS\Http\Request object. From here there are some methods you can call before executing the request: login(): Accepts a username as the first parameter and a password as the second parameter, and performs a basic authorization request against the URL. setHeaders(): Accepts an array of key => value pairs of headers that should be included with the request. sslCheck(): Accepts a boolean true or false as the only parameter, signaling whether SSL certificates should be validated or not. In most cases, this should be left default (true), unless you are aware that the SSL URL you are making a request against has an invalid certificate. forceTls(): Forces TLS for the request. This is primarily used with some payment gateways that enforce TLS requests. Afterwards, you can now make the request. To do so, you call the request method that you wish to perform (e.g. to perform a GET request you call get() and to perform a PUT request you call put()), passing along any parameters that should be included with the request (for POST and PUT requests). $request = \IPS\Http\Url::external( "http://someurl.com" )->request()->get(); This returns an \IPS\Http\Response object, which you can now inspect and manipulate as needed. Firstly, there are several useful properties you may need to reference: httpResponseVersion: This is the HTTP version of the response (1.0 or 1.1, typically) httpResponseCode: This is the HTTP response code. You may need to verify that a valid response code (i.e. 200) was returned after making a request. httpResponseText: This is the HTTP response text. For a 200 request, for instance, this will be "OK". httpHeaders: This will be an array containing all HTTP headers in the response as key => value pairs cookies: This will be an array of all Set-Cookie headers as key => value pairs. content: This is the body of the response. Casting the response as a string returns the content property noted above. There are some methods in the \IPS\Http\Response class that you can leverage to make working with certain common responses easier. decodeJson(): Calling this method will run the response through json_decode before returning it. If the response is not valid JSON, a RuntimeException will be thrown. decodeXml(): Calling this method will parse the response as XML, throwing a RuntimeException if the response is not valid XML. It is additionally worth noting that if the request fails for some reason (for instance, a time out connecting to the remote server), an \IPS\Http\Request\Exception exception will be thrown. To that end, you must wrap requests in a try/catch block. // Create a URL object $url = \IPS\Http\Url::external( "http://someurl.com" )->setQueryString( 'key', 'value' ); // Now fetch it and decode the JSON try { $response = $url->request()->get()->decodeJson(); } catch( \IPS\Http\Request\Exception $e ) { die( "There was a problem fetching the request" ); } catch( \RuntimeException $e ) { die( "The response was not valid JSON" ); } var_dump( $response ); exit;
  14. We are exploring possibilities as Lindy said, including implementing more event-based triggers (which we do in some places already, such as MemberSync extensions) and looking into adjusting the hooking system to allow it to work with traits as well. As Lindy said, we will update everyone with more information in due course.
  15. Accessing the database to store and query data is a necessity of nearly all applications and plugins that integrate with the Invision Community software. The \IPS\Db class handles database connections, and extends the default mysqli class in the PHP core library. Connecting to the database The default database connection (denoted by the connection details in conf_global.php) can be established automatically by calling \IPS\Db::i(). If a connection has not yet been established, it will be done so immediately on the fly when this method is called. The default connection uses utf8 (or utf8mb4 depending upon your configuration) and all database tables and columns must be configured as utf8. This is generally handled automatically by the Invision Community software, but is an important note to keep in mind. If you need to establish a connection to a remote database, this can be done by passing parameters to the i() method of \IPS\Db. The first parameter is an arbitrary string connection identifier, and the second parameter is an array with the connection settings. $connection = \IPS\Db::i( 'external', array( 'sql_host' => 'localhost', 'sql_user' => 'username', 'sql_pass' => 'password', 'sql_database' => 'database_name', 'sql_port' => 3306, 'sql_socket' => '/var/lib/mysql.sock', 'sql_utf8mb4' => true, ) ); You only need to supply the parameters that your connection requires. You can also support read/write separation automatically by passing the same parameters a second time with the prefix "sql_read_" instead of just "sql_", pointing to your read-only MySQL instance. Selecting data Selecting data from the database is one of the more common needs when interacting with the database. /** * Build SELECT statement * * @param array|string $columns The columns (as an array) to select or an expression * @param array|string $table The table to select from. Either (string) table_name or (array) ( name, alias ) or \IPS\Db\Select object * @param array|string|NULL $where WHERE clause - see \IPS\Db::compileWhereClause() for details * @param string|NULL $order ORDER BY clause * @param array|int $limit Rows to fetch or array( offset, limit ) * @param string|NULL|array $group Column(s) to GROUP BY * @param array|string|NULL $having HAVING clause (same format as WHERE clause) * @param int $flags Bitwise flags * @li \IPS\Db::SELECT_DISTINCT Will use SELECT DISTINCT * @li \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS Will return the result as a multidimensional array, with each joined table separately * @li \IPS\Db::SELECT_FROM_WRITE_SERVER Will send the query to the write server (if read/write separation is enabled) * @return \IPS\Db\Select * */ public function select( $columns=NULL, $table, $where=NULL, $order=NULL, $limit=NULL, $group=NULL, $having=NULL, $flags=0 ) You can call the select() method to perform a SELECT database query. Doing so returns an \IPS\Db\Select object which allows you to further refine the SELECT query. For instance, there are methods in this class to force a specific index to be used, to join other database tables, and to specify which key/value fields to use for results. // Get the select object $select = \IPS\Db::i()->select( '*', 'some_table', array( 'field=?', 1 ), 'some_column DESC', array( 0, 10 ) ); // Force a specific index to be used for the query $select = $select->forceIndex( 'some_index' ); // Join another table on $select = $select->join( 'other_table_name', 'other_table_name.column=some_table.column', 'LEFT' ); // Now, get the number of results returned $results = count( $select ); // Tell the iterator that keys should be 'column_a' and values should be 'column_b' $select = $select->setKeyField( 'column_a' )->setValueField( 'column_b' ); // Finally, loop over the results foreach( $select as $columnA => $columnB ) { print $columnA . ': ' . $columnB . '<br>'; } There are some important things to note here. The WHERE clause accepts many different formats you should be aware of. You can pass a string as the WHERE clause some_column='some value' or you can pass an array with the first element the WHERE clause using ? as placeholders for values and then each placeholder replaced with the subsequent array entries. This uses prepared statements in MySQL to help avoid SQL injection concerns and is recommended. array( 'some_column=? OR some_column=?', 'first value', 'second value' ) or, finally, you can pass an array of clauses which will be AND joined together array( array( 'some_column=?', 'test value' ), array( 'other_column=?', 1 ) ) You can call setKeyField() without calling setValueField(). Instead of the value being a string in this case, it will simply be an array of all columns selected (or if only one column is selected, the value will be a string with that column's value). Note that you must select the columns you wish to use for setKeyField and/or setValueField. The method definition for join() requires the first parameter to be the name of the table you wish to join, then the join 'on' clause, followed by the type of join to use (defaulting to LEFT). You can also specify a using clause for the join as the fourth parameter, if necessary. To control which columns to select, you will need to adjust the first parameter of the original SELECT clause (if you pass '*' MySQL will return all columns from all tables selected from and/or joined). \IPS\Db\Select implements both Iterator and Countable. This means you can treat it as an array and use a foreach() loop to loop over the results, and you can call count() against the object to get the number of results. Be aware, however, that count() by default only returns the number of results that the query returned. If you have 1000 rows in the table and use a limit clause to only return 100, then count() will show 100. Often you are selecting just one row (i.e. when performing a COUNT(*) query) and a bulit in first() method facilitates this. Be aware, however, that if the row does not exist an UnderflowException is thrown, so you should wrap such queries in a try/catch statement. try { $row = \IPS\Db::i()->select( '*', 'table', array( 'id=?', 2 ) )->first(); } catch( \UnderflowException $e ) { // There is no row with id=2 in table } Inserting, Updating and Deleting rows You will also want to insert, update and delete rows in MySQL periodically. To do so, you use the aptly named insert(), update() and delete() methods of the database driver. // Insert a row $new_id = \IPS\Db::i()->insert( 'some_table', array( 'column_one' => 'value', 'column_two' => 'value2' ) ); // Update that row \IPS\Db::i()->update( 'some_table', array( 'column_two' => 'other value' ), array( 'id_column=?', $new_id ) ); \IPS\Db::i()->delete( 'some_table', array( 'id_column=?', $new_id ) ); // Delete the row Inserting is straight forward. The first parameter to the method is the table name, followed by an associative array of column names => values. The new autoincrement id (if applicable) is returned by the method. Note that there is also a replace() method which behaves like insert(), except a REPLACE INTO query will be executed instead of an INSERT INTO query (in which case, if a duplicate unique index is encountered the original will be replaced with the new row). The update() method expects the first parameter to be the table name, the second parameter to be an associative array of column names => values, and the third parameter to be the where clause (if needed). Additionally, you can pass an array of table joins as the fourth parameter if needed, an array to represent the limit clause as the fifth parameter, and flags to modify the query as the last parameter, including: \IPS\Db::LOW_PRIORITY Will use LOW_PRIORITY \IPS\Db::IGNORE Will use IGNORE The delete() method is typically called with just the first parameter, the table name, to empty out the entire table, or also with a second parameter, the where clause, to delete specific rows. The method additionally accepts a third parameter to control the order of results for the DELETE query, a fourth parameter to limit the number of results deleted, and a fifth column that specifies the statement column if the WHERE clause is a statement. Affecting database structure You can create database tables, add, alter and remove columns from existing tables, and add, alter and remove indexes from existing tables through the \IPS\Db library. Additionally, methods exist to help determine if a table, column or index exists before using it. Please be warned that while it's possible to alter existing database tables, we are NOT accepting any marketplace submissions which alter existing IPS database tables! /** * Does table exist? * * @param string $name Table Name * @return bool */ public function checkForTable( $name ) /** * Does column exist? * * @param string $name Table Name * @param string $column Column Name * @return bool */ public function checkForColumn( $name, $column ) /** * Does index exist? * * @param string $name Table Name * @param string $index Index Name * @return bool */ public function checkForIndex( $name, $index ) /** * Create Table * * @code \IPS\Db::createTable( array( 'name' => 'table_name', // Table name 'columns' => array( ... ), // Column data - see \IPS\Db::compileColumnDefinition for details 'indexes' => array( ... ), // (Optional) Index data - see \IPS\Db::compileIndexDefinition for details 'comment' => '...', // (Optional) Table comment 'engine' => 'MEMORY', // (Optional) Engine to use - will default to not specifying one, unless a FULLTEXT index is specified, in which case MyISAM is forced 'temporary' => TRUE, // (Optional) Will sepcify CREATE TEMPORARY TABLE - defaults to FALSE 'if_not_exists' => TRUE, // (Optional) Will sepcify CREATE TABLE name IF NOT EXISTS - defaults to FALSE ) ); * @endcode * @param array $data Table Definition (see code sample for details) * @throws \IPS\Db\Exception * @return void|string */ public function createTable( $data ) /** * Create copy of table structure * * @param string $table The table name * @param string $newTableName Name of table to create * @throws \IPS\Db\Exception * @return void|string */ public function duplicateTableStructure( $table, $newTableName ) /** * Rename table * * @see <a href='http://dev.mysql.com/doc/refman/5.1/en/rename-table.html'>Rename Table</a> * @param string $oldName The current table name * @param string $newName The new name * @return void * @see <a href='http://stackoverflow.com/questions/12856783/best-practice-with-mysql-innodb-to-rename-huge-table-when-table-with-same-name-a'>Renaming huge InnoDB tables</a> * @see <a href='http://www.percona.com/blog/2011/02/03/performance-problem-with-innodb-and-drop-table/'>Performance problem dropping huge InnoDB tables</a> * @note A race condition can occur sometimes with InnoDB + innodb_file_per_table so we can't drop then rename...see above links */ public function renameTable( $oldName, $newName ) /** * Alter Table * Can only update the comment and engine * @note This will not examine key lengths and adjust. * * @param string $table Table name * @param string|null $comment Table comment. NULL to not change * @param string|null $engine Engine to use. NULL to not change * @return void */ public function alterTable( $table, $comment=NULL, $engine=NULL ) /** * Drop table * * @see <a href='http://dev.mysql.com/doc/refman/5.1/en/drop-table.html'>DROP TABLE Syntax</a> * @param string|array $table Table Name(s) * @param bool $ifExists Adds an "IF EXISTS" clause to the query * @param bool $temporary Table is temporary? * @return mixed */ public function dropTable( $table, $ifExists=FALSE, $temporary=FALSE ) /** * Add column to table in database * * @see \IPS\Db::compileColumnDefinition * @param string $table Table name * @param array $definition Column Definition (see \IPS\Db::compileColumnDefinition for details) * @return void */ public function addColumn( $table, $definition ) /** * Modify an existing column * * @see \IPS\Db::compileColumnDefinition * @param string $table Table name * @param string $column Column name * @param array $definition New column definition (see \IPS\Db::compileColumnDefinition for details) * @return void */ public function changeColumn( $table, $column, $definition ) /** * Drop a column * * @param string $table Table name * @param string|array $column Column name * @return void */ public function dropColumn( $table, $column ) /** * Add index to table in database * * @see \IPS\Db::compileIndexDefinition * @param string $table Table name * @param array $definition Index Definition (see \IPS\Db::compileIndexDefinition for details) * @param bool $discardDuplicates If adding a unique index, should duplicates be discarded? (If FALSE and there are any, an exception will be thrown) * @return void */ public function addIndex( $table, $definition, $discardDuplicates=TRUE ) /** * Modify an existing index * * @see \IPS\Db::compileIndexDefinition * @param string $table Table name * @param string $index Index name * @param array $definition New index definition (see \IPS\Db::compileIndexDefinition for details) * @return void */ public function changeIndex( $table, $index, $definition ) /** * Drop an index * * @param string $table Table name * @param string|array $index Column name * @return mixed */ public function dropIndex( $table, $index ) Most of these methods are self-explanatory and infrequently used, except when using the developer center to add queries for upgrades. Miscellaneous Finally, there are a handful of methods and properties in the class you may find useful or relevant while working with the database driver. If you need to obtain the database prefix being used for tables (represented by sql_tbl_prefix in conf_global) you can do so by calling \IPS\Db::i()->prefix . If you are building queries to manually run, you will need to prepend this to table names. If you need to build an SQL statement and then return it instead of running it, you can set \IPS\Db::i()->returnQuery = TRUE before calling the driver to build a query. To run a MySQL query that has been fully built already represented as a string, you can call the query() method. \IPS\Db::i()->query( "UPDATE some_table SET field_a='value' WHERE id_field=1" ); You should typically avoid using the query() method directly, as the other built in methods automatically handle things like escaping values, prepending the database table prefix and so on. If you need to build a UNION statement, there is a method available to facilitate this. /** * Build UNION statement * * @param array $selects Array of \IPS\Db\Select objects * @param string|NULL $order ORDER BY clause * @param array|int $limit Rows to fetch or array( offset, limit ) * @param string|null $group Group by clause * @param bool $unionAll TRUE to perform a UNION ALL, FALSE (default) to perform a regular UNION * @param int $flags Bitwise flags * @param array|string|NULL $where WHERE clause (see example) * @param string $querySelect Custom select for the outer query * @return \IPS\Db|Select */ public function union( $selects, $order, $limit, $group=NULL, $unionAll=FALSE, $flags=0, $where=NULL, $querySelect='*' ) To build a FIND_IN_SET() clause, which allows the query to search for specific values in a MySQL field that contains comma-separated values, you can use the findInSet() method. /** * FIND_IN_SET * Generates a WHERE clause to determine if any value from a column containing a comma-delimined list matches any value from an array * * @param string $column Column name (which contains a comma-delimited list) * @param array $values Acceptable values * @param bool $reverse If true, will match cases where NO values from $column match any from $values * @return string Where clause * @see \IPS\Db::in() More efficient equivilant for columns that do not contain comma-delimited lists */ public function findInSet( $column, $values, $reverse=FALSE ) Similarly, you can build an IN() clause by using the in() method. /** * IN * Generates a WHERE clause to determine if the value of a column matches any value from an array * * @param string $column Column name * @param array $values Acceptable values * @param bool $reverse If true, will match cases where $column does NOT match $values * @return string Where clause * @see \IPS\Db::findInSet() For columns that contain comma-delimited lists */ public function in( $column, $values, $reverse=FALSE ) If you are performing a query against a bitwise column and need to check a value, you can use the bitwiseWhere method (or simply build the WHERE clause manually).. /** * Bitwise WHERE clause * * @param array $definition Bitwise keys as defined by the class * @param string $key The key to check for * @param bool $value Value to check for * @return string * @throws \InvalidArgumentException */ public function bitwiseWhere( $definition, $key, $value=TRUE ) You will find that most of these miscellaneous methods are not referenced or needed as often as the core insert, update, delete, replace and select methods.
  16. Date and time handling is an important function of the software, and the \IPS\DateTime class provides several utility methods to assist with handling dates and times reliably. It is important to note that the \IPS\DateTime class extends the core PHP DateTime class, so all of the general PHP methods for working with dates and times are immediately available through this interface as well. Dates and times are represented in the database by unix timestamps. When displaying a date to a user, however, we need to convert the timestamp into a human-readable date and time, localized to the viewer's time zone. You can use the ts() static method for this purpose. $time = \IPS\DateTime::ts( $timestamp ); You can also use the create() static method to create a new datetime instance (which will default to the current date/time). $time = \IPS\DateTime::create(); The primary methods you will then use to display the date and/or time are as follows /* Show the date and time in the user's timezone */ print (string) $time; The magic __toString() method will automatically take care of time zone conversions and so on. /* Output a <time> HTML tag with the relative time displayed */ print $time->html( TRUE, FALSE, NULL ); The first parameter determines whether the date/time should be capitalized or not (set this to FALSE if the time will be used in the middle of a sentence for instance), while the second parameter determines if the 'short' version of the date/time should be used even when not on mobile (e.g. 1d instead of 1day). The last parameter allows you to override the member or language to use to format the time. /* Show just the date */ print $time->localeDate(); /* Show just the time - first parameter indicates whether or not to return seconds, while the second parameter indicates whether or not to return minutes */ print $time->localeTime( TRUE, TRUE ); /* Return just the month and day, without the year (or time) print $time->dayAndMonth(); /* Return the date with the 4 digit year */ print $time->fullYearLocaleDate(); /* Format the relative date/time */ print $time->relative( \IPS\DateTime::RELATIVE_FORMAT_NORMAL ); For the relative() method, the following constants are recognized: RELATIVE_FORMAT_NORMAL: Yesterday at 2pm RELATIVE_FORMAT_LOWER: yesterday at 2pm (e.g. "Edited yesterday at 2pm") RELATIVE_FORMAT_SHORT: 1dy (for mobile view) If you need to use a completely custom format, you can use the strFormat() method. While the DateTime class in PHP already has a built in format() method, it is not locale-aware and as such the strFormat() is used instead (which accepts any format accepted by strftime in PHP). print $time->strFormat( '%B' ); Finally, there are some standardized formats supported out of the box as well, primarily useful when specifications require dates to be formatted in a certain way (e.g. RSS) print $time->rfc3339(); // 2017-06-06T11:00:00Z print $time->rfc1123(); // Tuesday, 6 June 2017 11:00:00 GMT It is important to remember when caching data that dates and times should be localized based on the current viewing user's time zone (which is automatically detected). For this reason, you should not cache formatted dates or times, but rather format on display. Finally, there is a "datetime" template plugin which can be used to format dates and times in templates automatically. {datetime="$timestamp"} This will output the localized date and time from the __toString() call. The extra attributes supported by this plugin are: dateonly: Only return the date norelative: Do not return a relative date lowercase: Return the date in lowercase short: Return the short form of the date
  17. bfarber

    nexus/Item

    What it is The nexus/Item extension allows your application to integrate with Commerce, supporting paid content within a third party application. Downloads uses this integration to better support paid files through Commerce, for instance. How to use The class template includes 4 properties, and various methods depending upon the class you are extending. By default the template is generated as so: class _{class} extends \IPS\nexus\Invoice\Item\Charge // or \IPS\nexus\Invoice\Item\Purchase You will need to decide if you are extending an Item\Charge or an Item\Purchase and adjust accordingly. Once you have done so, you can extend any methods in this class that you wish. The four class properties to define are /** * @brief Application */ public static $application = '{app}'; /** * @brief Application */ public static $type = ''; /** * @brief Icon */ public static $icon = ''; /** * @brief Title */ public static $title = ''; You will specify your application under $application, the type of content under $type, an fa-* icon for $icon, and a language string for $title. From there, you will need to determine which methods you will need to override. Downloads, for instance, overrides the url() and acpUrl() methods to link to the file that was purchased, the image() and purchaseImage() methods to return the screenshot of the file as the image to represent the purchase, the renewalPaymentMethodIds() method to restrict which payment gateways can be used for renewals, and the acpPage() and clientAreaPage() methods to return information about the file when viewing the purchase. You may only need to override onPaid() in order to perform an action when a purchase within your application is marked as paid, or you may need to override other methods as Downloads does.
  18. What it is ContentModeratorPermissions extensions allow your application to add new content item permission options to the moderator configuration area. This extension is very similar to the ModeratorPermissions extension, except that it is specifically designed to allow you to adjust content item permissions. How to use This type of extension will need to be created manually by creating the folder and the class within the folder in your application, rather than through the developer center. Once created, you will be able to define the same 3 methods as you would in a ModeratorPermissions extension: getPermissions(), onChange() and onDelete(). The forum application defines such an extension which allows it to rebuild the search index if moderator permissions for forums where only authors can see their own posts are adjusted. This prevents moderators from being able to see topics posted by other users if they should not be able to.
  19. bfarber

    core/Uninstall

    What it is Uninstall extensions are executed when an application is uninstalled, allowing an application an opportunity to perform any necessary cleanup not automatically performed by Invision Community. Examples include deleting files that may have been stored special during installation, or pinging a remote API to disable the site. How to use The extension defines 3 methods (and a deprecated method left in place for backwards compatibility) /** * Code to execute before the application has been uninstalled * * @param string $application Application directory * @return array */ public function preUninstall( $application ) { } The preUninstall() method is called just prior to the application being uninstalled. This can be useful if you need to look data up before it is deleted, in order to perform the cleanup. /** * Code to execute after the application has been uninstalled * * @param string $application Application directory * @return array */ public function postUninstall( $application ) { } The postUninstall() method is called just after the application has been uninstalled. /** * Code to execute when other applications or plugins are uninstalled * * @param string $application Application directory * @param int $plugin Plugin ID * @return void */ public function onOtherUninstall( $application=NULL, $plugin=NULL ) { } If another application is uninstalled, you may need to take action within your application. For instance, your application may enable certain features integrating with the Calendar application, however if the Calendar application is uninstalled you may need to disable those features. This method allows your application an opportunity to take action when a different application (or plugin) is uninstalled.
  20. What it is The StreamItems extension allows an application to add "extra items" to stream results. While content stored in the search index (which includes content item data) is automatically returned in activity streams, if you want to show other data such as users signing up in an application or users performing another action in an application in activity streams, you will need to use a StreamItems extension to accomplish this. Calendar uses this functionality to show when a user has RSVP'd for an event in activity streams, for example. How to use The extension will define a single method where you will fetch and return extra stream results /** * Is there content to display? * * @param \IPS\Member|NULL $author The author to limit extra items to * @param Timestamp|NULL $lastTime If provided, only items since this date are included. If NULL, it works out which to include based on what results are being shown * @param Timestamp|NULL $firstTime If provided, only items before this date are included. If NULL, it works out which to include based on what results are being shown * @return array Array of \IPS\Content\Search\Result\Custom objects */ public function extraItems( $author=NULL, $lastTime=NULL, $firstTime=NULL ) { // Note! // Your application must define a setting and a language string in the format of "all_activity_{application}_{extensionname}" all in lower case. Without this, this plugin will not be executed. // This setting will automatically be used to store the administrators choice of whether to show this data or not. } return array(); You need to define a specific language string as shown in the code comment above for the extension to process, however otherwise this is all you need. If the content is being limited by author, the $author parameter will be set to an \IPS\Member object, otherwise it will be set to NULL. Similarly, $lastTime and $firstTime may be set to timestamps or may be set to NULL ($lastTime will typically be set, however). You will then fetch any results that match the parameters supplied, and create an array of \IPS\Content\Search\Result\Custom objects to return. /** * Is there content to display? * * @param \IPS\Member|NULL $author The author to limit extra items to * @param Timestamp|NULL $lastTime If provided, only items since this date are included. If NULL, it works out which to include based on what results are being shown * @param Timestamp|NULL $firstTime If provided, only items before this date are included. If NULL, it works out which to include based on what results are being shown * @return array Array of \IPS\Content\Search\Result\Custom objects */ public function extraItems( $author=NULL, $lastTime=NULL, $firstTime=NULL ) { $results = array(); $where = array( array( 'date>?', $lastTime ) ); if ( $firstTime ) { $where[] = array( 'date<?', $firstTime ); } if ( $author ) { $where[] = array( 'author_id=?', $author->member_id ); } foreach ( \IPS\Db::i()->select( '*', 'my_table', $where, 'date DESC', 10 ) as $row ) { $results[] = new \IPS\Content\Search\Result\Custom( \IPS\DateTime::ts( $row[ 'date' ] ), \IPS\Member::loggedIn()->language()->addToStack( 'some_language_string' ) ); } return $results; }
  21. bfarber

    core/Sitemap

    What it is A Sitemap extension allows your application to add content to the sitemap that the Invision Community software builds. Content items that are properly mapped in a ContentRouter extension will automatically be added, depending upon permissions, so you only need to add content not mapped through a content router extension. Member profiles are an example of non-content-item content added to the sitemap through a Sitemap extension. How to use When you generate the extension, 4 methods and one class property will be added to the class template by default. /** * @brief Recommended Settings */ public $recommendedSettings = array(); You can define the default recommended settings for settings defined in the settings() method (outlined below) using the class property. The property is an array with keys being the setting keys and the values being the recommended setting values. /** * Add settings for ACP configuration to the form * * @return array */ public function settings() { } The settings() method should return an array with keys as the setting keys, and the values being form helper objects where the form helper name is the setting key. For example: /** * Add settings for ACP configuration to the form * * @return array */ public function settings() { return array( 'my_setting' => new \IPS\Helpers\Form\Number( 'my_setting', \IPS\Settings::i()->my_setting, TRUE ) ); } /** * Get the sitemap filename(s) * * @return array */ public function getFilenames() { return array( 'sitemap_' . mb_strtolower('{class}') ); } The getFilenames() method returns a list of possible filenames generated by the Sitemap extension. You should not include more than \IPS\Sitemap::MAX_PER_FILE links in a single sitemap file, so in situations where you may have more than this many links to include, you will need to split the links amongst multiple sitemap files. For example: /** * Get the sitemap filename(s) * * @return array */ public function getFilenames() { /* Get a count of how many files we'll be generating */ $count = \IPS\Db::i()->select( 'COUNT(*)', 'my_database_table' )->first(); $count = ( $count > \IPS\Sitemap::MAX_PER_FILE ) ? ceil( $count / \IPS\Sitemap::MAX_PER_FILE ) : 1; /* Generate the file names */ $files = array(); for( $i=1; $i <= $count; $i++ ) { $files[] = "sitemap_myapp_" . $i; } /* Return */ return $files; } Here we select the total number of records we will be including, then we divide it by the number of records to include per-file, and then we generate names for each file as "sitemap_myapp_1", "sitemap_myapp_2" and so on. /** * Generate the sitemap * * @param string $filename The sitemap file to build (should be one returned from getFilenames()) * @param \IPS\Sitemap $sitemap Sitemap object reference * @return void */ public function generateSitemap( $filename, $sitemap ) { } Finally, the generateSitemap() file generates the actual sitemap file. First, you will need to take the filename to determine which results we are including. For instance, if the filename is "sitemap_myapp_2", then we know that our start offset in our database query should be "2 * \IPS\Sitemap::MAX_PER_FILE" and our offset limit should be "\IPS\Sitemap::MAX_PER_FILE". You will then want to fetch the results (typically by looping over a database result set) and push the results into an array temporarily before sending the values to buildSitemapFile(). /** * Generate the sitemap * * @param string $filename The sitemap file to build (should be one returned from getFilenames()) * @param \IPS\Sitemap $sitemap Sitemap object reference * @return void */ public function generateSitemap( $filename, $sitemap ) { /* Which file are we building? */ $_info = explode( '_', $filename ); $index = array_pop( $_info ) - 1; $entries = array(); $start = \IPS\Sitemap::MAX_PER_FILE * $index; $limit = \IPS\Sitemap::MAX_PER_FILE; /* Retrieve the members */ foreach( \IPS\Db::i()->select( '*', 'my_database_table', NULL, 'id ASC', array( $start, $limit ) ) as $row ) { $entries[] = array( 'url' => \IPS\Http\Url::internal( "app=myapp&module=module&controller=controller&id={$row['id']}", 'front', 'seotemplate', $row['seo_name'] ) ); } /* Build the file */ $sitemap->buildSitemapFile( $filename, $entries ); }
  22. bfarber

    core/Queue

    What it is Queue extensions allow your application to process certain functionality in the background. When deciding whether you need to use a task or a queue extension, your consideration should often come down to how much data needs to be processed. If you are running through 10 items on a regular schedule, you should use a task. If you need to loop through 50,000 items as quickly as possible, but this can be done in the background instead of immediately, you should use a Queue extension. Queue extensions are used to send follow notifications (when there are more than a specific number of recipients of the notification) as well as rebuilding images, among many other things. How it works When you create a new instance of this extension, there will be 4 methods in the class template. /** * Parse data before queuing * * @param array $data * @return array */ public function preQueueData( $data ) { return $data; } The preQueueData() method allows the Queue extension to calculate and store any data necessary prior to initiating the background queue task. Often times you may need to capture the highest ID available in the database at the time the task initiates to ensure you do not rebuild any data beyond that ID. Or you may need to capture the number of items in a database table for use with the progress meter later. You can simply return the $data unmodified, or you can add new entries to the $data array and return it. /** * Run Background Task * * @param mixed $data Data as it was passed to \IPS\Task::queue() * @param int $offset Offset * @return int New offset * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete */ public function run( $data, $offset ) { } The run() method accepts the data returned from preQueueData(), as well as an offset, and then runs the task. You will generally want to process a limited number of items each run (how many depends on how intensive the processing is, but typically somewhere between 50 and 500), and then either return the new offset (the number of items processed + the initial offset), or the new ID to start at. You can use this method if the database table you are working with is extremely large and you would prefer to run a query like "WHERE id > X LIMIT 500" instead of "LIMIT 50000, 500", which can get slow over time. If you have finished processing all of the items for this task, you should throw new \IPS\Task\Queue\OutOfRangeException;, however returning NULL will also cause the task to cease running as well. /** * Get Progress * * @param mixed $data Data as it was passed to \IPS\Task::queue() * @param int $offset Offset * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete */ public function getProgress( $data, $offset ) { return array( 'text' => 'Doing something...', 'complete' => 50 ); } The getProgress() method returns the title of the task to show on the AdminCP dashboard. You will be returning an array with key 'text' being the text to display and 'complete' being the percentage completed so far for this task. You can reference the offset against the total number of items in a database table to determine the percentage completed so far. /** * Perform post-completion processing * * @param array $data * @return void */ public function postComplete( $data ) { } The optional postComplete() method allows your extension to perform any post-completion cleanup. For example, if your task was moving files from one area to another, you may wish to delete the original files (or container folder) after the files have finished moving. To initiate the queue task after you have created it, you call \IPS\Task::queue() \IPS\Task::queue( 'myapp', 'ExtensionKey', array( 'initial_data' => true ), 3, array( 'initial_data' ) ); The first parameter is the application key and the second parameter is the extension key (e.g. the class name or file name of the Queue extension you wish to initiate). The third parameter is the default value for the $data array, which is then passed to preQueueData() and later to run() and getProgress(). If you have no initial data to supply you can pass an empty array or NULL, and you can still populate this variable within preQueueData() as needed. The fourth parameter is the queue task's priority. The lower priority, the higher the priority - that is to say, tasks with a priority of 1 will be ran before tasks with a priority of 2 (or higher). The last parameter is an array of keys from the data array that can be checked for duplicates, resulting in the task not being duplicated. For example, if you call the following: \IPS\Task::queue( 'myapp', 'ExtensionKey' ); \IPS\Task::queue( 'myapp', 'ExtensionKey' ); There will be two background tasks created. If, however, you call the following: \IPS\Task::queue( 'myapp', 'ExtensionKey', array( 'initial_data' => true ) ); \IPS\Task::queue( 'myapp', 'ExtensionKey', array( 'initial_data' => true ), 3, array( 'initial_data' ) ); There will only be one background task. This is because we instructed the queue() method on the second call that if there was already a queue task with the same application key and extension key which also had a data key 'initial_data' that matches our current value (true), do not duplicate the task, but replace it instead. If instead we had this \IPS\Task::queue( 'myapp', 'ExtensionKey', array( 'initial_data' => false ), 3, array( 'initial_data' ) ); \IPS\Task::queue( 'myapp', 'ExtensionKey', array( 'initial_data' => true ), 3, array( 'initial_data' ) ); Then we would again have two tasks, because the 'initial_data' value from the first and second tasks does not match. Queue tasks are used extensively throughout the Invision Community software and are a great way to offload intensive processing to the background so that administrators can move on with their tasks and not have to wait for a mundane unimportant task to finish.
  23. What it is Invision Community 4.2 introduces a powerful profile completion feature which can guide users into completing additional parts of their user profile following their registration. Your application can add additional steps that the administrator can enable for profile completion as well. How to use Upon creating a ProfileSteps extension, the class template will contain 9 methods. /** * Available Actions to complete steps * * @return array array( 'key' => 'lang_string' ) */ public static function actions() { return array( ... ); } The actions() method should return the available top-level actions that can be specified in the returned array as key => language string. /** * Available sub actions to complete steps * * @return array array( 'key' => 'lang_string' ) */ public static function subActions() { return array( ... ); } The subActions() method can be used to return the available sub-actions available. As an example, completing custom profile fields might be a top-level action, and then each custom profile field would be an available sub-action. The format of the array should be array( 'action' => array( 'subaction_key' => 'subaction_language_string' ) ) /** * Can the actions have multiple choices? * * @param string $action Action key (basic_profile, etc) * @return boolean */ public static function actionMultipleChoice( $action ) { return FALSE; } The actionMultipleChoice() method is designed to take the action key and determine if there are multiple choices available to the action. For example, linking social profiles does not carry with it multiple choices - you have either done it or you haven't. Supplying custom profile field information, however, will allow different values to be supplied. More often than not, you will simply want to return TRUE from this method. /** * Can be set as required? * * @return array * @note This is intended for items which have their own independent settings and dedicated enable pages, such as MFA and Social Login integration */ public static function canBeRequired() { return array( 'action' ); } As the note indicates, if your actions can be set as required, they should be returned as values in the array this method returns. Social login integration has its own configuration pages, so it cannot be set as required, however you can on the other hand require a user to supply their birthday, or to supply custom field details. /** * Has a specific step been completed? * * @param \IPS\Member\ProfileStep $step The step to check * @param \IPS\Member|NULL $member The member to check, or NULL for currently logged in * @return bool */ public function completed( \IPS\Member\ProfileStep $step, \IPS\Member $member = NULL ) { return FALSE; } The completed() method is called to determine if a specified member has completed a profile step that has been set up, returning FALSE to indicate the step has not been completed or TRUE to indicate it has been completed. For birthday, as an example, if the member has a birthday set then the step would be considered completed. /** * Format Form Values * * @param array $values The values from the form * @param \IPS\Member $member The member * @param \IPS\Helpers\Form $form The form * @return void */ public static function formatFormValues( $values, &$member, &$form ) { } When a member submits the profile completion form during their first visit, the formatFormValues() method is called in order to allow the ProfileSteps extension correctly format the submitted value. $member is passed by reference, so you can set $member->x properties, and save() will be called against the member automatically later, centrally. If any errors are encountered, you can set $form->error (or set an error on an individual form element) and return from the method. /** * Action URL * * @param string $action The action * @param \IPS\Member|NULL $member The member, or NULL for currently logged in * @return \IPS\Http\Url */ public function url( $action, \IPS\Member $member = NULL ) { return \IPS\Http\Url::internal( ... ); } If the user opts not to complete their profile and then later wishes to complete a step, the user will be sent to the url returned by url(). /** * Post ACP Save * * @param \IPS\Member\ProfileStep $step The step * @param array $values Form Values * @return void */ public function postAcpSave( \IPS\Member\ProfileStep $step, array $values ) { } Your extension can take action following a changed profile completion step. For example, if a custom profile field is set as required then the custom profile fields ProfileSteps extension will set the required flag for the field. /** * Wizard Steps * * @param \IPS\Member $member Member or NULL for currently logged in member * @return array */ public static function wizard( \IPS\Member $member = NULL ) { return array( ... ); } The wizard() method accepts one parameter (the member completing the wizard) and must return an array of callback functions to be added to the profile completion wizard. Generally speaking, each step should return a form with one or more form fields which will allow the member to complete the profile completion step. An example to review is the profile completion step for custom profile fields. /** * Wizard Steps * * @param \IPS\Member $member Member or NULL for currently logged in member * @return array */ public static function wizard( \IPS\Member $member = NULL ) { $member = $member ?: \IPS\Member::loggedIn(); $wizards = array(); foreach( \IPS\Member\ProfileStep::loadAll() AS $step ) { if ( $step->completion_act === 'profile_fields' AND ! $step->completed( $member ) ) { $wizards[ $step->key ] = function( $data ) use ( $member, $step ) { $form = new \IPS\Helpers\Form( 'profile_profile_fields_' . $step->id, 'profile_complete_next' ); foreach( $step->subcompletion_act as $item ) { $id = \substr( $item, 12 ); $field = \IPS\core\ProfileFields\Field::load( $id ); $form->add( $field->buildHelper() ); } if ( $values = $form->values() ) { static::formatFormValues( $values, $member, $form ); $member->save(); return $values; } return $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'profileCompleteTemplate' ), $step ); }; } } if ( count( $wizards ) ) { return $wizards; } } If the supplied member parameter is NULL, then we are working with the currently logged in member. Next we loop over all of the profile completion steps and look for any that match this type, and that have not been completed. If we find one, we start a new wizard step (which is a callback function that accepts a single $data parameter). Within this callback function we start a new form, loop over any subactions (individual custom fields in this case) and add those form elements to the form, which we then output. As we do elsewhere, we also check if the form is submitted and if so, we save the submitted values.
  24. bfarber

    core/Profile

    What it is A Profile extension allows you to add a tab to user profiles on the front end. Blog and Gallery use this extension to show the user's blogs and the user's gallery albums, respectively, on separate tabs on the user's profile. How to use The extension defines 3 methods and a property by default /** * Member */ protected $member; /** * Constructor * * @param \IPS\Member $member Member whose profile we are viewing * @return void */ public function __construct( \IPS\Member $member ) { $this->member = $member; } The constructor accepts an instance of \IPS\Member and stores it in a property. You can perform any other essential setup here as well, if needed. /** * Is there content to display? * * @return bool */ public function showTab() { return TRUE; } The showTab() method returns a boolean value to indicate if the tab should be shown or not. This can be used in order to only show tabs if the user has any actual data to be shown. For instance, the gallery albums tab is not shown if the user has not created any gallery albums. /** * Display * * @return string */ public function render() { return 'content to display'; } Finally, the render() method returns the content to display when the tab is clicked on, as raw HTML output. You can use \IPS\Helpers\Table instances here to facilitate the display of data, or output completely custom HTML. If you need to output any CSS from your application, you can do that as well here.
  25. What it does The Permissions extension type allows your application to add permission settings (typically permission matrix instances) to the edit permissions form when viewing groups in the AdminCP. While administrators can visit each individual area presently to edit group permissions, they can also visit the Manage Groups page in the AdminCP and click the padlock icon next to a group to edit all of the group's permissions throughout the suite from one page. How to use The extension defines a single method /** * Get node classes * * @return array */ public function getNodeClasses() { return array(); } This method should return an array of classes as the keys, and the values being either NULL or a callable function. The callback function will accept two parameters: the first is an array of current permissions, and the second is the \IPS\Member\Group being edited. The forums application defines the method as follows: /** * Get node classes * * @return array */ public function getNodeClasses() { return array( 'IPS\forums\Forum' => function( $current, $group ) { $rows = array(); foreach( \IPS\forums\Forum::roots( NULL ) AS $root ) { \IPS\forums\Forum::populatePermissionMatrix( $rows, $root, $group, $current ); } return $rows; } ); }
×
×
  • Create New...