├── .github └── FUNDING.yml ├── .gitignore ├── .svnignore ├── README.md ├── assets ├── scripts │ └── admin.js └── scss │ ├── admin.css │ └── admin.scss ├── bin ├── index.php └── wp-cli.php ├── composer.json ├── composer.lock ├── index.php ├── package-lock.json ├── package.json ├── readme.txt ├── src ├── Admin │ ├── Admin.php │ ├── css │ │ └── admin.css │ ├── index.php │ └── js │ │ └── admin.js ├── Feature.php ├── Features.php ├── Features │ ├── Document.php │ ├── LiveSearch │ │ ├── LiveSearch.php │ │ └── assets │ │ │ ├── css │ │ │ └── live-search.css │ │ │ └── js │ │ │ ├── live-search.min.js │ │ │ └── src │ │ │ └── live-search.js │ ├── Synonym.php │ ├── Woocommerce.php │ └── index.php ├── RediSearch │ ├── Client.php │ ├── Index.php │ └── Search.php ├── RedisRaw │ ├── AbstractRedisRawClient.php │ ├── PredisAdapter.php │ ├── RedisRawClientInterface.php │ └── index.php ├── Settings.php ├── Utils │ ├── MsOfficeParser.php │ └── index.php ├── WpRediSearch.php └── index.php ├── vendor ├── asika │ └── pdf2text │ │ ├── .gitignore │ │ ├── LINCENSE.md │ │ ├── README.md │ │ ├── composer.json │ │ ├── phpunit.xml.dist │ │ ├── src │ │ └── Pdf2text.php │ │ └── test │ │ ├── Pdf2textTest.php │ │ └── test.pdf ├── autoload.php └── composer │ ├── ClassLoader.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_real.php │ ├── autoload_static.php │ └── installed.json └── wp-redisearch.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [foadyousefi] 4 | custom: ['https://www.paypal.me/foadyousefi'] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.idea/ 3 | bin 4 | node_modules 5 | vendor/predis/predis/.git 6 | vendor/predis/predis/tests -------------------------------------------------------------------------------- /.svnignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | .gitignore 4 | node_modules 5 | package.json 6 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RediSearch 2 | 3 | **IMPORTANT**: The latest version of this plugin supports RediSearch version 2.0 (or higher) which runs on Redis 6.0 (or higher). If you have version prior to 2.0, please use this plugins 0.2.7. 4 | 5 | Redisearch implements a search engine on top of Redis. It has lots of advanced features, like exact phrase matching and numeric filtering for text queries, that are nearly not possible or inefficient with mysql search queries. 6 | 7 | Here you find a list of RediSearch features included in the plugin: 8 | 9 | **Search**: Instantly find the content you’re looking for. The first time. 10 | 11 | **Scoring fields differently**: Give different score to different fields. For example higher score to product name and number than its description. 12 | 13 | **Fuzzy Search**: Don't worry about visitors misspelling. 14 | 15 | **Autosuggest**: Adds a suggestion string to an auto-complete suggestion dictionary. 16 | 17 | **Synonyms**: RediSearch supports synonyms, that is searching for synonyms words defined by the synonym data structure. 18 | 19 | ### Existing features 20 | 21 | * WooCommerce: Index and search through most of existing products meta data. 22 | * Document: Index content of binary files such as pdf, word, excel and powerpoint. 23 | * Synonym: Adding synonym groups is simple. Just add each comma separated group on a new line in synonym settings and done. 24 | * Live search (aka autosuggest): Search as you type regardless of misspelling. 25 | 26 | 27 | ### Installation 28 | 1. First, you will need to properly install and configure [Redis](https://redis.io/topics/quickstart) and [RediSearch](https://oss.redislabs.com/redisearch/Quick_Start/). 29 | 2. Activate the plugin in WordPress. 30 | 3. In the RediSearch settings page, input your Redis host and port and do the configuration. 31 | 4. In RediSearch dashboard page, click on Index button. 32 | 5. Let you visitors enjoy. 33 | 34 | Optionaly, you can pass settings in your wp-config.php file like following. If you are using [Redis Object Cache](https://wordpress.org/plugins/redis-cache/) plugin, these settings may already exist. 35 | 36 | ``` 37 | define('WP_REDIS_HOST', '127.0.0.1'); 38 | define('WP_REDIS_PORT', '6379'); 39 | define('WP_REDIS_PASSWORD', 'your-password'); 40 | define('WP_REDIS_INDEX_NAME', 'indexName'); 41 | define('WP_REDIS_SCHEME', 'connectionScheme'); 42 | ``` 43 | 44 | 45 | ### Frequently Asked Questions 46 | 47 | #### What is wrong with WordPress native search? 48 | 49 | Although mySql is a great database to storing relational data, It acts very poor on search queries and you must forget about some features like fuzzy matching and synonyms. 50 | 51 | #### How Redisearch is compared to ElasticSearch? 52 | 53 | Yes, ElasticSearch is a great search engine and it has very good performance compared to mySql. But RediSearch has almost 5 to 10 times better performance and also its way easier to create index, sync your data and send query requests. 54 | 55 | ### Changelog 56 | 57 | ##### 0.3.4 58 | * **FIXED**: Fixed server connection issue caused by last release 59 | * **Added**: Option to select default language 60 | * **Added**: Added a filter to force RediSearch to replave WP_Query 61 | 62 | ##### 0.3.3 63 | * **Added**: Ability to connect via UNIX sockets 64 | 65 | ##### 0.3.2 66 | * **FIXED**: Fix issue while creating/updating/deleting posts 67 | 68 | ##### 0.3.1 69 | * **FIXED**: Fix an issue with live search 70 | 71 | ##### 0.3.0 72 | * **Updated**: Implemented RediSearch version 2.0 73 | 74 | ##### 0.2.7 75 | * **FIXED**: Fix some bugs. 76 | 77 | ##### 0.2.6 78 | * **FIXED**: Fix a bug preventing saving og feature settings. 79 | 80 | ##### 0.2.5 81 | * **Added**: Get index name option from wp-config 82 | * **Added**: Option for disabling stop words 83 | * **Added**: Adding a comma separated and user defined list of stop words 84 | * **Fixed**: Make search fields parent elements position to 'relative' so auto suggestion will appear in correct place 85 | 86 | ##### 0.2.4 87 | * **Fix**: Fix admin js and css files enqueue directory name case issue 88 | 89 | ##### 0.2.3 90 | * **Added**: Added password option. 91 | * **Added**: Ability to set redis server configurations in wp-config.php file. 92 | 93 | ##### 0.2.2 94 | * **Added**: Document feature for indexing binary file contents 95 | * **Added**: Filter hook 'wp_redisearch_indexable_post_status' to manipulate indexable post status 96 | * **Added**: Filter hook 'wp_redisearch_before_admin_wp_query' Applies to main query args. This is mainly for showing number of indexable posts 97 | * **Added**: Filter hook 'wp_redisearch_after_admin_wp_query' Applies after main query and recieves args and the $query object. This is mainly for showing number of indexable posts 98 | * **Added**: Filter hook 'wp_redisearch_before_index_wp_query' Applies to main query args. This hook is for manipulating arguments for indexing process 99 | * **Added**: Filter hook 'wp_redisearch_after_index_wp_query' Applies after main query and recieves args and the $query object. This hook is for manipulating $query object used for indexing posts 100 | 101 | ##### 0.2.1 102 | * **Added**: WooCommerce support added as Feature 103 | * **Fixed**: Return option values if empty string stores in database 104 | * **Fixed**: Fix incorrect link to settings page 105 | * **Fixed**: Fix harcoded index name in WP-CLI INFO command 106 | * **Added**: filter hook 'wp_redisearch_indexable_temrs' to manipulate indexable terms list 107 | * **Added**: filter hook 'wp_redisearch_indexable_post_types' to manipulate indexable post types 108 | 109 | ##### 0.2.0 110 | * **Added**: WP-CLI support 111 | * **Added**: Register and activating of Features 112 | * **Added**: filter hook 'wp_redisearch_indexable_meta_keys' to add extra meta keys to the index 113 | * **Added**: filter hook 'wp_redisearch_indexable_meta_schema' to manupulate type of post meta fields (default is text) 114 | * **Added**: action hook 'wp_redisearch_after_post_indexed' fires after posts indexed from the main index command 115 | * **Added**: action hook 'wp_redisearch_after_post_published' fires after a post have been published 116 | * **Added**: action hook 'wp_redisearch_after_post_deleted' fires after a post have been deleted 117 | * **Added**: action hook 'wp_redisearch_after_index_created' fires after main index created 118 | * **Added**: action hook 'wp_redisearch_settings_indexing_fields' fires after settings fields inside indexing options page 119 | * **Fixed:** Fix indexing posts on publish/update 120 | 121 | ##### 0.1.1 122 | * Use default value for settings if not set in settings 123 | 124 | ##### 0.1.0 125 | * Initial plugin 126 | -------------------------------------------------------------------------------- /assets/scripts/admin.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $(document).ready(function () { 3 | 4 | var features = $('.wprds-feature') 5 | var indexingOptions = $('.indexing-options') 6 | var startIndexing = $('.start-indexing') 7 | var resumeIndexing = $('.resume-indexing') 8 | 9 | features.on( 'click', '.save-settings', function( event ) { 10 | event.preventDefault(); 11 | var $this = $( this ) 12 | if ( $this.hasClass( 'disabled' ) ) { 13 | return; 14 | } 15 | var feature = $this.attr( 'data-feature' ); 16 | var $feature = features.find( '.wprds-feature-' + feature ); 17 | 18 | var settings = {}; 19 | 20 | var $settings = $feature.find('.setting-field'); 21 | 22 | $settings.each(function() { 23 | var type = $( this ).attr( 'type' ); 24 | var name = $( this ).attr( 'data-field-name' ); 25 | var value = $( this ).attr( 'value' ); 26 | if ( 'radio' === type ) { 27 | if ( this.checked ) { 28 | settings[ name ] = value; 29 | } 30 | } else { 31 | settings[ name ] = value; 32 | } 33 | }); 34 | console.log(settings) 35 | $this.parent().addClass( 'saving' ); 36 | 37 | $.ajax( { 38 | method: 'post', 39 | url: ajaxurl, 40 | data: { 41 | action: 'wp_redisearch_save_feature', 42 | feature: feature, 43 | nonce: wpRds.nonce, 44 | settings: settings 45 | } 46 | } ).done( function( response ) { 47 | setTimeout( function() { 48 | $this.parent().removeClass( 'saving' ); 49 | 50 | if ( '1' === settings.active ) { 51 | $feature.addClass( 'feature-active' ); 52 | } else { 53 | $feature.removeClass( 'feature-active' ); 54 | } 55 | 56 | if ( response.data.reindex ) { 57 | dropIndex(); 58 | } 59 | }, 700 ); 60 | } ).error( function() { 61 | setTimeout( function() { 62 | $feature.removeClass( 'saving' ); 63 | $feature.removeClass( 'feature-active' ); 64 | }, 700 ); 65 | } ); 66 | }); 67 | 68 | if (indexingOptions.attr('data-num-docs') == indexingOptions.attr('data-num-posts')) { 69 | resumeIndexing.css('display', 'none') 70 | } 71 | 72 | startIndexing.click(function (e) { 73 | e.preventDefault() 74 | startIndexing.css('display', 'none') 75 | dropIndex() 76 | }) 77 | 78 | 79 | resumeIndexing.click(function (e) { 80 | e.preventDefault() 81 | resumeIndexing.css('display', 'none') 82 | addToIndex() 83 | }) 84 | 85 | function dropIndex() { 86 | let postData = { 87 | action: 'wp_redisearch_drop_index' 88 | } 89 | jQuery.ajax({ 90 | data: postData, 91 | type: "post", 92 | url: wpRds.ajaxUrl 93 | }) 94 | .done(function (res) { 95 | if (res.data) { 96 | addToIndex() 97 | } 98 | }) 99 | .error(function (err) { 100 | console.log(err) 101 | }) 102 | } 103 | 104 | function addToIndex() { 105 | let postData = { 106 | action: 'wp_redisearch_add_to_index' 107 | } 108 | jQuery.ajax({ 109 | data: postData, 110 | type: "post", 111 | url: wpRds.ajaxUrl 112 | }) 113 | .done(function (res) { 114 | if (res.data.offset < res.data.found_posts) { 115 | updateProgressBar(res.data.offset, res.data.found_posts) 116 | addToIndex() 117 | } else { 118 | updateProgressBar(res.data.found_posts, res.data.found_posts) 119 | startIndexing.css('display', 'inline-block') 120 | writeToDisk() 121 | } 122 | }) 123 | .error(function (err) { 124 | console.log(err) 125 | }) 126 | } 127 | 128 | function writeToDisk() { 129 | let postData = { 130 | action: 'wp_redisearch_write_to_disk' 131 | } 132 | jQuery.ajax({ 133 | data: postData, 134 | type: "post", 135 | url: wpRds.ajaxUrl 136 | }) 137 | .done(function (res) { 138 | console.log('Data successfully written to the disk ', res.data) 139 | }) 140 | .error(function (err) { 141 | console.log('Error writing to the disk ', err) 142 | }) 143 | } 144 | 145 | function updateProgressBar(numDocs, numPosts) { 146 | let pBar = $("#indexBar") 147 | let statNumDoc = $('#statNumDoc') 148 | let statNumPosts = $('#statNumPosts') 149 | statNumDoc.text(numDocs) 150 | statNumPosts.text(numPosts) 151 | let width = (numDocs / numPosts) * 100 152 | pBar.css('width', width + '%') 153 | } 154 | 155 | renderProgressBar() 156 | function renderProgressBar() { 157 | let pBar = $("#indexBar") 158 | let numDocs = pBar.attr('data-num-docs') 159 | let numPosts = pBar.attr('data-num-posts') 160 | let width = (numDocs / numPosts) * 100 161 | pBar.css('width', width + '%') 162 | } 163 | 164 | }); 165 | } )( jQuery ); 166 | -------------------------------------------------------------------------------- /assets/scss/admin.css: -------------------------------------------------------------------------------- 1 | .wprds-form-wraper .wprds-field { 2 | position: relative; 3 | padding: 14px 12px 15px; 4 | background: #fff; 5 | border-width: 1px 1px 0 0; 6 | border-style: solid; 7 | border-color: #e5e5e5; } 8 | .wprds-form-wraper .wprds-field h3 { 9 | padding: 0; 10 | font-size: 28px; 11 | margin: 0; 12 | line-height: 1; 13 | font-weight: 200; } 14 | .wprds-form-wraper .wprds-field .label { 15 | font-weight: 600; 16 | margin-bottom: 10px; } 17 | .wprds-form-wraper .wprds-field.text-field input, 18 | .wprds-form-wraper .wprds-field.textarea textarea { 19 | display: block; 20 | width: 100%; 21 | margin-bottom: 10px; } 22 | .wprds-form-wraper .wprds-field .desc { 23 | color: #888; } 24 | 25 | .wprds-form-wraper .form-actions { 26 | flex: 1; 27 | margin-left: 10px; } 28 | 29 | .wprds-wrapper-grid { 30 | margin-left: 8px; 31 | margin-right: 8px; 32 | margin-top: 10px; } 33 | .wprds-wrapper-grid .postbox { 34 | float: left; 35 | clear: left; 36 | width: 50%; 37 | margin: 0 0 16px; } 38 | @media only screen and (max-width: 850px) { 39 | .wprds-wrapper-grid .postbox { 40 | margin-left: 0 !important; 41 | margin-right: 0 !important; 42 | width: 100%; } } 43 | .wprds-wrapper-grid .postbox .inside { 44 | margin-bottom: 0; } 45 | .wprds-wrapper-grid .postbox:nth-child(odd) { 46 | margin-left: -8px; } 47 | .wprds-wrapper-grid .postbox:nth-child(even) { 48 | float: right; 49 | clear: right; 50 | margin-right: -8px; } 51 | .wprds-wrapper-grid .hndle { 52 | font-size: 14px; 53 | padding: 8px 12px; 54 | margin: 0; 55 | line-height: 1.4; } 56 | .wprds-wrapper-grid .wprds-fields { 57 | border: #ebebeb solid 1px; 58 | background: #fafafa; 59 | border-radius: 3px; } 60 | .wprds-wrapper-grid .action-wrap { 61 | text-align: right; 62 | margin-top: 20px; } 63 | .wprds-wrapper-grid .reindex-required { 64 | line-height: 2; } 65 | .wprds-wrapper-grid .feature-fields { 66 | display: flex; } 67 | .wprds-wrapper-grid .feature-fields .field-name { 68 | flex: 1; } 69 | .wprds-wrapper-grid .feature-fields .input-wrap { 70 | flex: 3; } 71 | .wprds-wrapper-grid .saving:before, 72 | .wprds-wrapper-grid .loading:before { 73 | content: ' '; 74 | margin-right: 1.4em; 75 | top: 4px; 76 | border-radius: 50%; 77 | width: 8px; 78 | height: 8px; 79 | display: inline-block; 80 | font-size: 7px; 81 | position: relative; 82 | text-indent: -9999em; 83 | border-top: 5px solid #ccc; 84 | border-right: 5px solid #ccc; 85 | border-bottom: 5px solid #ccc; 86 | border-left: 5px solid #999; 87 | transform: translateZ(0); 88 | animation: spinner 1.1s infinite linear; } 89 | .wprds-wrapper-grid .feature-active:before { 90 | content: ''; 91 | position: absolute; 92 | left: 0; 93 | top: 0; 94 | background: #ade5b4; 95 | width: 6px; 96 | height: 100%; } 97 | 98 | @keyframes spinner { 99 | 0% { 100 | -webkit-transform: rotate(0deg); 101 | transform: rotate(0deg); } 102 | 100% { 103 | -webkit-transform: rotate(360deg); 104 | transform: rotate(360deg); } } 105 | 106 | .wprds-feature-notice { 107 | padding: 10px; } 108 | -------------------------------------------------------------------------------- /assets/scss/admin.scss: -------------------------------------------------------------------------------- 1 | .wprds-form-wraper { 2 | .wprds-field { 3 | position: relative; 4 | padding: 14px 12px 15px; 5 | background: #fff; 6 | border-width: 1px 1px 0 0; 7 | border-style: solid; 8 | border-color: #e5e5e5; 9 | 10 | h3 { 11 | padding: 0; 12 | font-size: 28px; 13 | margin: 0; 14 | line-height: 1; 15 | font-weight: 200; 16 | } 17 | 18 | .label { 19 | font-weight: 600; 20 | margin-bottom: 10px; 21 | } 22 | 23 | &.text-field input, 24 | &.textarea textarea { 25 | display: block; 26 | width: 100%; 27 | margin-bottom: 10px; 28 | } 29 | 30 | .desc { 31 | color: #888; 32 | } 33 | } 34 | 35 | .form-actions { 36 | flex: 1; 37 | margin-left: 10px; 38 | } 39 | } 40 | 41 | .wprds-wrapper-grid { 42 | margin-left: 8px; 43 | margin-right: 8px; 44 | margin-top: 10px; 45 | 46 | .postbox { 47 | float: left; 48 | clear: left; 49 | width: 50%; 50 | margin: 0 0 16px; 51 | 52 | @media only screen and (max-width: 850px) { 53 | margin-left: 0 !important; 54 | margin-right: 0 !important; 55 | width: 100%; 56 | } 57 | 58 | .inside { 59 | margin-bottom: 0; 60 | } 61 | } 62 | 63 | .postbox:nth-child(odd) { 64 | margin-left: -8px; 65 | } 66 | 67 | .postbox:nth-child(even) { 68 | float: right; 69 | clear: right; 70 | margin-right: -8px; 71 | } 72 | 73 | .hndle { 74 | font-size: 14px; 75 | padding: 8px 12px; 76 | margin: 0; 77 | line-height: 1.4; 78 | } 79 | 80 | .wprds-fields { 81 | border: #ebebeb solid 1px; 82 | background: #fafafa; 83 | border-radius: 3px; 84 | } 85 | 86 | .action-wrap { 87 | text-align: right; 88 | margin-top: 20px; 89 | } 90 | 91 | .reindex-required { 92 | line-height: 2; 93 | } 94 | 95 | .feature-fields { 96 | display: flex; 97 | 98 | .field-name { 99 | flex: 1; 100 | } 101 | 102 | .input-wrap { 103 | flex: 3; 104 | } 105 | } 106 | 107 | .saving, 108 | .loading { 109 | &:before { 110 | content: ' '; 111 | margin-right: 1.4em; 112 | top: 4px; 113 | border-radius: 50%; 114 | width: 8px; 115 | height: 8px; 116 | display: inline-block; 117 | font-size: 7px; 118 | position: relative; 119 | text-indent: -9999em; 120 | border-top: 5px solid #ccc; 121 | border-right: 5px solid #ccc; 122 | border-bottom: 5px solid #ccc; 123 | border-left: 5px solid #999; 124 | transform: translateZ(0); 125 | animation: spinner 1.1s infinite linear; 126 | } 127 | } 128 | 129 | .feature-active:before { 130 | content: ''; 131 | position: absolute; 132 | left: 0; 133 | top: 0; 134 | background: #ade5b4; 135 | width: 6px; 136 | height: 100%; 137 | } 138 | } 139 | 140 | @keyframes spinner { 141 | 0% { 142 | -webkit-transform: rotate(0deg); 143 | transform: rotate(0deg); 144 | } 145 | 100% { 146 | -webkit-transform: rotate(360deg); 147 | transform: rotate(360deg); 148 | } 149 | } 150 | 151 | .wprds-feature-notice { 152 | padding: 10px; 153 | } 154 | -------------------------------------------------------------------------------- /bin/index.php: -------------------------------------------------------------------------------- 1 | _connect_check(); 42 | 43 | try { 44 | $client = Setup::connect(); 45 | $index_name = Settings::indexName(); 46 | $info = $client->rawCommand('FT.INFO', [ $index_name ]); 47 | 48 | WP_CLI::log( WP_CLI::colorize( '%G' . __( '===== Info =====', 'wp-redisearch' ) . '%N' ) ); 49 | foreach ($info as $key => $value) { 50 | if ( gettype( $value ) == 'string' || gettype( $value ) == 'object' ) { 51 | WP_CLI::line( $value ); 52 | } elseif ( gettype( $value ) == 'array' ) { 53 | foreach ($value as $key => $sub_value) { 54 | if ( gettype( $sub_value ) == 'string' || gettype( $sub_value ) == 'object' ) { 55 | WP_CLI::line( $sub_value ); 56 | } elseif ( gettype( $sub_value ) == 'array' ) { 57 | $child_value_print = ''; 58 | foreach ($sub_value as $key => $child_value) { 59 | $child_value_print .= ' ' . $child_value; 60 | } 61 | WP_CLI::line( ' -' . $child_value_print ); 62 | } 63 | } 64 | } 65 | } 66 | } catch (\Exception $e) { 67 | if ( isset( $e ) ) { 68 | WP_CLI::error( implode( "\n", $e ) ); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Creates the index with the settings provided. 75 | * 76 | * @subcommand create-index 77 | * @since 0.2.0 78 | * 79 | * @param array $args 80 | * @param array $assoc_args 81 | */ 82 | public function create_index( $args, $assoc_args ) { 83 | $this->_connect_check(); 84 | 85 | // First of all, deletes index 86 | $this->drop_index( $args, $assoc_args ); 87 | 88 | $index = new Index( WPRedisearch::$client ); 89 | $result = $index->create(); 90 | 91 | if ( $result ) { 92 | WP_CLI::success( __( 'Index created', 'wp-redisearch' ) ); 93 | } else { 94 | WP_CLI::error( __( 'Index creating failed', 'wp-redisearch' ) ); 95 | } 96 | } 97 | 98 | /** 99 | * Drop (deletes) current index. 100 | * Warning! This will remove your existing index for the entire site. 101 | * 102 | * @subcommand drop-index 103 | * @since 0.2.0 104 | * 105 | * @param array $args 106 | * @param array $assoc_args 107 | */ 108 | public function drop_index( $args, $assoc_args ) { 109 | $this->_connect_check(); 110 | 111 | WP_CLI::line( __( 'Dropping index...', 'wp-redisearch' ) ); 112 | $index = new Index( WPRedisearch::$client ); 113 | $result = $index->drop(); 114 | if ( $result ) { 115 | WP_CLI::success( __( 'Index dropped', 'wp-redisearch' ) ); 116 | } else { 117 | WP_CLI::error( __( 'Index drop failed', 'wp-redisearch' ) ); 118 | } 119 | 120 | } 121 | 122 | /** 123 | * Index all posts for the site 124 | * 125 | * ## OPTIONS 126 | * 127 | * [--setup] 128 | * Drops existing index, creates new index then indexes the posts 129 | * 130 | * [--posts-per-page] 131 | * Sets number of posts to be indexed in a batch 132 | * 133 | * [--offset] 134 | * Bypasses passed number of posts, then indexes them 135 | * 136 | * [--post-type] 137 | * Sets post type to be indexed 138 | * 139 | * [--post-ids] 140 | * Only indexes posts from comma separated list 141 | * 142 | * [--write-to-disk] 143 | * If this option passed, created index, will be written to the disk 144 | * This is usefull in case there was some sort of network issues, you don't need to re-index the whole website 145 | * 146 | * ## EXAMPLES 147 | * 148 | * $ wp redisearch index --setup 149 | * Drops, creates and indexes 150 | * 151 | * $ wp redisearch index --posts-per-page=20 152 | * Indexes only 20 posts at a time 153 | * 154 | * $ wp redisearch index --offset=30 155 | * Starts indexing from post 31 156 | * 157 | * $ wp redisearch index --post-type=page 158 | * Only indexes pages 159 | * 160 | * $ wp redisearch index --post-ids=10,11,12,14 161 | * Only indexes posts with which have given ids 162 | * 163 | * $ wp redisearch index --write-to-disk 164 | * After finished with indexing, writes the index into the disk as a file 165 | * 166 | * @synopsis [--setup] [--posts-per-page] [--offset] [--post-type] [--post-ids] [--write-to-disk] 167 | * 168 | * @param array $args 169 | * @since 0.2.0 170 | * @param array $assoc_args 171 | */ 172 | public function index( $args, $assoc_args ) { 173 | global $wp_actions; 174 | 175 | $this->_connect_check(); 176 | 177 | if ( ! empty( $assoc_args['posts-per-page'] ) ) { 178 | $assoc_args['posts-per-page'] = absint( $assoc_args['posts-per-page'] ); 179 | } else { 180 | $assoc_args['posts-per-page'] = Settings::get( 'wp_redisearch_indexing_batches', 50 ); 181 | } 182 | 183 | if ( ! empty( $assoc_args['offset'] ) ) { 184 | $assoc_args['offset'] = absint( $assoc_args['offset'] ); 185 | } else { 186 | $assoc_args['offset'] = 0; 187 | } 188 | 189 | if ( empty( $assoc_args['post-type'] ) ) { 190 | $assoc_args['post-type'] = null; 191 | } 192 | 193 | if ( empty( $assoc_args['write-to-disk'] ) ) { 194 | $assoc_args['write-to-disk'] = null; 195 | } 196 | 197 | $total_indexed = 0; 198 | 199 | //Hold original wp_actions 200 | $this->temporary_wp_actions = $wp_actions; 201 | 202 | set_transient( 'wp_redisearch_wpcli_indexing', true, $this->transient_expiration ); 203 | 204 | timer_start(); 205 | 206 | // This clears away dashboard notifications 207 | update_option( 'wp_redisearch_last_indexed', time() ); 208 | delete_option( 'wp_redisearch_need_upgrade_index' ); 209 | 210 | // Run setup if flag was passed. 211 | if ( isset( $assoc_args['setup'] ) && $assoc_args['setup'] === true ) { 212 | $this->create_index( $args, $assoc_args ); 213 | } 214 | 215 | WP_CLI::log( WP_CLI::colorize( '%N' . __( 'Indexing posts... ', 'wp-redisearch' ) . '%N' ) ); 216 | 217 | $result = $this->_index_helper( $assoc_args ); 218 | 219 | WP_CLI::log( sprintf( __( 'Number of posts indexed on site: %d', 'wp-redisearch' ), $result['indexed'] ) ); 220 | 221 | if ( !empty( $result['errors'] ) ) { 222 | WP_CLI::error( sprintf( __( 'Number of post index errors on site: %d', 'wp-redisearch' ), count( $result['errors'] ) ) ); 223 | } 224 | 225 | WP_CLI::log( WP_CLI::colorize( '%Y' . __( 'Total time elapsed: ', 'wp-redisearch' ) . '%N' . timer_stop() ) ); 226 | 227 | delete_transient( 'wp_redisearch_wpcli_indexing' ); 228 | 229 | WP_CLI::success( __( 'Done!', 'wp-redisearch' ) ); 230 | } 231 | 232 | /** 233 | * Helper method for indexing posts 234 | * 235 | * @param array $args 236 | * 237 | * @since 0.2.0 238 | * @return array 239 | */ 240 | private function _index_helper( $args ) { 241 | $indexed = 0; 242 | $errors = array(); 243 | 244 | $posts_per_page = Settings::get( 'wp_redisearch_indexing_batches', 50 ); 245 | 246 | if ( ! empty( $args['posts-per-page'] ) ) { 247 | $posts_per_page = absint( $args['posts-per-page'] ); 248 | } 249 | 250 | $offset = 0; 251 | 252 | if ( ! empty( $args['offset'] ) ) { 253 | $offset = absint( $args['offset'] ); 254 | } 255 | 256 | $post_types = Settings::get( 'wp_redisearch_post_types', 'post' ); 257 | 258 | if ( ! empty( $args['post-type'] ) ) { 259 | $post_types = explode( ',', $args['post-type'] ); 260 | $post_types = array_map( 'trim', $post_types ); 261 | } elseif ( isset( $post_types ) && !empty( $post_types ) ) { 262 | $post_types = array_keys( $post_types ); 263 | } elseif ( !isset( $post_types ) || empty( $post_types ) ) { 264 | $post_types = array( 'post' ); 265 | } 266 | 267 | /** 268 | * Modify indexable post types 269 | * 270 | * @since 0.2.1 271 | * @param array $post_types Default terms list 272 | * @return array $post_types Modified taxobomy terms list 273 | */ 274 | $post_types = apply_filters( 'wp_redisearch_indexable_post_types', $post_types ); 275 | 276 | $post_in = null; 277 | 278 | if ( ! empty( $args['post-ids'] ) ) { 279 | $post_in = explode( ',', $args['post-ids'] ); 280 | $post_in = array_map( 'trim', $post_in ); 281 | $post_in = array_map( 'absint', $post_in ); 282 | $post_in = array_filter( $post_in ); 283 | 284 | $posts_per_page = count($post_in); 285 | } 286 | 287 | /** 288 | * Create WP_Query here and reuse it in the loop to avoid high memory consumption. 289 | */ 290 | $query = new WP_Query(); 291 | 292 | $index = new Index( WPRedisearch::$client ); 293 | 294 | while ( true ) { 295 | 296 | $default_args = Settings::query_args(); 297 | $default_args['post_type'] = $post_types; 298 | $default_args['posts_per_page'] = $posts_per_page; 299 | $default_args['offset'] = $offset; 300 | 301 | $args = apply_filters( 'wp_redisearch_posts_args', $default_args); 302 | 303 | if ( $post_in ) { 304 | $args['post__in'] = $post_in; 305 | } 306 | 307 | $query->query( $args ); 308 | 309 | if ( $query->have_posts() ) { 310 | 311 | while ( $query->have_posts() ) { 312 | $query->the_post(); 313 | 314 | $indexing_options = array(); 315 | $index_name = Settings::indexName(); 316 | $indexing_options['language'] = apply_filters( 'wp_redisearch_index_language', 'english', get_the_ID() ); 317 | $indexing_options['fields'] = $index->prepare_post( get_the_ID() ); 318 | 319 | $result = $index->addPosts($index_name, get_the_ID(), $indexing_options); 320 | $this->reset_transient(); 321 | 322 | do_action( 'wp_redisearch_cli_post_index', get_the_ID() ); 323 | 324 | if ( ! $result ) { 325 | $errors[] = get_the_ID(); 326 | } else { 327 | $indexed ++; 328 | } 329 | } 330 | } else { 331 | break; 332 | } 333 | 334 | WP_CLI::log( 'Processed ' . ( $query->post_count + $offset ) . '/' . $query->found_posts . ' entries. . .' ); 335 | 336 | $offset += $posts_per_page; 337 | 338 | usleep( 500 ); 339 | 340 | // Avoid running out of memory 341 | $this->stop_the_insanity(); 342 | } 343 | 344 | wp_reset_postdata(); 345 | 346 | // If --write-to-disk flag passed or it was enabled in settings, after indexing posts, write document to disk to persist it. 347 | if ( ! empty( $args['write-to-disk'] ) || Settings::get( 'wp_redisearch_write_to_disk' ) ) { 348 | $index->writeToDisk(); 349 | } 350 | 351 | return array( 'indexed' => $indexed, 'errors' => $errors ); 352 | } 353 | 354 | /** 355 | * Resets some values to reduce memory footprint. 356 | */ 357 | private function stop_the_insanity() { 358 | global $wpdb, $wp_object_cache, $wp_actions, $wp_filter; 359 | 360 | $wpdb->queries = array(); 361 | 362 | if ( is_object( $wp_object_cache ) ) { 363 | $wp_object_cache->group_ops = array(); 364 | $wp_object_cache->stats = array(); 365 | $wp_object_cache->memcache_debug = array(); 366 | 367 | // Make sure this is a public property, before trying to clear it 368 | try { 369 | $cache_property = new ReflectionProperty( $wp_object_cache, 'cache' ); 370 | if ( $cache_property->isPublic() ) { 371 | $wp_object_cache->cache = array(); 372 | } 373 | unset( $cache_property ); 374 | } catch ( ReflectionException $e ) { 375 | } 376 | 377 | /* 378 | * In the case where we're not using an external object cache, we need to call flush on the default 379 | * WordPress object cache class to clear the values from the cache property 380 | */ 381 | if ( ! wp_using_ext_object_cache() ) { 382 | wp_cache_flush(); 383 | } 384 | 385 | if ( is_callable( $wp_object_cache, '__remoteset' ) ) { 386 | call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important 387 | } 388 | } 389 | 390 | // Prevent wp_actions from growing out of control 391 | $wp_actions = $this->temporary_wp_actions; 392 | 393 | // It's high memory consuming as WP_Query instance holds all query results inside itself 394 | // and in theory $wp_filter will not stop growing until Out Of Memory exception occurs. 395 | if ( isset( $wp_filter['get_term_metadata'] ) ) { 396 | /* 397 | * WordPress 4.7 has a new Hook infrastructure, so we need to make sure 398 | * we're accessing the global array properly 399 | */ 400 | if ( class_exists( 'WP_Hook' ) && $wp_filter['get_term_metadata'] instanceof WP_Hook ) { 401 | $filter_callbacks = &$wp_filter['get_term_metadata']->callbacks; 402 | } else { 403 | $filter_callbacks = &$wp_filter['get_term_metadata']; 404 | } 405 | if ( isset( $filter_callbacks[10] ) ) { 406 | foreach ( $filter_callbacks[10] as $hook => $content ) { 407 | if ( preg_match( '#^[0-9a-f]{32}lazyload_term_meta$#', $hook ) ) { 408 | unset( $filter_callbacks[10][ $hook ] ); 409 | } 410 | } 411 | } 412 | } 413 | } 414 | 415 | 416 | /** 417 | * Lists all registered features. 418 | * 419 | * ## OPTIONS 420 | * [--all] 421 | * Lists all registered features regardless of their active status 422 | * 423 | * ## EXAMPLES 424 | * 425 | * $ wp redisearch list-features 426 | * Lists activated features 427 | * 428 | * $ wp redisearch list-features --all 429 | * Lists all regisred features 430 | * 431 | * @synopsis [--all] 432 | * @subcommand list-features 433 | * @since 0.2.0 434 | * @param array $args 435 | * @param array $assoc_args 436 | */ 437 | public function list_features( $args, $assoc_args ) { 438 | if ( empty( $assoc_args['all'] ) ) { 439 | $features = get_option( 'wp_redisearch_feature_settings', array() ); 440 | WP_CLI::line( __( 'Active features:', 'wp-redisearch' ) ); 441 | 442 | foreach ( $features as $key => $feature ) { 443 | if( $feature['active'] ) { 444 | WP_CLI::line( $key ); 445 | } 446 | } 447 | } else { 448 | WP_CLI::line( __( 'Registered features:', 'wp-redisearch' ) ); 449 | $features = wp_list_pluck( Features::init()->features, 'slug' ); 450 | 451 | foreach ( $features as $feature ) { 452 | WP_CLI::line( $feature ); 453 | } 454 | } 455 | } 456 | 457 | 458 | /** 459 | * Activate a feature. 460 | * 461 | * Activates a feature and returns error, in case there is some 462 | * If feature retuires re-index, returns warning 463 | * 464 | * ## OPTIONS 465 | * 466 | * 467 | * Slug of registered feature to be activated 468 | * 469 | * ## EXAMPLES 470 | * 471 | * $ wp redisearch activate-feature synonym 472 | * This will activate synonym feature 473 | * 474 | * @synopsis 475 | * @subcommand activate-feature 476 | * @since 0.2.0 477 | * @param array $args 478 | * @param array $assoc_args 479 | */ 480 | public function activate_feature( $args, $assoc_args ) { 481 | $feature = Features::init()->get_registered_feature( $args[0] ); 482 | 483 | if ( empty( $feature ) ) { 484 | WP_CLI::error( __( 'No feature with this slug is registered.', 'wp-redisearch' ) ); 485 | } 486 | 487 | if ( $feature->is_active() ) { 488 | WP_CLI::error( __( 'This feature is already active.', 'wp-redisearch' ) ); 489 | } 490 | 491 | $status = $feature->requirements_status(); 492 | 493 | if ( 1 === $status->code ) { 494 | WP_CLI::error( sprintf( __( 'Feature requirements are not met: %s', 'wp-redisearch' ), WP_CLI::colorize( '%R' . implode( "\n\n", (array) $status->message ) . '%N' ) ) ); 495 | } elseif ( 2 === $status->code ) { 496 | WP_CLI::warning( sprintf( __( 'Feature can be used, but there are warnings: %s', 'elasticpress' ), WP_CLI::colorize( '%y' . implode( "\n\n", (array) $status->message ) . '%N' ) ) ); 497 | } 498 | 499 | Features::init()->update_feature( $feature->slug, array( 'active' => true ) ); 500 | 501 | if ( $feature->requires_reindex ) { 502 | WP_CLI::warning( __( 'This feature requires a re-index. It might not work properly until you run the index command.', 'wp-redisearch' ) ); 503 | WP_CLI::line( sprintf( __( 'Just run %s', 'wp-redisearch' ), WP_CLI::colorize( '%G' . 'wp redisearch index --setup' . '%N' ) ) ); 504 | } 505 | 506 | WP_CLI::success( sprintf( __( 'Feature %s activated', 'wp-redisearch' ), WP_CLI::colorize( '%G' . $feature->title . '%N' ) ) ); 507 | } 508 | 509 | /** 510 | * Dectivate a feature. 511 | * 512 | * deactivates a feature and returns error, in case there is some 513 | * If feature retuires re-index, returns warning 514 | * 515 | * ## OPTIONS 516 | * 517 | * 518 | * Slug of registered feature to be deactivated 519 | * 520 | * ## EXAMPLES 521 | * 522 | * $ wp redisearch deactivate-feature synonym 523 | * This will deactivate synonym feature 524 | * 525 | * 526 | * @synopsis 527 | * @subcommand deactivate-feature 528 | * @since 0.2.0 529 | * @param array $args 530 | * @param array $assoc_args 531 | */ 532 | public function deactivate_feature( $args, $assoc_args ) { 533 | $feature = Features::init()->get_registered_feature( $args[0] ); 534 | 535 | if ( empty( $feature ) ) { 536 | WP_CLI::error( __( 'No feature with this slug is registered.', 'wp-redisearch' ) ); 537 | } 538 | 539 | $active_features = get_option( 'wp_redisearch_feature_settings', array() ); 540 | 541 | $key = array_search( $feature->slug, array_keys( $active_features ) ); 542 | 543 | if ( false === $key || empty( $active_features[ $feature->slug ]['active'] ) ) { 544 | WP_CLI::error( __( 'This feature is not active', 'wp-redisearch' ) ); 545 | } 546 | 547 | Features::init()->update_feature( $feature->slug, array( 'active' => false ) ); 548 | 549 | // Some features like synonym even on deactivation require re-index. 550 | if ( ! empty( $feature->deactivation_requires_reindex ) ) { 551 | WP_CLI::warning( __( 'This feature requires a re-index after deactivation also. It might not work properly until you run the index command.', 'wp-redisearch' ) ); 552 | WP_CLI::line( sprintf( __( 'Just run %s', 'wp-redisearch' ), WP_CLI::colorize( '%G' . 'wp redisearch index --setup' . '%N' ) ) ); 553 | } 554 | WP_CLI::success( sprintf( __( 'Feature %s deactivated', 'wp-redisearch' ), WP_CLI::colorize( '%G' . $feature->title . '%N' ) ) ); 555 | } 556 | 557 | /** 558 | * Provide better error messaging for common connection errors 559 | * 560 | * @since 0.2.0 561 | */ 562 | private function _connect_check() { 563 | if ( WPRedisearch::$serverException ) { 564 | WP_CLI::error( __( 'Redis server is not running.', 'wp-redisearch' ) ); 565 | } elseif ( WPRedisearch::$moduleException ) { 566 | WP_CLI::error( __( 'Redis server is running but RediSearch module is not loaded.', 'wp-redisearch' ) ); 567 | } 568 | } 569 | 570 | /** 571 | * Reset transient while indexing 572 | * 573 | * @since 0.2.0 574 | */ 575 | private function reset_transient() { 576 | set_transient( 'wp_redisearch_wpcli_indexing', true, $this->transient_expiration ); 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-redisearch/wp-redisearch", 3 | "description": "RediSearch plugin for Wordpress", 4 | "keywords": ["wordpress", "redis", "redisearch"], 5 | "homepage": "http://github.com/7kmco/wp-redisearch", 6 | "license": "MIT", 7 | "support": { 8 | "issues": "https://github.com/7kmco/wp-redisearch/issues" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Foad Yousefi", 13 | "email": "foadyousefi@gmail.com", 14 | "homepage": "https://7km.co" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "WpRediSearch\\": "src/" 20 | } 21 | }, 22 | "minimum-stability": "dev", 23 | "require": { 24 | "predis/predis": "^1.1", 25 | "foadyousefi/seven-fields": "dev-master", 26 | "asika/pdf2text": "dev-master", 27 | "vaites/php-apache-tika": "dev-master", 28 | "front/redisearch": "dev-master" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "a9d260ab686d236f3173db3e4ca44f93", 8 | "packages": [ 9 | { 10 | "name": "asika/pdf2text", 11 | "version": "dev-master", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/asika32764/php-pdf-2-text.git", 15 | "reference": "a14ea95695a277e385dbc03caeddb91c5e10319f" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/asika32764/php-pdf-2-text/zipball/a14ea95695a277e385dbc03caeddb91c5e10319f", 20 | "reference": "a14ea95695a277e385dbc03caeddb91c5e10319f", 21 | "shasum": "" 22 | }, 23 | "type": "library", 24 | "autoload": { 25 | "psr-4": { 26 | "Asika\\": "src" 27 | } 28 | }, 29 | "notification-url": "https://packagist.org/downloads/", 30 | "license": [ 31 | "GPL v2" 32 | ], 33 | "authors": [ 34 | { 35 | "name": "Simon Asika", 36 | "email": "asika32764@gmail.com" 37 | } 38 | ], 39 | "description": "Simple PHP PDF to text class.", 40 | "time": "2016-11-02T03:47:29+00:00" 41 | }, 42 | { 43 | "name": "foadyousefi/seven-fields", 44 | "version": "dev-master", 45 | "source": { 46 | "type": "git", 47 | "url": "https://github.com/foadyousefi/seven-fields.git", 48 | "reference": "89e1431b0960657135ee44095d862afe48baeb62" 49 | }, 50 | "dist": { 51 | "type": "zip", 52 | "url": "https://api.github.com/repos/foadyousefi/seven-fields/zipball/89e1431b0960657135ee44095d862afe48baeb62", 53 | "reference": "89e1431b0960657135ee44095d862afe48baeb62", 54 | "shasum": "" 55 | }, 56 | "require": { 57 | "php": ">=5.3" 58 | }, 59 | "type": "library", 60 | "autoload": { 61 | "psr-4": { 62 | "SevenFields\\": "src/" 63 | } 64 | }, 65 | "notification-url": "https://packagist.org/downloads/", 66 | "license": [ 67 | "GPL-2.0" 68 | ], 69 | "authors": [ 70 | { 71 | "name": "Foad Yousefi", 72 | "email": "foadyousefi@gmail.com", 73 | "homepage": "https://7km.co/" 74 | } 75 | ], 76 | "description": "WordPress developer-friendly option pages with custom fields.", 77 | "homepage": "https://7km.co/", 78 | "time": "2019-01-09T12:17:22+00:00" 79 | }, 80 | { 81 | "name": "front/redisearch", 82 | "version": "dev-master", 83 | "source": { 84 | "type": "git", 85 | "url": "https://github.com/front/redisearch.git", 86 | "reference": "e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9" 87 | }, 88 | "dist": { 89 | "type": "zip", 90 | "url": "https://api.github.com/repos/front/redisearch/zipball/e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9", 91 | "reference": "e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9", 92 | "shasum": "" 93 | }, 94 | "require": { 95 | "predis/predis": "^1.1" 96 | }, 97 | "type": "library", 98 | "autoload": { 99 | "psr-4": { 100 | "FKRediSearch\\": "src/" 101 | } 102 | }, 103 | "notification-url": "https://packagist.org/downloads/", 104 | "license": [ 105 | "MIT" 106 | ], 107 | "authors": [ 108 | { 109 | "name": "Foad Yousefi", 110 | "email": "foadyousefi@gmail.com", 111 | "homepage": "https://frontkom.no" 112 | }, 113 | { 114 | "name": "Fábio Neves", 115 | "email": "fabio@frontkom.com", 116 | "homepage": "https://frontkom.com" 117 | }, 118 | { 119 | "name": "Bartosz Jarzyna", 120 | "email": "bartosz@netkata.com", 121 | "homepage": "https://netkata.com" 122 | } 123 | ], 124 | "description": "CMS/Framework agnostic RediSearch client", 125 | "homepage": "http://github.com/front/redisearch", 126 | "keywords": [ 127 | "redis", 128 | "redisearch" 129 | ], 130 | "time": "2020-10-13T12:33:36+00:00" 131 | }, 132 | { 133 | "name": "predis/predis", 134 | "version": "v1.1.x-dev", 135 | "source": { 136 | "type": "git", 137 | "url": "https://github.com/predis/predis.git", 138 | "reference": "5f4b87080f4df0042a57d537137c1d2c17a2b79a" 139 | }, 140 | "dist": { 141 | "type": "zip", 142 | "url": "https://api.github.com/repos/predis/predis/zipball/5f4b87080f4df0042a57d537137c1d2c17a2b79a", 143 | "reference": "5f4b87080f4df0042a57d537137c1d2c17a2b79a", 144 | "shasum": "" 145 | }, 146 | "require": { 147 | "php": ">=5.3.9" 148 | }, 149 | "require-dev": { 150 | "cweagans/composer-patches": "^1.6", 151 | "phpunit/phpunit": "~4.8" 152 | }, 153 | "suggest": { 154 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 155 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 156 | }, 157 | "type": "library", 158 | "extra": { 159 | "composer-exit-on-patch-failure": true, 160 | "patches": { 161 | "phpunit/phpunit-mock-objects": { 162 | "Fix PHP 7 and 8 compatibility": "./tests/phpunit_mock_objects.patch" 163 | }, 164 | "phpunit/phpunit": { 165 | "Fix PHP 7 compatibility": "./tests/phpunit_php7.patch", 166 | "Fix PHP 8 compatibility": "./tests/phpunit_php8.patch" 167 | } 168 | } 169 | }, 170 | "autoload": { 171 | "psr-4": { 172 | "Predis\\": "src/" 173 | } 174 | }, 175 | "notification-url": "https://packagist.org/downloads/", 176 | "license": [ 177 | "MIT" 178 | ], 179 | "authors": [ 180 | { 181 | "name": "Daniele Alessandri", 182 | "email": "suppakilla@gmail.com", 183 | "homepage": "http://clorophilla.net", 184 | "role": "Creator & Maintainer" 185 | }, 186 | { 187 | "name": "Till Krüss", 188 | "homepage": "https://till.im", 189 | "role": "Maintainer" 190 | } 191 | ], 192 | "description": "Flexible and feature-complete Redis client for PHP and HHVM", 193 | "homepage": "http://github.com/predis/predis", 194 | "keywords": [ 195 | "nosql", 196 | "predis", 197 | "redis" 198 | ], 199 | "funding": [ 200 | { 201 | "url": "https://github.com/sponsors/tillkruss", 202 | "type": "github" 203 | } 204 | ], 205 | "time": "2020-09-20T15:16:49+00:00" 206 | }, 207 | { 208 | "name": "vaites/php-apache-tika", 209 | "version": "dev-master", 210 | "source": { 211 | "type": "git", 212 | "url": "https://github.com/vaites/php-apache-tika.git", 213 | "reference": "dcc27626ee7d0f572a828539a607d734094b9e73" 214 | }, 215 | "dist": { 216 | "type": "zip", 217 | "url": "https://api.github.com/repos/vaites/php-apache-tika/zipball/dcc27626ee7d0f572a828539a607d734094b9e73", 218 | "reference": "dcc27626ee7d0f572a828539a607d734094b9e73", 219 | "shasum": "" 220 | }, 221 | "require": { 222 | "ext-curl": "*", 223 | "php": ">=7.2.0" 224 | }, 225 | "require-dev": { 226 | "filp/whoops": "^2.7", 227 | "nunomaduro/phpinsights": "^1.14", 228 | "phpstan/phpstan": "^0.12.26", 229 | "phpunit/phpunit": "^8.0", 230 | "symfony/var-dumper": "^5.1" 231 | }, 232 | "type": "library", 233 | "extra": { 234 | "supported-versions": [ 235 | "1.15", 236 | "1.16", 237 | "1.17", 238 | "1.18", 239 | "1.19", 240 | "1.19.1", 241 | "1.20", 242 | "1.21", 243 | "1.22", 244 | "1.23", 245 | "1.24", 246 | "1.24.1" 247 | ] 248 | }, 249 | "autoload": { 250 | "psr-4": { 251 | "Vaites\\ApacheTika\\": "src/" 252 | } 253 | }, 254 | "notification-url": "https://packagist.org/downloads/", 255 | "license": [ 256 | "MIT" 257 | ], 258 | "authors": [ 259 | { 260 | "name": "David Martínez", 261 | "email": "contacto@davidmartinez.net" 262 | } 263 | ], 264 | "description": "Apache Tika bindings for PHP: extracts text from documents and images (with OCR), metadata and more...", 265 | "keywords": [ 266 | "OCR", 267 | "apache", 268 | "doc", 269 | "documents", 270 | "docx", 271 | "odt", 272 | "office", 273 | "pdf", 274 | "ppt", 275 | "pptx", 276 | "tika" 277 | ], 278 | "time": "2020-10-19T20:23:24+00:00" 279 | } 280 | ], 281 | "packages-dev": [], 282 | "aliases": [], 283 | "minimum-stability": "dev", 284 | "stability-flags": { 285 | "foadyousefi/seven-fields": 20, 286 | "asika/pdf2text": 20, 287 | "vaites/php-apache-tika": 20, 288 | "front/redisearch": 20 289 | }, 290 | "prefer-stable": false, 291 | "prefer-lowest": false, 292 | "platform": [], 293 | "platform-dev": [], 294 | "plugin-api-version": "1.1.0" 295 | } 296 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | set_menu_position( 20 ) 35 | ->set_icon( 'dashicons-search' ) 36 | ->plain_page() 37 | ->add_fields(array( __CLASS__, 'redisearchStatusPage')); 38 | // Redis server configurations. 39 | Container::make( __( 'Redis server', 'wp-redisearch' ), 'redis-server') 40 | ->set_parent('redisearch') 41 | ->add_fields(array( __CLASS__, 'RedisServerConf') ); 42 | // Indexing options and configurations. 43 | Container::make( __( 'Indexing options', 'wp-redisearch' ), 'indexing-options') 44 | ->set_parent('redisearch') 45 | ->add_fields(array( __CLASS__, 'redisearchIndexingFields') ); 46 | } 47 | 48 | 49 | /** 50 | * Fields for Redis Status option page. 51 | * @return void $fields 52 | * @since 0.1.0 53 | */ 54 | public static function redisearchStatusPage() { 55 | Fields::add('html', 'stats', 'Index status', self::indexStatusHtml() ); 56 | $features = Features::init()->features; 57 | if ( isset( $features ) && !empty( $features ) ) { 58 | echo '
'; 59 | echo '
'; 60 | foreach ($features as $feature) { 61 | ?> 62 |
63 |
64 |

title, 'wp-redisearch' ); ?>

65 |
66 | output_feature_box(); ?> 67 |
68 |
69 |
70 |
'; 73 | } 74 | } 75 | 76 | public static function indexStatusHtml() { 77 | $default_args = Settings::query_args(); 78 | $default_args['posts_per_page'] = -1; 79 | $args = apply_filters( 'wp_redisearch_posts_args', $default_args); 80 | 81 | /** 82 | * filter wp_redisearch_before_admin_wp_query 83 | * Fires before wp_query. This is useful if you want for some reasons, manipulate WP_Query 84 | * 85 | * @since 0.2.2 86 | * @param array $args Array of arguments passed to WP_Query 87 | * @return array $args Array of manipulated arguments 88 | */ 89 | $args = apply_filters( 'wp_redisearch_before_admin_wp_query', $args ); 90 | 91 | $query = new \WP_Query( $args ); 92 | 93 | /** 94 | * filter wp_redisearch_after_admin_wp_query 95 | * Fires after wp_query. This is useful if you want to manipulate results of WP_Query 96 | * 97 | * @since 0.2.2 98 | * @param array $args Array of arguments passed to WP_Query 99 | * @param object $query Result object of WP_Query 100 | */ 101 | $query = apply_filters( 'wp_redisearch_after_admin_wp_query', $query, $args ); 102 | 103 | $num_posts = $query->found_posts; 104 | 105 | $index_options = __( 'Indexing options:', 'wp-redisearch' ); 106 | $index_btn = __( 'Index posts', 'wp-redisearch' ); 107 | $num_docs = 0; 108 | if ( isset( WpRediSearch::$indexInfo ) && gettype( WpRediSearch::$indexInfo ) == 'array' ) { 109 | $num_docs_offset = array_search( 'num_docs', WpRediSearch::$indexInfo ) + 1; 110 | $num_docs = WpRediSearch::$indexInfo[$num_docs_offset]; 111 | } 112 | 113 | $status_html = <<<"EOT" 114 |
115 |
116 |
117 |

This is RediSearch status page.

118 |

Whith the current settings, there is ${num_posts} posts to be indexed.

119 |

Right now, ${num_docs} posts have been indexed.

120 |
121 | ${index_options} 122 | 123 | 124 |
125 |
126 |
127 |
128 | 129 | ${num_docs}/${num_posts} 130 |
131 | 138 |
139 |
140 |
141 | EOT; 142 | return $status_html; 143 | } 144 | 145 | /** 146 | * Fields for Redis Server Configuration option page. 147 | * @since 0.1.0 148 | * @param 149 | * @return object $fields 150 | */ 151 | public static function RedisServerConf() { 152 | Fields::add( 'header', null, __( 'Redis server configurations', 'wp-redisearch' ) ); 153 | Fields::add( 'select', 'wp_redisearch_connection_scheme', __( 'Server communication scheme', 'wp-redisearch' ), __( 'Choose Redis server communication scheme.', 'wp-redisearch'), array('tcp' => 'TCP', 'unix' => 'UNIX') ); 154 | Fields::add( 'text', 'wp_redisearch_server', __( 'Redis server/path', 'wp-redisearch' ), __( 'Redis server url, usually it is 127.0.0.1 and in case of using UNIX socket, the socket path.', 'wp-redisearch' ) ); 155 | Fields::add( 'text', 'wp_redisearch_port', __( 'Redis port', 'wp-redisearch' ), __( 'Redis port number, by default it is 6379', 'wp-redisearch' ) ); 156 | Fields::add( 'password', 'wp_redisearch_password', __( 'Redis server password', 'wp-redisearch' ), __( 'If your redis server is not password protected, leave this field blank', 'wp-redisearch' ) ); 157 | Fields::add( 'text', 'wp_redisearch_index_name', __( 'Redisearch index name', 'wp-redisearch' ) ); 158 | } 159 | 160 | 161 | /** 162 | * Fields for indexable stuff options page. 163 | * @since 0.1.0 164 | * @param 165 | * @return object $fields 166 | */ 167 | public static function redisearchIndexingFields() { 168 | Fields::add( 'header', null, __( 'General indexing settings', 'wp-redisearch' ) ); 169 | Fields::add( 'select', 'wp_redisearch_default_language', __( 'Default language', 'wp-redisearch' ), __( 'Set sites default language. This setting can be changed per post by utilizing "wp_redisearch_index_language" filter', 'wp-redisearch' ), self::supportedLanguages() ); 170 | Fields::add( 'text', 'wp_redisearch_indexing_batches', __( 'Posts will be indexed in baches of:', 'wp-redisearch' ) ); 171 | Fields::add( 'checkbox', 'wp_redisearch_search_in_admin', __( 'Allow RediSearch in admin', 'wp-redisearch' ), __( 'If enabled, the plugin overrides searches done in admin area.', 'wp-redisearch') ); 172 | Fields::add( 'header', null, __( 'Persist index after server restart.', 'wp-redisearch' ), __( 'Redisearch is in-memory database, which means after server restart (for any reason), all data in the redis database will be lost. But redis also can write to the disk.', 'wp-redisearch' ) ); 173 | Fields::add( 'checkbox', 'wp_redisearch_write_to_disk', __( 'Write redis data to the disk', 'wp-redisearch' ), __( 'If enabled, after indexing manualy in redisearch dashboard or adding new post to the site, entire redisearch index will be written to the disk and after server restart, you won\'t loos any data', 'wp-redisearch') ); 174 | Fields::add( 'header', null, 'What to be indexed' ); 175 | Fields::add( 'multiselect', 'wp_redisearch_post_types', __( 'Post types', 'wp-redisearch' ), __( 'Post types to be indexed', 'wp-redisearch' ), self::postTypes() ); 176 | Fields::add( 'multiselect', 'wp_redisearch_indexable_terms', __( 'Taxonomies', 'wp-redisearch' ), __( 'Post tag, category and custom taxonomies to be indexed', 'wp-redisearch' ), self::getTerms() ); 177 | Fields::add( 'header', null, 'Stop-words' ); 178 | Fields::add( 'checkbox', 'wp_redisearch_disable_stop_words', __( 'Disable stop words support. (Not recommended)', 'wp-redisearch' ), __( 'If this checkbox checked, no any words will be discarded from being indexed.', 'wp-redisearch') ); 179 | Fields::add( 'textarea', 'wp_redisearch_stop_words', __( 'A list of comma separated stop-words.', 'wp-redisearch' ), __('RediSearch support stop-words out of the box, but the list of default stop-words and languages, are limited.
Here you can add a list of stop-words in your language.
Just remember after editing this list, only posts published after this setting will be affected. So re-indexing after changing this list is highly recommended. If you leave this blank, RediSearch will fall back to default wtop-words.', 'wp-redisearch' ) ); 180 | // In case we need to extend option fields. 181 | do_action( 'wp_redisearch_settings_indexing_fields' ); 182 | } 183 | 184 | /** 185 | * List of all post types. 186 | * @return array 187 | * @since 0.1.0 188 | */ 189 | private static function postTypes() { 190 | $post_types = get_post_types( 191 | array( 192 | 'public' => true, 193 | 'exclude_from_search' => false, 194 | 'show_ui' => true, 195 | ) 196 | ); 197 | /** 198 | * We exclude product from indexable post types, since woocommerce support added via Features. 199 | * 200 | * @since 0.2.1 201 | */ 202 | if ( function_exists( 'array_diff' ) ) { 203 | $post_types = array_diff( $post_types, array( 'product' ) ); 204 | } 205 | /** 206 | * We exclude attachment from indexable post types, since document support added via Features. 207 | * 208 | * @since 0.2.1 209 | */ 210 | if ( function_exists( 'array_diff' ) ) { 211 | $post_types = array_diff( $post_types, array( 'attachment' ) ); 212 | } 213 | return $post_types; 214 | } 215 | 216 | /** 217 | * Get all terms. 218 | * @return array $terms 219 | * @since 0.1.0 220 | */ 221 | private static function getTerms() { 222 | $post_types = self::postTypes(); 223 | $indexable_taxonomies = array(); 224 | 225 | foreach ( $post_types as $post_type ) { 226 | $taxonomies = get_object_taxonomies( $post_type, 'objects' ); 227 | foreach ( $taxonomies as $taxonomy ) { 228 | if ( $taxonomy->public || $taxonomy->publicly_queryable ) { 229 | $indexable_taxonomies[] = $taxonomy->name; 230 | } 231 | } 232 | } 233 | 234 | return $indexable_taxonomies; 235 | } 236 | 237 | /** 238 | * Enqueue admin scripts. 239 | * @since 0.1.0 240 | * @param 241 | * @return 242 | */ 243 | public function redisearchEnqueueScripts() { 244 | wp_enqueue_script( 'wp_redisearch_admin_js', WPRS_URL . 'src/Admin/js/admin.js', array( 'jquery' ), WPRS_VERSION, true ); 245 | $localized_data = array( 246 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 247 | 'nonce' => wp_create_nonce( 'wprds_dashboard_nonce' ) 248 | ); 249 | wp_localize_script( 'wp_redisearch_admin_js', 'wpRds', $localized_data ); 250 | wp_enqueue_style( 'wp_redisearch_admin_styles', WPRS_URL . 'src/Admin/css/admin.css', false, WPRS_VERSION ); 251 | } 252 | 253 | /** 254 | * action for "index it" ajax call to start indexing selected posts. 255 | * @since 0.1.0 256 | * @param 257 | * @return 258 | */ 259 | public static function wp_redisearch_add_to_index() { 260 | $index = new Index( (new Client())->return() ); 261 | $results = $index->create()->add(); 262 | wp_send_json_success( $results ); 263 | } 264 | 265 | /** 266 | * Write to disk to persist data. 267 | * @since 0.1.0 268 | * @param 269 | * @return 270 | */ 271 | public static function wp_redisearch_write_to_disk() { 272 | if ( Settings::get( 'wp_redisearch_write_to_disk' ) ) { 273 | $client = (new Client())->return(); 274 | $index = new \FKRediSearch\Index($client); 275 | $index->setIndexName( Settings::indexName() ); 276 | $result = $index->writeToDisk(); 277 | wp_send_json_success( $result ); 278 | } 279 | } 280 | 281 | /** 282 | * action for "index it" ajax call to start indexing selected posts. 283 | * @since 0.1.0 284 | * @param 285 | * @return 286 | */ 287 | public function wp_redisearch_index_post_on_publish( $post_id, $post, $update ) { 288 | // If this is a revision, of it is auto save, don't do anything. 289 | if ( wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) 290 | return; 291 | 292 | $wprdsIndex = new Index( (new Client())->return() ); 293 | 294 | $index_name = Settings::indexName(); 295 | 296 | // If post is not published or un-published, delete from index then, return. 297 | if ( $post->post_status != 'publish' ) { 298 | $wprdsIndex->deletePost( $post_id ); 299 | 300 | /** 301 | * Filter wp_redisearch_after_post_deleted fires after post deleted from the index. 302 | * 303 | * @since 0.2.0 304 | * @param array $index_name Index name 305 | * @param array $post The post object 306 | */ 307 | do_action( 'wp_redisearch_after_post_deleted', $index_name, $post ); 308 | 309 | 310 | // If enabled, write to disk 311 | if ( Settings::get( 'wp_redisearch_write_to_disk' ) ) { 312 | $wprdsIndex->writeToDisk(); 313 | } 314 | return; 315 | } 316 | 317 | $content = wp_strip_all_tags( $post->post_content, true ); 318 | $permalink = get_permalink( $post_id ); 319 | $user = get_userdata( $post->post_author ); 320 | 321 | if ( $user instanceof WP_User ) { 322 | $user_data = $user->display_name; 323 | } else { 324 | $user_data = ''; 325 | } 326 | 327 | $post_date = $post->post_date; 328 | $post_modified = $post->post_modified; 329 | // If date is invalid, set it to null 330 | if ( ! strtotime( $post_date ) || $post_date === "0000-00-00 00:00:00" ) { 331 | $post_date = null; 332 | } 333 | 334 | $defaultLanguage = get_option( 'wp_redisearch_default_language', 'english'); 335 | $indexing_options['language'] = apply_filters( 'wp_redisearch_index_language', $defaultLanguage, $post_id ); 336 | $indexing_options['fields'] = $wprdsIndex->preparePost( $post_id ); 337 | $indexing_options['extra_params'] = array( 'REPLACE' ); 338 | // Finally, add post to index 339 | $wprdsIndex->addPosts( $post_id, $indexing_options ); 340 | 341 | /** 342 | * Filter wp_redisearch_after_post_indexed fires after post added to the index. 343 | * 344 | * @since 0.2.0 345 | * @param array $index_name Index name 346 | * @param array $post The post object 347 | * @param array $indexing_options Posts extra options like language and fields 348 | */ 349 | do_action( 'wp_redisearch_after_post_published', $index_name, $post, $indexing_options ); 350 | 351 | // If enabled, write to disk 352 | if ( Settings::get( 'wp_redisearch_write_to_disk' ) ) { 353 | $wprdsIndex->writeToDisk(); 354 | } 355 | } 356 | 357 | /** 358 | * Drop existing index. 359 | * @since 0.1.0 360 | * @param 361 | * @return 362 | */ 363 | public static function wp_redisearch_drop_index() { 364 | $client = (new Client())->return(); 365 | $index = new \FKRediSearch\Index($client); 366 | $index->setIndexName( Settings::indexName() ); 367 | $results = $index->drop( TRUE ); 368 | wp_send_json_success( $results ); 369 | } 370 | 371 | public function supportedLanguages() { 372 | return array( 373 | 'arabic' => 'Arabic', 374 | 'danish' => 'Danish', 375 | 'dutch' => 'Dutch', 376 | 'english' => 'English', 377 | 'finnish' => 'Finnish', 378 | 'french' => 'French', 379 | 'german' => 'German', 380 | 'hungarian' => 'Hungarian', 381 | 'italian' => 'Italian', 382 | 'norwegian' => 'Norwegian', 383 | 'portuguese' => 'Portuguese', 384 | 'romanian' => 'Romanian', 385 | 'russian' => 'Russian', 386 | 'spanish' => 'Spanish', 387 | 'swedish' => 'Swedish', 388 | 'tamil' => 'Tamil', 389 | 'turkish' => 'Turkish', 390 | 'chinese' => 'Chinese', 391 | ); 392 | } 393 | 394 | } -------------------------------------------------------------------------------- /src/Admin/css/admin.css: -------------------------------------------------------------------------------- 1 | .wprds-form-wraper .wprds-field { 2 | position: relative; 3 | padding: 14px 12px 15px; 4 | background: #fff; 5 | border-width: 1px 1px 0 0; 6 | border-style: solid; 7 | border-color: #e5e5e5; } 8 | .wprds-form-wraper .wprds-field h3 { 9 | padding: 0; 10 | font-size: 28px; 11 | margin: 0; 12 | line-height: 1; 13 | font-weight: 200; } 14 | .wprds-form-wraper .wprds-field .label { 15 | font-weight: 600; 16 | margin-bottom: 10px; } 17 | .wprds-form-wraper .wprds-field.text-field input, 18 | .wprds-form-wraper .wprds-field.textarea textarea { 19 | display: block; 20 | width: 100%; 21 | margin-bottom: 10px; } 22 | .wprds-form-wraper .wprds-field .desc { 23 | color: #888; } 24 | 25 | .wprds-form-wraper .form-actions { 26 | flex: 1; 27 | margin-left: 10px; } 28 | 29 | .wprds-wrapper-grid { 30 | margin-left: 8px; 31 | margin-right: 8px; 32 | margin-top: 10px; } 33 | .wprds-wrapper-grid .postbox { 34 | float: left; 35 | clear: left; 36 | width: 50%; 37 | margin: 0 0 16px; } 38 | @media only screen and (max-width: 850px) { 39 | .wprds-wrapper-grid .postbox { 40 | margin-left: 0 !important; 41 | margin-right: 0 !important; 42 | width: 100%; } } 43 | .wprds-wrapper-grid .postbox .inside { 44 | margin-bottom: 0; } 45 | .wprds-wrapper-grid .postbox:nth-child(odd) { 46 | margin-left: -8px; } 47 | .wprds-wrapper-grid .postbox:nth-child(even) { 48 | float: right; 49 | clear: right; 50 | margin-right: -8px; } 51 | .wprds-wrapper-grid .hndle { 52 | font-size: 14px; 53 | padding: 8px 12px; 54 | margin: 0; 55 | line-height: 1.4; } 56 | .wprds-wrapper-grid .wprds-fields { 57 | border: #ebebeb solid 1px; 58 | background: #fafafa; 59 | border-radius: 3px; } 60 | .wprds-wrapper-grid .action-wrap { 61 | text-align: right; 62 | margin-top: 20px; } 63 | .wprds-wrapper-grid .reindex-required { 64 | line-height: 2; } 65 | .wprds-wrapper-grid .feature-fields { 66 | display: flex; } 67 | .wprds-wrapper-grid .feature-fields .field-name { 68 | flex: 1; } 69 | .wprds-wrapper-grid .feature-fields .input-wrap { 70 | flex: 3; } 71 | .wprds-wrapper-grid .saving:before, 72 | .wprds-wrapper-grid .loading:before { 73 | content: ' '; 74 | margin-right: 1.4em; 75 | top: 4px; 76 | border-radius: 50%; 77 | width: 8px; 78 | height: 8px; 79 | display: inline-block; 80 | font-size: 7px; 81 | position: relative; 82 | text-indent: -9999em; 83 | border-top: 5px solid #ccc; 84 | border-right: 5px solid #ccc; 85 | border-bottom: 5px solid #ccc; 86 | border-left: 5px solid #999; 87 | transform: translateZ(0); 88 | -webkit-animation: spinner 1.1s infinite linear; 89 | animation: spinner 1.1s infinite linear; } 90 | .wprds-wrapper-grid .feature-active:before { 91 | content: ''; 92 | position: absolute; 93 | left: 0; 94 | top: 0; 95 | background: #ade5b4; 96 | width: 6px; 97 | height: 100%; } 98 | 99 | @-webkit-keyframes spinner { 100 | 0% { 101 | transform: rotate(0deg); } 102 | 100% { 103 | transform: rotate(360deg); } } 104 | 105 | @keyframes spinner { 106 | 0% { 107 | transform: rotate(0deg); } 108 | 100% { 109 | transform: rotate(360deg); } } 110 | 111 | .wprds-feature-notice { 112 | padding: 10px; } 113 | -------------------------------------------------------------------------------- /src/Admin/index.php: -------------------------------------------------------------------------------- 1 | $value ) { 103 | $this->$key = $value; 104 | } 105 | } 106 | 107 | /** 108 | * Returns requirements status of the feature 109 | * 110 | * @since 0.2.0 111 | * @return 112 | */ 113 | public function requirements_status() { 114 | $status = new \stdClass(); 115 | $status->code = 0; 116 | $status->message = array(); 117 | 118 | if ( ! empty( $this->requirements_cb ) ) { 119 | $status = call_user_func( $this->requirements_cb, $this ); 120 | } 121 | 122 | /** 123 | * If feature is active but requirements are not satisfied 124 | * deactivate the feature. 125 | * This is usefull in case for example, for some reasons WooCommerce plugin being deactivated. 126 | * 127 | * @since 0.2.3 128 | */ 129 | if ( $status->code == 1 && $this->is_active() ) { 130 | Features::init()->update_feature( $this->slug, array( 'active' => false ) ); 131 | } 132 | 133 | return apply_filters( 'wp_redisearch_feature_requirements_status', $status, $this ); 134 | } 135 | 136 | /** 137 | * Run on every page load for feature to set itself up 138 | * 139 | * @since 0.2.0 140 | */ 141 | public function setup() { 142 | if ( ! empty( $this->setup_cb ) ) { 143 | call_user_func( $this->setup_cb, $this ); 144 | } 145 | 146 | // 147 | add_action( 'wp_redisearch_settings_indexing_fields', array( $this, 'feature_options_fields' ) ); 148 | 149 | do_action( 'wp_redisearch_feature_setup', $this->slug, $this ); 150 | } 151 | 152 | /** 153 | * Returns true if feature is active 154 | * 155 | * @since 0.2.0 156 | * @return boolean 157 | */ 158 | public function is_active() { 159 | $feature_settings = get_option( 'wp_redisearch_feature_settings', array() ); 160 | 161 | $active = false; 162 | 163 | if ( ! empty( $feature_settings[ $this->slug ] ) && $feature_settings[ $this->slug ]['active'] ) { 164 | $active = true; 165 | } 166 | 167 | return apply_filters( 'wp_redisearch_feature_active', $active, $feature_settings, $this ); 168 | } 169 | 170 | /** 171 | * Run after a feature is activated 172 | * 173 | * @since 0.2.0 174 | */ 175 | public function after_activation() { 176 | if ( ! empty( $this->activation_cb ) ) { 177 | call_user_func( $this->activation_cb, $this ); 178 | } 179 | do_action( 'wp_redisearch_feature_after_activation', $this->slug, $this ); 180 | } 181 | 182 | /** 183 | * Run after a feature is de-activated 184 | * 185 | * @since 0.2.0 186 | */ 187 | public function after_deactivation() { 188 | if ( ! empty( $this->deactivation_cb ) ) { 189 | call_user_func( $this->deactivation_cb, $this ); 190 | } 191 | do_action( 'wp_redisearch_feature_after_deactivation', $this->slug, $this ); 192 | } 193 | 194 | /** 195 | * Outputs feature box. 196 | * 197 | * @since 0.2.0 198 | */ 199 | public function feature_options_fields() { 200 | if ( ! empty( $this->feature_options_cb ) ) { 201 | call_user_func( $this->feature_options_cb, $this ); 202 | } 203 | } 204 | 205 | /** 206 | * Outputs feature box. 207 | * 208 | * @since 0.2.0 209 | */ 210 | public function output_feature_box() { 211 | $requirements_status = $this->requirements_status(); 212 | if ( ! empty( $requirements_status->message ) ) { 213 | $messages = (array) $requirements_status->message; 214 | $notice_class = $requirements_status->code == 1 ? 'error' : 'warning'; 215 | foreach ( $messages as $message ) { 216 | echo '
'; 217 | echo wp_kses_post( $message ); 218 | echo '
'; 219 | } 220 | } 221 | 222 | $this->output_desc(); 223 | $this->output_settings_box(); 224 | } 225 | 226 | /** 227 | * Outputs feature description 228 | * 229 | * @since 0.2.0 230 | */ 231 | public function output_desc() { 232 | if ( ! empty( $this->feature_desc_cb ) ) { 233 | call_user_func( $this->feature_desc_cb, $this ); 234 | } 235 | 236 | do_action( 'wp_redisearch_feature_box_full', $this->slug, $this ); 237 | } 238 | 239 | /** 240 | * Outputs Settings. 241 | * 242 | * @since 0.2.0 243 | */ 244 | public function output_settings_box() { 245 | $requirements_status = $this->requirements_status(); 246 | ?> 247 | 248 |

249 |
250 |
251 |
252 |
260 | 268 |
269 |
270 | 271 | feature_settings_cb ) ) { 273 | call_user_func( $this->feature_settings_cb, $this ); 274 | return; 275 | } 276 | do_action( 'wp_redisearch_feature_box_settings_' . $this->slug, $this ); 277 | ?> 278 |
279 | requires_reindex ) : ?> 280 | 281 | 282 | 283 | 284 | 285 | 287 | 288 | 289 |
290 | setup(); 27 | } 28 | return $instance; 29 | } 30 | 31 | /** 32 | * Initiate class actions 33 | * 34 | * @since 0.2.0 35 | */ 36 | public function setup() { 37 | add_action( 'init', array( $this, 'setup_features' ), 0 ); 38 | } 39 | 40 | 41 | /** 42 | * Save individual feature settings. 43 | * This method used to save features settings using ajax request. 44 | * 45 | * @since 0.2.0 46 | */ 47 | public function wp_redisearch_save_feature() { 48 | if ( empty( $_POST['feature'] ) || empty( $_POST['settings'] ) || ! check_ajax_referer( 'wprds_dashboard_nonce', 'nonce', false ) ) { 49 | wp_send_json_error(); 50 | exit; 51 | } 52 | 53 | $data = $this->update_feature( $_POST['feature'], $_POST['settings'] ); 54 | 55 | // Since we deactivated, delete auto activate notice 56 | if ( empty( $_POST['settings']['active'] ) ) { 57 | delete_option( 'wp_redisearch_feature_requires_reindex' ); 58 | } 59 | 60 | wp_send_json_success( $data ); 61 | } 62 | 63 | /** 64 | * Registers a feature 65 | * 66 | * @param string $slug 67 | * @param array $args 68 | * 69 | * Parameters: 70 | * "title" (string) - Human readable title 71 | * "default_settings" (array) - Array of default settings. 72 | * "setup_cb" (callback) - Callback function when the feature is activated 73 | * "requirements_cb" (callback) - Callback function to check feature requirements 74 | * "activation_cb" (callback) - Callback function after feature activation 75 | * "feature_desc_cb" (callback) - Callback function that outputs HTML for feature description 76 | * "feature_settings_cb" (callback) - Callback function that outputs custom feature settings 77 | * 78 | * @since 0.2.0 79 | * @return boolean 80 | */ 81 | public function register_feature( $slug, $args ) { 82 | if ( empty( $slug ) || empty( $args ) || ! is_array( $args ) ) { 83 | return false; 84 | } 85 | $args['slug'] = $slug; 86 | $this->features[ $slug ] = new Feature( $args ); 87 | 88 | return true; 89 | } 90 | 91 | /** 92 | * Activate or deactivate a feature 93 | * 94 | * @param string $slug 95 | * @param array $settings 96 | * @param bool $force 97 | * @since 0.2.0 98 | * @return array|bool 99 | */ 100 | public function update_feature( $slug, $settings ) { 101 | $feature = $this->get_registered_feature( $slug ); 102 | 103 | if ( empty( $feature ) ) { 104 | return false; 105 | } 106 | 107 | $original_state = $feature->is_active(); 108 | 109 | $feature_settings = get_option( 'wp_redisearch_feature_settings', array() ); 110 | 111 | if ( empty( $feature_settings[ $slug ] ) ) { 112 | // If doesn't exist, merge with feature defaults 113 | $feature_settings[ $slug ] = wp_parse_args( $settings, $feature->default_settings ); 114 | } else { 115 | // If exist just merge changed values into current 116 | $feature_settings[ $slug ] = wp_parse_args( $settings, $feature_settings[ $slug ] ); 117 | } 118 | update_option( 'fj_tests_fea', $feature_settings ); 119 | update_option( 'fj_tests_fea_sl', $slug ); 120 | update_option( 'fj_tests_fea_sett', $settings ); 121 | // Make sure active is a bool 122 | $feature_settings[ $slug ]['active'] = (bool) $feature_settings[ $slug ]['active']; 123 | 124 | $sanitize_feature_settings = apply_filters( 'wp_redisearch_sanitize_feature_settings', $feature_settings, $feature ); 125 | 126 | update_option( 'wp_redisearch_feature_settings', $sanitize_feature_settings ); 127 | 128 | $data = array( 129 | 'reindex' => false, 130 | ); 131 | 132 | if ( $feature_settings[ $slug ]['active'] && ! $original_state ) { 133 | if ( ! empty( $feature->requires_reindex ) ) { 134 | $data['reindex'] = true; 135 | } 136 | $feature->after_activation(); 137 | } elseif ( !$feature_settings[ $slug ]['active'] && $original_state ) { 138 | if ( ! empty( $feature->deactivation_requires_reindex ) ) { 139 | $data['reindex'] = true; 140 | } 141 | $feature->after_deactivation(); 142 | } 143 | /** 144 | * Some features require re-index on deactivatino also. 145 | * This filter, forces re-indexing 146 | * @since 0.2.0 147 | * @param bool $data['rendex'] reinindex status. 148 | * @param object $feature The featur itself. 149 | */ 150 | $data['reindex'] = apply_filters( 'wp_redisearch_feature_reindex', $data['reindex'], $feature ); 151 | 152 | return $data; 153 | } 154 | 155 | /** 156 | * Setup all active features 157 | * 158 | * @since 0.2.0 159 | */ 160 | public function setup_features() { 161 | foreach ( $this->features as $feature_slug => $feature ) { 162 | if ( $feature->is_active() ) { 163 | $feature->setup(); 164 | } 165 | } 166 | } 167 | 168 | 169 | /** 170 | * Get a Feature object from its slug 171 | * @param string $slug 172 | * @since 0.2.0 173 | * @return Feature 174 | */ 175 | public function get_registered_feature( $slug ) { 176 | if ( empty( $this->features[ $slug ] ) ) { 177 | return false; 178 | } 179 | return $this->features[ $slug ]; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Features/Document.php: -------------------------------------------------------------------------------- 1 | return(); 30 | $this->index = new Index($client); 31 | $this->index->setIndexName( Settings::indexName() ); 32 | 33 | Features::init()->register_feature( 'document', array( 34 | 'title' => 'Document', 35 | 'setup_cb' => array( $this, 'setup' ), 36 | 'activation_cb' => array( $this, 'activated' ), 37 | 'deactivation_cb' => array( $this, 'deactivated' ), 38 | 'feature_desc_cb' => array( $this, 'feature_desc' ), 39 | 'feature_options_cb' => array( $this, 'feature_options' ), 40 | 'requires_reindex' => true, 41 | 'deactivation_requires_reindex' => true, 42 | ) ); 43 | } 44 | 45 | /** 46 | * This method will run on each page load. 47 | * You can hook functions which must run always. 48 | * 49 | * @since 0.2.2 50 | */ 51 | public function setup () { 52 | add_filter( 'wp_redisearch_indexable_post_types', array( __CLASS__, 'indexable_post_type' ) ); 53 | add_filter( 'wp_redisearch_indexable_post_status', array( __CLASS__, 'indexable_post_status' ) ); 54 | add_filter( 'wp_redisearch_indexable_meta_keys', array( __CLASS__, 'indexable_meta_keys' ) ); 55 | add_filter( 'wp_redisearch_prepared_post_args', array( __CLASS__, 'index_document' ), 10, 2 ); 56 | add_filter( 'wp_redisearch_before_admin_wp_query', array( __CLASS__, 'before_wp_query' ) ); 57 | add_filter( 'wp_redisearch_before_index_wp_query', array( __CLASS__, 'before_wp_query' ) ); 58 | add_filter( 'wp_redisearch_after_admin_wp_query', array( __CLASS__, 'after_wp_query' ), 10, 2 ); 59 | add_filter( 'wp_redisearch_after_index_wp_query', array( __CLASS__, 'after_wp_query' ), 10, 2 ); 60 | } 61 | 62 | /** 63 | * Add attachment to indexable post types list. 64 | * 65 | * @param array $post_types Array of indexable post types 66 | * @since 0.2.2 67 | * @return array $post_types 68 | */ 69 | public static function indexable_post_type ( $post_types ) { 70 | return array_merge( $post_types, array( 'attachment' ) ); 71 | } 72 | 73 | /** 74 | * Add inherit to indexable post types list. 75 | * 76 | * @param array $post_status Array of indexable post status 77 | * @since 0.2.2 78 | * @return array $post_status 79 | */ 80 | public static function indexable_post_status ( $post_status ) { 81 | return array_merge( $post_status, array( 'inherit' ) ); 82 | } 83 | 84 | /** 85 | * Add document field to the index 86 | * 87 | * @param array $meta Existing meta keys 88 | * 89 | * @return array 90 | * @since 0.2.2 91 | */ 92 | public static function indexable_meta_keys( array $meta ) { 93 | $indexable_keys = array( 'document' ); 94 | 95 | return array_merge( $meta, $indexable_keys ); 96 | } 97 | 98 | /** 99 | * add allowed mime types to WP_Query 100 | * 101 | * @since 0.2.2 102 | */ 103 | public static function before_wp_query ( $args ) { 104 | add_filter( 'posts_where', array( __CLASS__, 'add_mime_types_to_query' ) ); 105 | return $args; 106 | } 107 | 108 | /** 109 | * Remove posts_where filter after WP_Query runs 110 | * 111 | * @since 0.2.2 112 | */ 113 | public static function after_wp_query ( $query, $args) { 114 | remove_filter( 'posts_where', array( __CLASS__, 'add_mime_types_to_query' ) ); 115 | return $query; 116 | } 117 | 118 | /** 119 | * Add allowed mime types to the main WP_Query 120 | * We do this because we only want to query posts with allowed mime types, or no mime type (posts, pages, products and ...) 121 | * 122 | * @since 0.2.2 123 | * @param string $where mysql query string 124 | * @return string $where Altered mysql query 125 | */ 126 | public static function add_mime_types_to_query ( $where ) { 127 | $allowed_mime_types = self::allowed_mime_types(); 128 | 129 | $where_mime_types = ''; 130 | foreach ( $allowed_mime_types as $key => $value ) { 131 | $where_mime_types .= '"' . $value . '", '; 132 | } 133 | 134 | $where .= ' AND post_mime_type IN( ' . $where_mime_types . '"") '; 135 | return $where; 136 | } 137 | 138 | 139 | /** 140 | * Fires after feature activation. 141 | * 142 | * @since 0.2.2 143 | */ 144 | public function activated () { 145 | } 146 | 147 | /** 148 | * Fires after feature deactivation. 149 | * 150 | * @since 0.2.2 151 | */ 152 | public function deactivated () { 153 | } 154 | 155 | /** 156 | * Feature description. 157 | * This will be added to feature setting box. 158 | * 159 | * @since 0.2.2 160 | */ 161 | public function feature_desc () { 162 | ?> 163 |

164 |

Apache Tika' ) 166 | ?>

167 | 168 | post_type !== 'attachment' ) { 192 | return $post_args; 193 | } 194 | 195 | global $wp_filesystem; 196 | 197 | require_once( ABSPATH . 'wp-admin/includes/file.php' ); 198 | 199 | if ( ! WP_Filesystem() ) { 200 | return $post_args; 201 | } 202 | 203 | $allowed_mime_types = self::allowed_mime_types(); 204 | 205 | if ( in_array( $post->post_mime_type, $allowed_mime_types ) ) { 206 | $file_name = get_attached_file( $post->ID ); 207 | 208 | if ( $wp_filesystem->exists( $file_name, false, 'f' ) ) { 209 | $file_content = ''; 210 | /** 211 | * Parse file contents base on their file mime_type 212 | * 213 | * First, we will try to use Apache Tika rest server, a toolkit to extract file content from virtually any file types. 214 | * Apache Tika is faster to read files and output file is way cleaner than pure php method. 215 | */ 216 | try { 217 | $tika_host = Settings::get( 'wp_redisearch_tika_host', '127.0.0.1' ); 218 | $tika_port = Settings::get( 'wp_redisearch_tika_port', 9998 ); 219 | $tika_client = TikaClient::make( $tika_host, $tika_port ); 220 | $file_content = $tika_client->getText( $file_name ); 221 | $file_content = str_replace(array("\r", "\\n"), '', $file_content); 222 | } catch ( \Exception $e ) { 223 | } 224 | // If Tika rest server is down or for some reason was not able to do the tricks, we try php methods. 225 | if ( !$file_content ) { 226 | // If pdf attachment is pdf 227 | if ( $post->post_mime_type == 'application/pdf' ) { 228 | $pdf2text = new \Asika\Pdf2text; 229 | $file_content = $pdf2text->decode( $file_name ); 230 | // Or if attachment is word, excel or powepoint 231 | } elseif ( in_array( $post->post_mime_type, array( 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ) ) ) { 232 | $file_content = MsOfficeParser::getText( $file_name ); 233 | // Or if its another allowed mime_type 234 | } else { 235 | $file_content = $wp_filesystem->get_contents( $file_name ); 236 | } 237 | } 238 | $post_args = array_merge( $post_args, array( 'document' => $file_content ) ); 239 | } 240 | } 241 | 242 | return $post_args; 243 | 244 | } 245 | 246 | /** 247 | * Remove synonym terms from the index 248 | * @since 0.2.2 249 | * @param 250 | * @return 251 | */ 252 | public static function delete() { 253 | } 254 | 255 | /** 256 | * Get allowed mime types 257 | * 258 | * @since 0.2.2 259 | * @return array 260 | */ 261 | public static function allowed_mime_types() { 262 | $mine_types = array( 263 | 'pdf' => 'application/pdf', 264 | 'ppt' => 'application/vnd.ms-powerpoint', 265 | 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 266 | 'xls' => 'application/vnd.ms-excel', 267 | 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 268 | 'doc' => 'application/msword', 269 | 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 270 | ); 271 | 272 | return apply_filters( 'wp_redisearch_allowed_documents_mime_types', $mine_types); 273 | } 274 | } -------------------------------------------------------------------------------- /src/Features/LiveSearch/LiveSearch.php: -------------------------------------------------------------------------------- 1 | return(); 33 | $this->query = new Query( $client, Settings::indexName() ); 34 | 35 | Features::init()->register_feature( 'live-search', array( 36 | 'title' => 'Live Search', 37 | 'setup_cb' => array( $this, 'setup' ), 38 | 'requirements_cb' => array( $this, 'requirements' ), 39 | 'activation_cb' => array( $this, 'activated' ), 40 | 'deactivation_cb' => array( $this, 'deactivated' ), 41 | 'feature_desc_cb' => array( $this, 'feature_desc' ), 42 | 'feature_options_cb' => array( $this, 'feature_options' ), 43 | 'requires_reindex' => false, 44 | 'deactivation_requires_reindex' => false, 45 | ) ); 46 | } 47 | 48 | /** 49 | * This method will run on each page load. 50 | * You can hook functions which must run always. 51 | * 52 | * @since 0.2.0 53 | */ 54 | public function requirements() { 55 | $status = new \stdClass(); 56 | $status->code = 2; 57 | $status->message = array(); 58 | $status->message[] = __( 'Re-indexing is highly recommended.', 'wp-redisearch' ); 59 | 60 | return $status; 61 | } 62 | 63 | /** 64 | * This method will run on each page load. 65 | * You can hook functions which must run always. 66 | * 67 | * @since 0.2.0 68 | */ 69 | public function setup () { 70 | add_action('wp_ajax_wp_redisearch_get_suggestion', array( $this, 'get_suggestion' ) ); 71 | add_action('wp_ajax_nopriv_wp_redisearch_get_suggestion', array( $this, 'get_suggestion' ) ); 72 | add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_script' ) ); 73 | } 74 | 75 | /** 76 | * Enqueue script and styles for live search 77 | * 78 | * @since 0.2.0 79 | */ 80 | public function enqueue_script () { 81 | wp_enqueue_script( 'wp_redisearch_live_search', WPRS_URL . 'src/Features/LiveSearch/assets/js/live-search.min.js', array( 'jquery' ), WPRS_VERSION, true ); 82 | $localized_data = array( 83 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ) 84 | ); 85 | wp_localize_script( 'wp_redisearch_live_search', 'wpRds', $localized_data ); 86 | wp_enqueue_style( 'wp_redisearch_public_css', WPRS_URL . 'src/Features/LiveSearch/assets/css/live-search.css', array(), WPRS_VERSION ); 87 | } 88 | 89 | /** 90 | * Fires after feature activation. 91 | * 92 | * @since 0.2.0 93 | */ 94 | public function activated () { 95 | } 96 | 97 | /** 98 | * Fires after feature deactivation. 99 | * 100 | * @since 0.2.0 101 | */ 102 | public function deactivated () { 103 | } 104 | 105 | /** 106 | * Feature description. 107 | * This will be added to feature setting box. 108 | * 109 | * @since 0.2.0 110 | */ 111 | public function feature_desc () { 112 | ?> 113 |

114 |

115 | post_title; 157 | $post_permalink = $permalink = get_permalink( $post->ID ); 158 | self::add( $index_name, $post_title, $post_permalink, 1 ); 159 | } 160 | 161 | /** 162 | * Add auto suggestion to the index. 163 | * 164 | * @since 0.2.0 165 | * @param string $index_name Index name 166 | * @param string $post_title Post title 167 | * @param string $post_permalink Post permalink 168 | * @param float $score Score for the term 169 | * @return 170 | */ 171 | public static function add( $index_name, $post_title, $post_permalink, $score ) { 172 | // First, lets make sure it it does not exists 173 | try { 174 | $command = array_merge( [$index_name . 'Sugg', $post_title] ); 175 | self::$client->rawCommand('FT.SUGDEL', $command); 176 | } catch (\Exception $e) { 177 | } 178 | // Prepare command for adding post 179 | $command = array_merge( [$index_name . 'Sugg', $post_title , $score, 'PAYLOAD', $post_permalink] ); 180 | self::$client->rawCommand('FT.SUGADD', $command); 181 | } 182 | 183 | /** 184 | * Remove synonym terms from the index 185 | * @since 0.2.0 186 | * @param string $index_name Index name 187 | * @param object $post The post object 188 | * @return 189 | */ 190 | public static function delete( $index_name, $post ) { 191 | $post_title = $post->post_title; 192 | $command = array_merge( [$index_name . 'Sugg', $post_title] ); 193 | self::$client->rawCommand('FT.SUGDEL', $command); 194 | } 195 | 196 | /** 197 | * Ajax callback for getting suggestion results 198 | * @since 0.2.0 199 | * @param 200 | * @return string $suggestion_results 201 | */ 202 | public function get_suggestion() { 203 | $noResults = Settings::get( 'wp_redisearch_suggested_results', 10 ); 204 | $term = $_POST['term']; 205 | $suggestion_results = $this->query 206 | ->limit(0, $noResults ) 207 | ->search( $term . '*' ); 208 | wp_send_json( $suggestion_results->getDocuments() ); 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /src/Features/LiveSearch/assets/css/live-search.css: -------------------------------------------------------------------------------- 1 | .autocomplete { 2 | /*the container must be positioned relative:*/ 3 | position: relative; 4 | display: inline-block; } 5 | 6 | .autocomplete-items { 7 | position: absolute; 8 | border: 1px solid #d4d4d4; 9 | border-bottom: none; 10 | border-top: none; 11 | z-index: 99; 12 | /*position the autocomplete items to be the same width as the container:*/ 13 | top: 100%; 14 | left: 0; 15 | right: 0; } 16 | 17 | .autocomplete-items div { 18 | padding: 10px; 19 | cursor: pointer; 20 | background-color: #fff; 21 | border-bottom: 1px solid #d4d4d4; } 22 | 23 | .autocomplete-items div:hover { 24 | /*when hovering an item:*/ 25 | background-color: #e9e9e9; } 26 | 27 | .autocomplete-active { 28 | /*when navigating through the items using the arrow keys:*/ 29 | background-color: DodgerBlue !important; 30 | color: #ffffff; } 31 | -------------------------------------------------------------------------------- /src/Features/LiveSearch/assets/js/live-search.min.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function(){document.querySelectorAll('[name="s"]').forEach(e=>{var t;function n(e){if(!e)return!1;!function(e){for(var t=0;t=e.length&&(t=0),t<0&&(t=e.length-1),e[t].classList.add("autocomplete-active")}function i(t){for(var n=document.getElementsByClassName("autocomplete-items"),i=0;i"+a[o].post_title.substr(0,t.length)+"",s.innerHTML+=a[o].post_title.substr(t.length),s.innerHTML+="",s.addEventListener("click",function(t){e.value=this.getElementsByTagName("input")[0].value,e.closest("form").submit(),i()}),n.appendChild(s)}})}(s,o)}),e.addEventListener("keydown",function(e){var i=document.getElementById(this.id+"autocomplete-list");i&&(i=i.getElementsByTagName("div")),40==e.keyCode?(t++,n(i)):38==e.keyCode?(t--,n(i)):13==e.keyCode&&t>-1&&i&&i[t].click()}),document.addEventListener("click",function(e){i(e.target)})})}); -------------------------------------------------------------------------------- /src/Features/LiveSearch/assets/js/src/live-search.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $(document).ready(function () { 3 | 4 | function autocomplete(input) { 5 | input.forEach(inp => { 6 | inp.setAttribute("autocomplete", "off"); 7 | var currentFocus; 8 | inp.addEventListener("input", function(e) { 9 | var a, val = this.value; 10 | closeAllLists(); 11 | if (!val) { return false;} 12 | currentFocus = -1; 13 | this.parentNode.style.position = "relative"; 14 | this.parentNode.style.display = "block"; 15 | a = document.createElement("DIV"); 16 | a.setAttribute("id", this.id + "autocomplete-list"); 17 | a.setAttribute("class", "autocomplete-items"); 18 | this.parentNode.appendChild(a); 19 | getSuggestion(val, a); 20 | }); 21 | inp.addEventListener("keydown", function(e) { 22 | var x = document.getElementById(this.id + "autocomplete-list"); 23 | if (x) x = x.getElementsByTagName("div"); 24 | if (e.keyCode == 40) { 25 | currentFocus++; 26 | addActive(x); 27 | } else if (e.keyCode == 38) { 28 | currentFocus--; 29 | addActive(x); 30 | } else if (e.keyCode == 13) { 31 | // e.preventDefault(); 32 | if (currentFocus > -1) { 33 | if (x) x[currentFocus].click(); 34 | } 35 | } 36 | }); 37 | function addActive(x) { 38 | if (!x) return false; 39 | removeActive(x); 40 | if (currentFocus >= x.length) currentFocus = 0; 41 | if (currentFocus < 0) currentFocus = (x.length - 1); 42 | x[currentFocus].classList.add("autocomplete-active"); 43 | } 44 | function removeActive(x) { 45 | for (var i = 0; i < x.length; i++) { 46 | x[i].classList.remove("autocomplete-active"); 47 | } 48 | } 49 | function closeAllLists(elmnt) { 50 | var x = document.getElementsByClassName("autocomplete-items"); 51 | for (var i = 0; i < x.length; i++) { 52 | if (elmnt != x[i] && elmnt != inp) { 53 | x[i].parentNode.removeChild(x[i]); 54 | } 55 | } 56 | } 57 | function getSuggestion(term, a) { 58 | let postData = { 59 | action: 'wp_redisearch_get_suggestion', 60 | term: term 61 | } 62 | jQuery.ajax({ 63 | data: postData, 64 | type: "post", 65 | url: wpRds.ajaxUrl, 66 | success: function (data) { 67 | let results = data; 68 | let b; 69 | for (let i = 0; i < results.length; i = i + 2) { 70 | b = document.createElement("DIV"); 71 | b.innerHTML = "" + results[i].post_title.substr(0, term.length) + ""; 72 | b.innerHTML += results[i].post_title.substr(term.length); 73 | b.innerHTML += ""; 74 | b.addEventListener("click", function(e) { 75 | inp.value = this.getElementsByTagName("input")[0].value; 76 | inp.closest('form').submit(); 77 | closeAllLists(); 78 | }); 79 | a.appendChild(b); 80 | } 81 | } 82 | }); 83 | } 84 | document.addEventListener("click", function (e) { 85 | closeAllLists(e.target); 86 | }); 87 | }); 88 | } 89 | 90 | autocomplete(document.querySelectorAll('[name="s"]')); 91 | 92 | }); 93 | } )( jQuery ); 94 | -------------------------------------------------------------------------------- /src/Features/Synonym.php: -------------------------------------------------------------------------------- 1 | return(); 27 | $this->index = new Index($client); 28 | $this->index->setIndexName( Settings::indexName() ); 29 | 30 | Features::init()->register_feature( 'synonym', array( 31 | 'title' => 'Synonym', 32 | 'setup_cb' => array( $this, 'setup' ), 33 | 'activation_cb' => array( $this, 'activated' ), 34 | 'deactivation_cb' => array( $this, 'deactivated' ), 35 | 'feature_desc_cb' => array( $this, 'feature_desc' ), 36 | 'feature_options_cb' => array( $this, 'feature_options' ), 37 | 'requires_reindex' => true, 38 | 'deactivation_requires_reindex' => true, 39 | ) ); 40 | } 41 | 42 | /** 43 | * This method will run on each page load. 44 | * You can hook functions which must run always. 45 | * 46 | * @since 0.2.0 47 | */ 48 | public function setup () { 49 | add_action( 'wp_redisearch_after_index_created', array( $this, 'add' ) ); 50 | } 51 | 52 | /** 53 | * Fires after feature activation. 54 | * 55 | * @since 0.2.0 56 | */ 57 | public function activated () { 58 | // self::add(); 59 | } 60 | 61 | /** 62 | * Fires after feature deactivation. 63 | * 64 | * @since 0.2.0 65 | */ 66 | public function deactivated () { 67 | $this->delete(); 68 | } 69 | 70 | /** 71 | * Feature description. 72 | * This will be added to feature setting box. 73 | * 74 | * @since 0.2.0 75 | */ 76 | public function feature_desc () { 77 | ?> 78 |

79 | For example:
boy, child, baby
girl, child, baby
man, person, adult

When these three groups are located inside the synonym data structure, it is possible to search for \'child\' and receive documents contains \'boy\', \'girl\', \'child\' and \'baby\'.
Keep in mined, only those posts indexed after adding synonyms list will be affected.', 'wp-redisearch' ) ); 91 | } 92 | 93 | /** 94 | * Add synonym terms to the index 95 | * @return void 96 | * @since 0.2.0 97 | */ 98 | public function add() { 99 | $synonym_terms = Settings::get( 'wp_redisearch_synonyms_list' ); 100 | if ( !isset( $synonym_terms ) || empty( $synonym_terms ) ) { 101 | return; 102 | } 103 | $synonym_terms = preg_split("/\\r\\n|\\r|\\n/", $synonym_terms ); 104 | $synonym_terms = array_map( function ($terms) { 105 | return explode(',', $terms); 106 | }, $synonym_terms ); 107 | $this->index->synonymAdd( $synonym_terms ); 108 | } 109 | 110 | /** 111 | * Remove synonym terms from the index 112 | * @return void 113 | * @since 0.2.0 114 | */ 115 | public function delete() { 116 | $this->index->synonymDump(); 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/Features/Woocommerce.php: -------------------------------------------------------------------------------- 1 | return(); 26 | $this->index = new Index($client); 27 | $this->index->setIndexName( Settings::indexName() ); 28 | 29 | Features::init()->register_feature( 'woocommerce', array( 30 | 'title' => 'WooCommerce', 31 | 'setup_cb' => array( $this, 'setup' ), 32 | 'requirements_cb' => array( $this, 'requirements' ), 33 | 'activation_cb' => array( $this, 'activated' ), 34 | 'deactivation_cb' => array( $this, 'deactivated' ), 35 | 'feature_desc_cb' => array( $this, 'feature_desc' ), 36 | 'feature_options_cb' => array( $this, 'feature_options' ), 37 | 'requires_reindex' => true, 38 | 'deactivation_requires_reindex' => false, 39 | ) ); 40 | } 41 | 42 | /** 43 | * This method will run on each page load. 44 | * You can hook functions which must run always. 45 | * 46 | * @since 0.2.1 47 | */ 48 | public function setup () { 49 | add_filter( 'wp_redisearch_indexable_post_types', array( $this, 'indexable_post_type' ) ); 50 | add_filter( 'wp_redisearch_indexable_meta_keys', array( $this, 'indexable_meta_keys' ) ); 51 | add_filter( 'wp_redisearch_indexable_temrs', array( $this, 'indexable_terms' ), 10, 2 ); 52 | } 53 | 54 | /** 55 | * Add product to indexable post types list. 56 | * 57 | * @param array $post_types Array of indexable post types 58 | * @since 0.2.1 59 | * @return array $post_types 60 | */ 61 | public function indexable_post_type ( $post_types ) { 62 | return array_merge( $post_types, array( 'product' ) ); 63 | } 64 | 65 | /** 66 | * Add product meta keys to indexable meta keys 67 | * 68 | * @param array $meta Existing meta keys 69 | * @since 0.2.1 70 | * @return array 71 | */ 72 | public function indexable_meta_keys( $meta ) { 73 | $indexable_keys = array_unique( 74 | array( 75 | '_thumbnail_id', 76 | '_product_attributes', 77 | '_wpb_vc_js_status', 78 | '_swatch_type', 79 | 'total_sales', 80 | '_downloadable', 81 | '_virtual', 82 | '_regular_price', 83 | '_sale_price', 84 | '_tax_status', 85 | '_tax_class', 86 | '_purchase_note', 87 | '_featured', 88 | '_weight', 89 | '_length', 90 | '_width', 91 | '_height', 92 | '_visibility', 93 | '_sku', 94 | '_sale_price_dates_from', 95 | '_sale_price_dates_to', 96 | '_price', 97 | '_sold_individually', 98 | '_manage_stock', 99 | '_backorders', 100 | '_stock', 101 | '_upsell_ids', 102 | '_crosssell_ids', 103 | '_stock_status', 104 | '_product_version', 105 | '_product_tabs', 106 | '_override_tab_layout', 107 | '_suggested_price', 108 | '_min_price', 109 | '_customer_user', 110 | '_variable_billing', 111 | '_wc_average_rating', 112 | '_product_image_gallery', 113 | '_bj_lazy_load_skip_post', 114 | '_min_variation_price', 115 | '_max_variation_price', 116 | '_min_price_variation_id', 117 | '_max_price_variation_id', 118 | '_min_variation_regular_price', 119 | '_max_variation_regular_price', 120 | '_min_regular_price_variation_id', 121 | '_max_regular_price_variation_id', 122 | '_min_variation_sale_price', 123 | '_max_variation_sale_price', 124 | '_min_sale_price_variation_id', 125 | '_max_sale_price_variation_id', 126 | '_default_attributes', 127 | '_swatch_type_options', 128 | '_order_key', 129 | '_billing_company', 130 | '_billing_address_1', 131 | '_billing_address_2', 132 | '_billing_city', 133 | '_billing_postcode', 134 | '_billing_country', 135 | '_billing_state', 136 | '_billing_email', 137 | '_billing_phone', 138 | '_shipping_address_1', 139 | '_shipping_address_2', 140 | '_shipping_city', 141 | '_shipping_postcode', 142 | '_shipping_country', 143 | '_shipping_state', 144 | '_billing_last_name', 145 | '_billing_first_name', 146 | '_shipping_first_name', 147 | '_shipping_last_name', 148 | ) 149 | ); 150 | 151 | return array_merge( $meta, $indexable_keys ); 152 | } 153 | 154 | /** 155 | * Index WooCommerce taxonomy names. 156 | * 157 | * @param array $terms Array of indexable taxonomy names 158 | * @param array $post Post(product) properties array. 159 | * @since 0.2.1 160 | * @return array 161 | */ 162 | public function indexable_terms ( $terms, $post ) { 163 | $wc_taxonomies = array( 'product_type', 'product_visibility', ); 164 | 165 | if ( $attribute_taxonomies = wc_get_attribute_taxonomies() ) { 166 | foreach ( $attribute_taxonomies as $tax ) { 167 | if ( $name = wc_attribute_taxonomy_name( $tax->attribute_name ) ) { 168 | if ( empty( $tax->attribute_) ) { 169 | $wc_taxonomies[] = $name; 170 | } 171 | } 172 | } 173 | } 174 | 175 | return array_merge( $terms, $wc_taxonomies ); 176 | } 177 | 178 | /** 179 | * Check if WooCommerce plugin installed. 180 | * 181 | * @since 0.2.1 182 | */ 183 | public function requirements () { 184 | $status = new \stdClass(); 185 | if ( ! class_exists( 'WooCommerce' ) ) { 186 | $status->code = 1; 187 | $status->message = esc_html__( 'WooCommerce plugin not installed.', 'wp-redisearch' ); 188 | } else { 189 | $status->code = 0; 190 | $status->message = ''; 191 | } 192 | 193 | return $status; 194 | } 195 | 196 | /** 197 | * Fires after feature activation. 198 | * 199 | * @since 0.2.1 200 | */ 201 | public function activated () { 202 | } 203 | 204 | /** 205 | * Fires after feature deactivation. 206 | * 207 | * @since 0.2.1 208 | */ 209 | public function deactivated () { 210 | } 211 | 212 | /** 213 | * Feature description. 214 | * This will be added to feature setting box. 215 | * 216 | * @since 0.2.1 217 | */ 218 | public function feature_desc () { 219 | ?> 220 |

221 | client = Setup::connect( 18 | Settings::RedisServer(), 19 | Settings::RedisPort(), 20 | Settings::RedisPassword(), 21 | 0, 22 | Settings::RedisScheme() 23 | ); 24 | } 25 | 26 | public function return() { 27 | return $this->client; 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/RediSearch/Index.php: -------------------------------------------------------------------------------- 1 | client = $client; 26 | 27 | $index = new RedisearchIndex($this->client); 28 | // Set index name. 29 | $index->setIndexName(Settings::indexName()); 30 | 31 | $index->on('HASH'); 32 | $this->index = $index; 33 | } 34 | 35 | /** 36 | * Create connection to redis server. 37 | * @since 0.1.0 38 | * @param 39 | * @return 40 | */ 41 | public function create() { 42 | // First of all, we reset saved index_meta from options 43 | $num_docs = 0; 44 | if ( isset( WpRediSearch::$indexInfo ) && gettype( WpRediSearch::$indexInfo ) == 'array' ) { 45 | $num_docs_offset = array_search( 'num_docs', WpRediSearch::$indexInfo ) + 1; 46 | $num_docs = WpRediSearch::$indexInfo[$num_docs_offset]; 47 | } 48 | if ( $num_docs == 0 ) { 49 | delete_option( 'wp_redisearch_index_meta' ); 50 | } 51 | 52 | $indexName = Settings::indexName(); 53 | 54 | $prefixes = array(); 55 | 56 | $postTypes = Settings::get( 'wp_redisearch_post_types' ); 57 | 58 | if ( isset( $postTypes ) && !empty( $postTypes ) ) { 59 | $postTypes = array_keys( $postTypes ); 60 | } elseif ( !isset( $postTypes ) || empty( $postTypes ) ) { 61 | $postTypes = array( 'post' ); 62 | } 63 | 64 | $postTypes = apply_filters( 'wp_redisearch_indexable_post_types', $postTypes ); 65 | 66 | foreach ($postTypes as $postType ) { 67 | $prefixes[] = $indexName . ':' . $postType; 68 | } 69 | $this->index->setPrefix($prefixes); 70 | // Setting a field name for score so we don't need to parse index info everytime 71 | $this->index->setScoreField('documentScore'); 72 | // Delete the index. 73 | $this->index->drop(); 74 | 75 | $indexableFields = array( 76 | 'post_title' => array( 77 | 'type' => 'TEXT', 78 | 'weight' => 5.0, 79 | 'sortable' => TRUE 80 | ), 81 | 'post_content' => array( 82 | 'type' => 'TEXT' 83 | ), 84 | 'post_content_filtered' => array( 85 | 'type' => 'TEXT' 86 | ), 87 | 'post_excerpt' => array( 88 | 'type' => 'TEXT' 89 | ), 90 | 'post_type' => array( 91 | 'type' =>'TEXT' 92 | ), 93 | 'post_author' => array( 94 | 'type' => 'TEXT' 95 | ), 96 | 'post_id' => array( 97 | 'type' => 'NUMERIC', 98 | 'sortable' => TRUE 99 | ), 100 | 'menu_order' => array( 101 | 'type' => 'NUMERIC' 102 | ), 103 | 'permalink' => array( 104 | 'type' => 'TEXT' 105 | ), 106 | 'post_date' => array( 107 | 'type' => 'NUMERIC', 108 | 'sortable' => TRUE 109 | ) 110 | ); 111 | 112 | /** 113 | * Filter index-able post meta 114 | * Allows for specifying public or private meta keys that may be indexed. 115 | * @since 0.2.0 116 | * @param array Array 117 | */ 118 | $indexableMetaKeys = apply_filters( 'wp_redisearch_indexable_meta_keys', array() ); 119 | 120 | $metaSchema = array(); 121 | 122 | if ( isset( $indexableMetaKeys ) && !empty( $indexableMetaKeys ) ) { 123 | foreach ($indexableMetaKeys as $meta) { 124 | $metaSchema[$meta] = array( 125 | 'type' => 'TEXT' 126 | ); 127 | } 128 | } 129 | /** 130 | * Filter index-able post meta schema 131 | * Allows for manipulating schema of public or private meta keys. 132 | * @since 0.2.0 133 | * @param array $metaSchema Array of index-able meta key schemas. 134 | * @param array $indexableMetaKeys Array of index-able meta keys. 135 | */ 136 | $metaSchema = apply_filters( 'wp_redisearch_indexable_meta_schema', $metaSchema, $indexableMetaKeys ); 137 | 138 | $indexableTerms = array_keys( Settings::get( 'wp_redisearch_indexable_terms', array() ) ); 139 | $termsSchema = array(); 140 | if ( isset( $indexableTerms ) && !empty( $indexableTerms ) ) { 141 | foreach ($indexableTerms as $term) { 142 | $termsSchema[$term] = array( 143 | 'type' => 'TAG' 144 | ); 145 | } 146 | } 147 | 148 | $indexableFields = array_merge( $indexableFields, $metaSchema, $termsSchema ); 149 | 150 | /** 151 | * Stop words support. 152 | * If disabled from settings page, then we will add no stop words. 153 | * @since 0.2.5 154 | */ 155 | $stop_words_disabled = Settings::get( 'wp_redisearch_disable_stop_words', false ); 156 | if ( $stop_words_disabled ) { 157 | $this->index->noStopWords(); 158 | } else { 159 | $stopWords = Settings::get( 'wp_redisearch_stop_words', null ); 160 | if ( isset( $stopWords ) && $stopWords != null ) { 161 | $stopWordsArray = explode( ',', $stopWords); 162 | $stopWordsArray = array_map( 'trim', $stopWordsArray ); 163 | if ( count( $stopWordsArray ) !== 0 ) { 164 | $this->index->setStopWords( $stopWordsArray ); 165 | } 166 | } 167 | } 168 | 169 | // Loop through the fields. 170 | foreach ( $indexableFields as $name => $field ) { 171 | $type = $field['type']; 172 | if (!empty($type)) { 173 | switch ($type) { 174 | case 'NUMERIC': 175 | $this->index->addNumericField( $name, $field['sortable'] ?? FALSE ); 176 | break; 177 | 178 | case 'TAG': 179 | $this->index->addTagField( $name ); 180 | break; 181 | 182 | case 'GEO': 183 | $this->index->addGeoField( $name ); 184 | break; 185 | 186 | default: 187 | $weight = '1.0'; 188 | if ( isset( $field['weight'] ) ) { 189 | $weight = $field['weight']; 190 | } 191 | $this->index->addTextField( $name, $weight, $field['sortable'] ?? FALSE ); 192 | } 193 | } 194 | } 195 | 196 | // Save/Create the Index. 197 | $this->index->create(); 198 | 199 | /** 200 | * Action wp_redisearch_after_index_created fires after index created. 201 | * Some features need to do something after activation. Some of them trigger re-indexing. 202 | * But after they do what they suppose to do with the index, the index will be deleted to re-index the site. 203 | * So those features can use this filter instead. 204 | * 205 | * @since 0.2.0 206 | * @param array $client Created redis client instance 207 | */ 208 | do_action( 'wp_redisearch_after_index_created', $this->client); 209 | 210 | return $this; 211 | } 212 | 213 | /** 214 | * Prepare items (posts) to be indexed. 215 | * @since 0.1.0 216 | * @param 217 | * @return object $this 218 | */ 219 | public function add() { 220 | $index_meta = get_option( 'wp_redisearch_index_meta' ); 221 | if ( empty( $index_meta ) ) { 222 | $index_meta['offset'] = 0; 223 | } 224 | $posts_per_page = apply_filters( 'wp_redisearch_posts_per_page', Settings::get( 'wp_redisearch_indexing_batches', 20 ) ); 225 | 226 | $default_args = Settings::query_args(); 227 | $default_args['posts_per_page'] = $posts_per_page; 228 | $default_args['offset'] = $index_meta['offset']; 229 | 230 | $args = apply_filters( 'wp_redisearch_posts_args', $default_args); 231 | 232 | /** 233 | * filter wp_redisearch_before_index_wp_query 234 | * Fires before wp_query. This is useful if you want for some reasons, manipulate WP_Query 235 | * 236 | * @since 0.2.2 237 | * @param array $args Array of arguments passed to WP_Query 238 | * @return array $args Array of manipulated arguments 239 | */ 240 | $args = apply_filters( 'wp_redisearch_before_index_wp_query', $args ); 241 | 242 | $query = new \WP_Query( $args ); 243 | 244 | /** 245 | * filter wp_redisearch_after_index_wp_query 246 | * Fires after wp_query. This is useful if you want to manipulate results of WP_Query 247 | * 248 | * @since 0.2.2 249 | * @param array $args Array of arguments passed to WP_Query 250 | * @param object $query Result object of WP_Query 251 | */ 252 | $query = apply_filters( 'wp_redisearch_after_index_wp_query', $query, $args ); 253 | 254 | $index_meta['found_posts'] = $query->found_posts; 255 | 256 | if ( $index_meta['offset'] >= $index_meta['found_posts'] ) { 257 | $index_meta['offset'] = $index_meta['found_posts']; 258 | } 259 | 260 | if ( $query->have_posts() ) { 261 | $indexName = Settings::indexName(); 262 | 263 | while ( $query->have_posts() ) { 264 | $query->the_post(); 265 | $indexingOptions = array(); 266 | 267 | $id = get_the_id(); 268 | // Post language. This could be useful to do some stop word, stemming and etc. 269 | $indexingOptions['language'] = apply_filters( 'wp_redisearch_index_language', 'english', $id ); 270 | $indexingOptions['fields'] = $this->preparePost( get_the_id() ); 271 | 272 | $this->addPosts( $id, $indexingOptions ); 273 | 274 | /** 275 | * Action wp_redisearch_after_post_indexed fires after post added to the index. 276 | * Since this action called from within post loop, all Wordpress functions for post are available in the calback. 277 | * Example: 278 | * To get post title, you can simply call 'get_the_title()' function 279 | * 280 | * @since 0.2.0 281 | * @param array $client Created redis client instance 282 | * @param array $indexName Index name 283 | * @param array $indexingOptions Posts extra options like language and fields 284 | */ 285 | do_action( 'wp_redisearch_after_post_indexed', $this->client, $indexName, $indexingOptions ); 286 | } 287 | $index_meta['offset'] = absint( $index_meta['offset'] + $posts_per_page ); 288 | update_option( 'wp_redisearch_index_meta', $index_meta ); 289 | } 290 | return $index_meta; 291 | } 292 | 293 | /** 294 | * Prepare a post for indexing. 295 | * 296 | * @param object $post_id 297 | * @since 0.1.0 298 | * @return bool|array 299 | */ 300 | public function preparePost( $post_id ) { 301 | $post = get_post( $post_id ); 302 | $user = get_userdata( $post->post_author ); 303 | 304 | if ( $user instanceof WP_User ) { 305 | $user_data = $user->display_name; 306 | } else { 307 | $user_data = ''; 308 | } 309 | 310 | $post_date = $post->post_date; 311 | // If date is invalid, set it to null 312 | if ( ! strtotime( $post_date ) || $post_date === "0000-00-00 00:00:00" ) { 313 | $post_date = null; 314 | } 315 | 316 | 317 | $post_categories = get_the_category( $post->ID ); 318 | 319 | $post_args = array( 320 | 'post_id' => $post->ID, 321 | 'post_author' => $user_data, 322 | 'post_date' => strtotime( $post_date ), 323 | 'post_title' => $post->post_title, 324 | 'post_excerpt' => $post->post_excerpt, 325 | 'post_content_filtered' => wp_strip_all_tags( apply_filters( 'the_content', $post->post_content ), true ), 326 | 'post_content' => wp_strip_all_tags( $post->post_content, true ), 327 | 'post_type' => $post->post_type, 328 | 'permalink' => get_permalink( $post->ID ), 329 | 'menu_order' => absint( $post->menu_order ) 330 | ); 331 | 332 | $post_terms = apply_filters( 'wp_redisearch_prepared_terms', $this->prepare_terms( $post ), $post ); 333 | 334 | $prepared_meta = $this->prepare_meta( $post->ID ); 335 | 336 | $post_args = array_merge( $post_args, $post_terms, $prepared_meta ); 337 | 338 | $post_args = apply_filters( 'wp_redisearch_prepared_post_args', $post_args, $post ); 339 | 340 | return $post_args; 341 | } 342 | 343 | /** 344 | * Prepare post terms. 345 | * @since 0.1.0 346 | * @param integer $post 347 | * @return string 348 | */ 349 | private function prepare_terms( $post ) { 350 | $indexableTerms = Settings::get( 'wp_redisearch_indexable_terms' ); 351 | $indexableTerms = isset( $indexableTerms ) ? array_keys( $indexableTerms ) : array(); 352 | 353 | /** 354 | * Filter wp_redisearch_indexable_temrs to manipulate indexable terms list 355 | * 356 | * @since 0.2.1 357 | * @param array $indexableTerms Default terms list 358 | * @param array $post The post object 359 | * @return array $indexableTerms Modified taxobomy terms list 360 | */ 361 | $indexableTerms = apply_filters( 'wp_redisearch_indexable_temrs', $indexableTerms, $post ); 362 | 363 | if ( empty( $indexableTerms ) ) { 364 | return array(); 365 | } 366 | 367 | $terms = array(); 368 | foreach ( $indexableTerms as $taxonomy ) { 369 | 370 | $post_terms = get_the_terms( $post->ID, $taxonomy ); 371 | 372 | if ( ! $post_terms || is_wp_error( $post_terms ) ) { 373 | continue; 374 | } 375 | 376 | $terms_dic = []; 377 | 378 | foreach ( $post_terms as $term ) { 379 | $terms_dic[] = $term->name; 380 | } 381 | $terms_dic = implode( ',', $terms_dic ); 382 | $terms[$taxonomy] = ltrim( $terms_dic ); 383 | } 384 | 385 | return $terms; 386 | } 387 | 388 | /** 389 | * Prepare post meta. 390 | * @since 0.2.1 391 | * @param integer $post_id 392 | * @return array $prepared_meta 393 | */ 394 | public function prepare_meta( $post_id ) { 395 | $post_meta = (array) get_post_meta( $post_id ); 396 | 397 | if ( empty( $post_meta ) ) { 398 | return array(); 399 | } 400 | 401 | $prepared_meta = array(); 402 | 403 | /** 404 | * Filter index-able post meta 405 | * Allows for specifying public or private meta keys that may be indexed. 406 | * @since 0.2.0 407 | * @param array Array 408 | */ 409 | $indexableMetaKeys = apply_filters( 'wp_redisearch_indexable_meta_keys', array() ); 410 | 411 | foreach( $post_meta as $key => $value ) { 412 | if ( in_array( $key, $indexableMetaKeys ) ) { 413 | $extracted_value = maybe_unserialize( $value[0] ); 414 | $prepared_meta[$key] = is_array( $extracted_value ) ? json_encode( maybe_unserialize( $value[0] ) ) : $extracted_value; 415 | } 416 | } 417 | 418 | return $prepared_meta; 419 | } 420 | 421 | /** 422 | * Add to index or in other term, index items. 423 | * 424 | * @param $id 425 | * @param array $indexingOptions 426 | * 427 | * @return object $index 428 | * @since 0.1.0 429 | */ 430 | public function addPosts( $id, array $indexingOptions ) { 431 | 432 | $document = new \FKRediSearch\Document; 433 | $document->setLanguage( $indexingOptions['language'] ); 434 | 435 | $documentScore = $indexingOptions['score'] ?? 1; 436 | $document->setScore( $documentScore ); 437 | $document->setId( $this->index->getIndexName() . ':' . get_post_type( $id ) . ':' . $id ); 438 | 439 | $document->setFields( $indexingOptions['fields'] ); 440 | 441 | $this->index->add( $document ); 442 | 443 | return $this; 444 | } 445 | 446 | /** 447 | * Delete post from index. 448 | * @since 0.1.0 449 | * @param 450 | * @return object $this 451 | */ 452 | public function deletePost($id) { 453 | $command = array( $this->index->getIndexName(), $id , 'DD' ); 454 | $this->client->rawCommand('FT.DEL', $command); 455 | return $this; 456 | } 457 | 458 | /** 459 | * Write entire redisearch index to the disk to persist it. 460 | * @since 0.1.0 461 | * @param 462 | * @return 463 | */ 464 | public function writeToDisk() { 465 | return $this->client->rawCommand('SAVE', []); 466 | } 467 | 468 | } -------------------------------------------------------------------------------- /src/RediSearch/Search.php: -------------------------------------------------------------------------------- 1 | client = $client; 18 | } 19 | 20 | /** 21 | * Search in the index. 22 | * @since 0.1.0 23 | * @param object $query 24 | * @return 25 | */ 26 | public function search( $wp_query ) { 27 | $index_name = Settings::indexName(); 28 | $wprds_query = $wp_query->query_vars['s']; 29 | // Offset search results based on pagination 30 | $from = 0; 31 | $offset = $wp_query->query_vars['posts_per_page']; 32 | if ( isset( $wp_query->query_vars['paged'] ) && $wp_query->query_vars['paged'] > 1 ) { 33 | $from = $wp_query->query_vars['posts_per_page'] * ( $wp_query->query_vars['paged'] - 1 ); 34 | } 35 | $search_results = $this->client->rawCommand('FT.SEARCH', [$index_name, $wprds_query, 'NOCONTENT', 'LIMIT', $from, $offset]); 36 | return $search_results; 37 | } 38 | } -------------------------------------------------------------------------------- /src/RedisRaw/AbstractRedisRawClient.php: -------------------------------------------------------------------------------- 1 | redis->flushAll(); 16 | } 17 | 18 | public function multi(bool $usePipeline = false) { 19 | } 20 | 21 | public function rawCommand(string $command, array $arguments){ 22 | } 23 | 24 | public function prepareRawCommandArguments(string $command, array $arguments) : array { 25 | foreach ($arguments as $index => $argument) { 26 | if (!is_scalar($arguments[$index])) { 27 | $arguments[$index] = (string)$argument; 28 | } 29 | } 30 | 31 | array_unshift($arguments, $command); 32 | return $arguments; 33 | } 34 | 35 | 36 | public function normalizeRawCommandResult($rawResult) { 37 | return $rawResult === 'OK' ? true : $rawResult; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/RedisRaw/PredisAdapter.php: -------------------------------------------------------------------------------- 1 | 'tcp', 24 | 'host' => $hostname, 25 | 'port' => $port, 26 | 'database' => $db, 27 | 'password' => $password, 28 | ); 29 | } else { 30 | $clientArgs = array( 31 | 'scheme' => 'unix', 32 | 'path' => $host 33 | ); 34 | } 35 | $this->redis = new Client( $clientArgs ); 36 | $this->redis->connect(); 37 | return $this; 38 | } 39 | 40 | public function multi(bool $usePipeline = false) { 41 | return $this->redis->pipeline(); 42 | } 43 | 44 | public function rawCommand(string $command, array $arguments) { 45 | $preparedArguments = $this->prepareRawCommandArguments($command, $arguments); 46 | $rawResult = $this->redis->executeRaw($preparedArguments); 47 | return $this->normalizeRawCommandResult($rawResult); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RedisRaw/RedisRawClientInterface.php: -------------------------------------------------------------------------------- 1 | $post_types, 160 | 'post_status' => $post_status, 161 | 'ignore_sticky_posts' => true, 162 | 'orderby' => 'ID', 163 | 'order' => 'DESC', 164 | 'fields' => 'all', 165 | ); 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /src/Utils/MsOfficeParser.php: -------------------------------------------------------------------------------- 1 | convertToText(); 12 | */ 13 | class MsOfficeParser { 14 | public static function getText( $file_path ) { 15 | if(isset($file_path) && !file_exists($file_path)) { 16 | return "File Not exists"; 17 | } 18 | $fileInfo = pathinfo($file_path); 19 | $file_ext = $fileInfo['extension']; 20 | if($file_ext == "doc" || $file_ext == "docx" || $file_ext == "xlsx" || $file_ext == "pptx"){ 21 | if($file_ext == "doc") { 22 | return self::parseDoc($file_path); 23 | } elseif($file_ext == "docx") { 24 | return self::parseDocx($file_path); 25 | } elseif($file_ext == "xlsx") { 26 | return self::parseXlsx($file_path); 27 | } elseif($file_ext == "pptx") { 28 | return self::parsePptx($file_path); 29 | } 30 | } else { 31 | return "Unsupported file type"; 32 | } 33 | } 34 | 35 | /** 36 | * Extract Microsoft Office Word before 2007 file contents 37 | * 38 | * @param string $file_path 39 | * @return string $file_content 40 | */ 41 | private static function parseDoc( $file_path ) { 42 | $fileHandle = fopen($file_path, "r"); 43 | $line = @fread($fileHandle, filesize($file_path)); 44 | $lines = explode(chr(0x0D),$line); 45 | $file_content = ""; 46 | foreach($lines as $thisline) { 47 | $pos = strpos($thisline, chr(0x00)); 48 | if (($pos !== FALSE)||(strlen($thisline)==0)) { 49 | } else { 50 | $file_content .= $thisline." "; 51 | } 52 | } 53 | $file_content = preg_replace("/[^a-zA-Z0-9\s\,\.\-\n\r\t@\/\_\(\)]/","",$file_content); 54 | return $file_content; 55 | } 56 | 57 | /** 58 | * Extract Microsoft Office Word 2007+ file contents 59 | * 60 | * @param string $file_path 61 | * @return string $file_content 62 | */ 63 | private static function parseDocx( $file_path ) { 64 | $file_content = ''; 65 | $zip = zip_open( $file_path ); 66 | if (!$zip || is_numeric( $zip ) ) return false; 67 | while ( $zip_entry = zip_read( $zip ) ) { 68 | if ( zip_entry_open( $zip, $zip_entry ) == FALSE ) continue; 69 | if ( zip_entry_name( $zip_entry ) != "word/document.xml" ) continue; 70 | $file_content .= zip_entry_read( $zip_entry, zip_entry_filesize( $zip_entry ) ); 71 | zip_entry_close( $zip_entry ); 72 | }// end while 73 | zip_close( $zip ); 74 | $file_content = str_replace('', ' ', $file_content); 75 | $file_content = str_replace('', ' ', $file_content); 76 | $file_content = strip_tags($file_content); 77 | return $file_content; 78 | } 79 | 80 | /** 81 | * Extract Microsoft Office Excel file contents 82 | * 83 | * @param string $file_path 84 | * @return string $file_content 85 | */ 86 | private static function parseXlsx( $file_path ) { 87 | $xml_filename = "xl/sharedStrings.xml"; //content file name 88 | $zip_handle = new ZipArchive; 89 | $file_content = ""; 90 | if (true === $zip_handle->open($file_path)) { 91 | if (($xml_index = $zip_handle->locateName($xml_filename)) !== false) { 92 | $file_content = $zip_handle->getFromIndex($xml_index); 93 | } else { 94 | $file_content .=""; 95 | } 96 | $zip_handle->close(); 97 | } else { 98 | $file_content .=""; 99 | } 100 | return $file_content; 101 | } 102 | 103 | /** 104 | * Extract Microsoft Office Powerpoint file contents 105 | * 106 | * @param string $file_path 107 | * @return string $file_content 108 | */ 109 | private static function parsePptx( $file_path ) { 110 | $zip_handle = new ZipArchive; 111 | $file_content = ""; 112 | if ( true === $zip_handle->open( $file_path ) ) { 113 | $slide_number = 1; //loop through slide files 114 | while(($xml_index = $zip_handle->locateName( "ppt/slides/slide".$slide_number.".xml")) !== false ){ 115 | $xml_datas = $zip_handle->getFromIndex( $xml_index ); 116 | $dom_document = new DOMDocument(); 117 | $xml_handle = $dom_document->loadXML( $xml_datas, LIBXML_NOENT | LIBXML_XINCLUDE | LIBXML_NOERROR | LIBXML_NOWARNING ); 118 | $file_content .= strip_tags( $xml_handle->saveXML() ); 119 | $slide_number++; 120 | } 121 | if ( $slide_number == 1 ) { 122 | $file_content .=""; 123 | } 124 | $zip_handle->close(); 125 | } else { 126 | $file_content .=""; 127 | } 128 | return $file_content; 129 | } 130 | } -------------------------------------------------------------------------------- /src/Utils/index.php: -------------------------------------------------------------------------------- 1 | redisearchAdminNotice(); 75 | // First, initiate features 76 | if ( !self::$serverException && !self::$moduleException ) { 77 | Features::init(); 78 | new LiveSearch; 79 | new Synonym; 80 | new WooCommerce; 81 | new Document; 82 | } 83 | 84 | $this->admin = new Admin; 85 | $this->handleAjaxRequests(); 86 | // Do the search 87 | if ( !self::$redisearchException ) { 88 | add_filter( 'posts_request', array( $this, 'redisearchPostsRequest' ), 10, 2 ); 89 | add_filter( 'the_posts', array( $this, 'filterThePosts' ), 10, 2 ); 90 | add_action( 'wp_insert_post', array( $this->admin, 'wp_redisearch_index_post_on_publish' ), 10, 3 ); 91 | add_action( 'save_post', array( $this->admin, 'wp_redisearch_index_post_on_publish' ), 10, 3 ); 92 | 93 | } 94 | 95 | } 96 | 97 | /** 98 | * Check for errors like: 99 | * - Redis server 100 | * - Redisearch module 101 | * - If index exists 102 | * @since 0.1.0 103 | * @param 104 | * @return 105 | */ 106 | public function redisearchAdminNotice() { 107 | try { 108 | $this->client = (new Client())->return(); 109 | } catch (\Exception $e) { 110 | if ( isset( $e ) ) { 111 | self::$serverException = true; 112 | } 113 | } 114 | if ( self::$serverException ) { 115 | self::$redisearchException = true; 116 | add_action( 'admin_notices', array(__CLASS__, 'redisServerConnectionNotice' ) ); 117 | } else { 118 | // Check if RediSearch module is loaded. 119 | try { 120 | $loaded_modules = $this->client->rawCommand('MODULE', ['LIST']); 121 | if ( isset( $loaded_modules ) && !empty( $loaded_modules ) ) { 122 | foreach ($loaded_modules as $module) { 123 | if ( !in_array( 'search', $module ) ) { 124 | self::$moduleException = true; 125 | } 126 | } 127 | } else { 128 | self::$moduleException = true; 129 | } 130 | } catch (\Exception $e) { 131 | if ( isset( $e ) ) { 132 | self::$moduleException = true; 133 | } 134 | } 135 | if ( self::$moduleException ) { 136 | self::$redisearchException = true; 137 | add_action( 'admin_notices', array(__CLASS__, 'redisearchNotLoadedNotice' ) ); 138 | } else { 139 | $index_name = Settings::indexName(); 140 | // Check if index exists. 141 | try { 142 | self::$indexInfo = $this->client->rawCommand('FT.INFO', [$index_name]); 143 | } catch (\Exception $e) { 144 | if ( isset( $e ) ) { 145 | $indexNotFound = true; 146 | } 147 | } 148 | if ( self::$indexInfo === 'Unknown Index name' || isset( $indexNotFound ) ) { 149 | self::$indexException = true; 150 | self::$redisearchException = true; 151 | add_action( 'admin_notices', array(__CLASS__, 'redisearchIndexNotExistNotice' ) ); 152 | } 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Show admin notice if error in redis server connection. 159 | * @since 0.1.0 160 | * @param 161 | * @return 162 | */ 163 | public static function redisServerConnectionNotice() { 164 | $redis_settings_page = admin_url('admin.php?page=redisearch'); 165 | ?> 166 |
167 |

settings', 'wp-redisearch' ), $redis_settings_page); ?>

168 |
169 | 181 |
182 |

settings', 'wp-redisearch' ), $redis_settings_page); ?>

183 |
184 | 196 |
197 |

settings page and re-index your website.', 'wp-redisearch' ), $redis_settings_page); ?>

198 |
199 | admin, 'wp_redisearch_add_to_index' ) ); 210 | add_action('wp_ajax_nopriv_wp_redisearch_add_to_index', array( $this->admin, 'wp_redisearch_add_to_index' ) ); 211 | 212 | add_action('wp_ajax_wp_redisearch_write_to_disk', array( $this->admin, 'wp_redisearch_write_to_disk' ) ); 213 | add_action('wp_ajax_nopriv_wp_redisearch_write_to_disk', array( $this->admin, 'wp_redisearch_write_to_disk' ) ); 214 | 215 | add_action('wp_ajax_wp_redisearch_drop_index', array( $this->admin, 'wp_redisearch_drop_index' ) ); 216 | add_action('wp_ajax_nopriv_wp_redisearch_drop_index', array( $this->admin, 'wp_redisearch_drop_index' ) ); 217 | 218 | add_action('wp_ajax_wp_redisearch_save_feature', array( Features::init(), 'wp_redisearch_save_feature' ) ); 219 | add_action('wp_ajax_nopriv_wp_redisearch_save_feature', array( Features::init(), 'wp_redisearch_save_feature' ) ); 220 | } 221 | 222 | /** 223 | * Filter the posts to return redisearch posts. 224 | * @since 0.1.0 225 | * @param array $posts 226 | * @param object $query 227 | * @return array $new_posts 228 | */ 229 | public function filterThePosts( $posts, $query ) { 230 | if ( !apply_filters( 'wp_redisearch_force_redisearch', FALSE, $query ) && 231 | ( !Settings::SearchInAdmin() || 232 | !$query->is_main_query() || 233 | ( method_exists( $query, 'is_search' ) && ! $query->is_search() ) || 234 | empty( $query->query_vars['s'] ) 235 | ) 236 | ) { 237 | return $posts; 238 | } 239 | return $this->searchQueryPosts; 240 | } 241 | 242 | /** 243 | * Filter search query and return posts found in redisearch. 244 | * Reset query to return nothing. 245 | * 246 | * @param string $request 247 | * @param object $query 248 | * 249 | * @return string 250 | * @since 0.1.0 251 | */ 252 | public function redisearchPostsRequest( string $request, $query ) { 253 | global $wpdb; 254 | if ( self::$redisearchException ) { 255 | $query->redisearch_success = false; 256 | return $request; 257 | } 258 | 259 | if ( !apply_filters( 'wp_redisearch_force_redisearch', FALSE, $query ) && 260 | ( !Settings::SearchInAdmin() || 261 | !$query->is_main_query() || 262 | ( method_exists( $query, 'is_search' ) && !$query->is_search() ) || 263 | empty( $query->query_vars['s'] ) 264 | ) 265 | ) { 266 | return $request; 267 | } 268 | 269 | $search = new Query( $this->client, Settings::indexName() ); 270 | 271 | // Offset search results based on pagination 272 | $from = 0; 273 | $offset = $query->query_vars['posts_per_page']; 274 | if ( isset( $query->query_vars['paged'] ) && $query->query_vars['paged'] > 1 ) { 275 | $from = $query->query_vars['posts_per_page'] * ( $query->query_vars['paged'] - 1 ); 276 | } 277 | $results = $search 278 | ->limit( $from, $offset ) 279 | ->return( array( 'post_id' ) ) 280 | ->search( $query->query_vars['s'] ); 281 | $searchResults = $results->getDocuments(); 282 | 283 | $searchCount = $results->getCount(); 284 | 285 | if ( $searchCount == 0 ) { 286 | $query->redisearch_success = true; 287 | return $request; 288 | } 289 | 290 | $searchResults = array_map( function( $res ) { 291 | return $res->post_id; 292 | }, $searchResults ); 293 | $args = array( 294 | 'post_type' => 'any', 295 | 'post_status' => 'any', 296 | 'orderby' => 'post__in', 297 | 'post__in' => $searchResults 298 | ); 299 | 300 | /** 301 | * filter wp_redisearch_before_search_wp_query 302 | * Fires before wp_query. This is useful if you want for some reasons, manipulate WP_Query 303 | * 304 | * @since 0.2.2 305 | * @param array $args Array of arguments passed to WP_Query 306 | * @return array $args Array of manipulated arguments 307 | */ 308 | $args = apply_filters( 'wp_redisearch_before_search_wp_query', $args ); 309 | 310 | $searched_posts = new \WP_Query( $args ); 311 | /** 312 | * filter wp_redisearch_after_search_wp_query 313 | * Fires after wp_query. This is useful if you want to manipulate results of WP_Query 314 | * 315 | * @since 0.2.2 316 | * @param object $query WP_Query 317 | * @param array $args Array of arguments passed to WP_Query 318 | * @param object $searched_posts Result object of WP_Query 319 | */ 320 | $query = apply_filters( 'wp_redisearch_after_search_wp_query', $query, $searched_posts, $args ); 321 | 322 | $this->searchQueryPosts = $searched_posts->posts; 323 | $query->found_posts = $searchCount; 324 | $query->redisearch_success = true; 325 | $query->max_num_pages = ceil( $searchCount / $query->get( 'posts_per_page' ) ); 326 | 327 | 328 | return "SELECT * FROM $wpdb->posts WHERE 1=0"; 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /src/index.php: -------------------------------------------------------------------------------- 1 | decode($fileName); 24 | ``` 25 | 26 | # Lincense 27 | 28 | GNU General Public License version 2 or later. 29 | 30 | -------------------------------------------------------------------------------- /vendor/asika/pdf2text/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asika/pdf2text", 3 | "description": "Simple PHP PDF to text class.", 4 | "license": "GPL v2", 5 | "authors": [ 6 | { 7 | "name": "Simon Asika", 8 | "email": "asika32764@gmail.com" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "require": { 13 | 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Asika\\" : "src" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vendor/asika/pdf2text/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | test 9 | 10 | 11 | -------------------------------------------------------------------------------- /vendor/asika/pdf2text/src/Pdf2text.php: -------------------------------------------------------------------------------- 1 | decodedtext = ''; 63 | $this->filename = $filename; 64 | } 65 | 66 | /** 67 | * Get output text. 68 | * 69 | * @deprecated Use "decode" method instead 70 | * 71 | * @param boolean $echo True to echo it. 72 | * 73 | * @return string 74 | */ 75 | public function output($echo = false) 76 | { 77 | if ($echo) 78 | { 79 | echo $this->decodedtext; 80 | } 81 | else 82 | { 83 | return $this->decodedtext; 84 | } 85 | } 86 | 87 | /** 88 | * Using unicode. 89 | * 90 | * @deprecated Use "decode" method instead 91 | * 92 | * @param boolean $input True or not to use unicode. 93 | * 94 | * @return void 95 | */ 96 | public function setUnicode($input) 97 | { 98 | // 4 for unicode. But 2 should work in most cases just fine 99 | if ($input) 100 | { 101 | $this->multibyte = 4; 102 | } 103 | else 104 | { 105 | $this->multibyte = 2; 106 | } 107 | } 108 | 109 | /** 110 | * Method to set property showprogress 111 | * 112 | * @deprecated Use "decode" method instead 113 | * 114 | * @param boolean $showprogress 115 | * 116 | * @return static Return self to support chaining. 117 | */ 118 | public function showProgress($showprogress) 119 | { 120 | $this->showprogress = $showprogress; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Method to set property convertquotes 127 | * 128 | * @deprecated Use "decode" method instead 129 | * 130 | * @param int $convertquotes 131 | * 132 | * @return static Return self to support chaining. 133 | */ 134 | public function convertQuotes($convertquotes) 135 | { 136 | $this->convertquotes = $convertquotes; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Save decode options 143 | * 144 | * @param string $fileName 145 | * @param int $convertQuotes ENT_COMPAT (double-quotes), ENT_QUOTES (Both), ENT_NOQUOTES (None) 146 | * @param bool $showProgress TRUE if you have problems with time-out 147 | * @param bool $multiByteUnicode 148 | */ 149 | public function saveOptions($fileName, $convertQuotes, $showProgress, $multiByteUnicode) 150 | { 151 | $this->convertquotes = $convertQuotes; 152 | $this->showprogress = $showProgress; 153 | $this->multibyte = $multiByteUnicode ? 4 : 2; 154 | $this->filename = $fileName; 155 | } 156 | 157 | /** 158 | * Decode PDF file 159 | * 160 | * @param string $fileName 161 | * @param int $convertQuotes ENT_COMPAT (double-quotes), ENT_QUOTES (Both), ENT_NOQUOTES (None) 162 | * @param bool $showProgress TRUE if you have problems with time-out 163 | * @param bool $multiByteUnicode 164 | * 165 | * @return string 166 | */ 167 | public function decode($fileName, $convertQuotes = ENT_QUOTES, $showProgress = false, $multiByteUnicode = true) 168 | { 169 | $this->saveOptions($fileName, $convertQuotes, $showProgress, $multiByteUnicode); 170 | 171 | if (empty($fileName)) 172 | { 173 | return ''; 174 | } 175 | 176 | // Read the data from pdf file 177 | $pdfContent = @file_get_contents($this->filename, FILE_BINARY); 178 | $this->decodePDF($pdfContent); 179 | 180 | return $this->output(); 181 | } 182 | 183 | /** 184 | * Decode PDF content 185 | * 186 | * @param string $pdfContent Binary PDF content 187 | * @param int $convertQuotes ENT_COMPAT (double-quotes), ENT_QUOTES (Both), ENT_NOQUOTES (None) 188 | * @param bool $showProgress TRUE if you have problems with time-out 189 | * @param bool $multiByteUnicode 190 | * 191 | * @return string 192 | */ 193 | public function decodeContent($pdfContent, $convertQuotes = ENT_QUOTES, $showProgress = false, $multiByteUnicode = true) 194 | { 195 | $this->saveOptions('', $convertQuotes, $showProgress, $multiByteUnicode); 196 | $this->decodePDF($pdfContent); 197 | 198 | return $this->output(); 199 | } 200 | 201 | /** 202 | * Decode PDF 203 | * 204 | * @deprecated Use "decode" method instead 205 | * 206 | * @param string $pdfContent 207 | * 208 | * @return string 209 | */ 210 | public function decodePDF($pdfContent = '') 211 | { 212 | // Get all text data. 213 | $transformations = array(); 214 | $texts = array(); 215 | 216 | // Get the list of all objects. 217 | preg_match_all("#obj[\n|\r](.*)endobj[\n|\r]#ismU", $pdfContent . "endobj\r", $objects); 218 | $objects = @$objects[1]; 219 | 220 | // Select objects with streams. 221 | for ($i = 0; $i < count($objects); $i++) 222 | { 223 | $currentObject = $objects[$i]; 224 | 225 | // Prevent time-out 226 | @set_time_limit(0); 227 | 228 | if ($this->showprogress) 229 | { 230 | flush(); 231 | ob_flush(); 232 | } 233 | 234 | // Check if an object includes data stream. 235 | if (preg_match("#stream[\n|\r](.*)endstream[\n|\r]#ismU", $currentObject . "endstream\r", $stream)) 236 | { 237 | $stream = ltrim($stream[1]); 238 | 239 | // Check object parameters and look for text data. 240 | $options = $this->getObjectOptions($currentObject); 241 | 242 | if (!(empty($options["Length1"]) && empty($options["Type"]) && empty($options["Subtype"]))) 243 | { 244 | continue; 245 | } 246 | 247 | // Hack, length doesnt always seem to be correct 248 | unset($options["Length"]); 249 | 250 | // So, we have text data. Decode it. 251 | $data = $this->getDecodedStream($stream, $options); 252 | 253 | if (strlen($data)) 254 | { 255 | if (preg_match_all("#BT[\n|\r| ](.*)ET[\n|\r| ]#ismU", $data . "ET\r", $textContainers)) 256 | { 257 | $textContainers = @$textContainers[1]; 258 | $this->getDirtyTexts($texts, $textContainers); 259 | } 260 | else 261 | { 262 | $this->getCharTransformations($transformations, $data); 263 | } 264 | } 265 | } 266 | } 267 | // Analyze text blocks taking into account character transformations and return results. 268 | $this->decodedtext = $this->getTextUsingTransformations($texts, $transformations); 269 | 270 | // Analyze text blocks taking into account character transformations and return results. 271 | return $this->getTextUsingTransformations($texts, $transformations); 272 | } 273 | 274 | /** 275 | * Decode ASCII Hex. 276 | * 277 | * @param string $input ASCII string. 278 | * 279 | * @return string 280 | */ 281 | private function decodeAsciiHex($input) 282 | { 283 | $output = ""; 284 | $isOdd = true; 285 | $isComment = false; 286 | 287 | for ($i = 0, $codeHigh = -1; $i < strlen($input) && $input[$i] !== '>'; $i++) 288 | { 289 | $c = $input[$i]; 290 | 291 | if ($isComment) 292 | { 293 | if ($c === '\r' || $c === '\n') 294 | { 295 | $isComment = false; 296 | } 297 | continue; 298 | } 299 | 300 | switch ($c) 301 | { 302 | case '\0': 303 | case '\t': 304 | case '\r': 305 | case '\f': 306 | case '\n': 307 | case ' ': 308 | break; 309 | case '%': 310 | $isComment = true; 311 | break; 312 | 313 | default: 314 | $code = hexdec($c); 315 | if ($code === 0 && $c != '0') 316 | { 317 | return ""; 318 | } 319 | 320 | if ($isOdd) 321 | { 322 | $codeHigh = $code; 323 | } 324 | else 325 | { 326 | $output .= chr($codeHigh * 16 + $code); 327 | } 328 | 329 | $isOdd = !$isOdd; 330 | break; 331 | } 332 | } 333 | 334 | if ($input[$i] !== '>') 335 | { 336 | return ""; 337 | } 338 | 339 | if ($isOdd) 340 | { 341 | $output .= chr($codeHigh * 16); 342 | } 343 | 344 | return $output; 345 | } 346 | 347 | /** 348 | * Descode ASCII 85. 349 | * 350 | * @param string $input ASCII string. 351 | * 352 | * @return string 353 | */ 354 | private function decodeAscii85($input) 355 | { 356 | $output = ""; 357 | 358 | $isComment = false; 359 | $ords = array(); 360 | 361 | for ($i = 0, $state = 0; $i < strlen($input) && $input[$i] !== '~'; $i++) 362 | { 363 | $c = $input[$i]; 364 | 365 | if ($isComment) 366 | { 367 | if ($c === '\r' || $c === '\n') 368 | { 369 | $isComment = false; 370 | } 371 | continue; 372 | } 373 | 374 | if ($c === '\0' || $c === '\t' || $c === '\r' || $c === '\f' || $c === '\n' || $c === ' ') 375 | { 376 | continue; 377 | } 378 | if ($c === '%') 379 | { 380 | $isComment = true; 381 | continue; 382 | } 383 | if ($c === 'z' && $state === 0) 384 | { 385 | $output .= str_repeat(chr(0), 4); 386 | continue; 387 | } 388 | if ($c < '!' || $c > 'u') 389 | { 390 | return ""; 391 | } 392 | 393 | $code = ord($input[$i]) & 0xff; 394 | $ords[$state++] = $code - ord('!'); 395 | 396 | if ($state == 5) 397 | { 398 | $state = 0; 399 | for ($sum = 0, $j = 0; $j < 5; $j++) 400 | $sum = $sum * 85 + $ords[$j]; 401 | for ($j = 3; $j >= 0; $j--) 402 | $output .= chr($sum >> ($j * 8)); 403 | } 404 | } 405 | if ($state === 1) 406 | { 407 | return ""; 408 | } 409 | elseif ($state > 1) 410 | { 411 | for ($i = 0, $sum = 0; $i < $state; $i++) 412 | $sum += ($ords[$i] + ($i == $state - 1)) * pow(85, 4 - $i); 413 | for ($i = 0; $i < $state - 1; $i++) 414 | { 415 | try 416 | { 417 | if (false == ($o = chr($sum >> ((3 - $i) * 8)))) 418 | { 419 | throw new \Exception('Error'); 420 | } 421 | $output .= $o; 422 | } 423 | catch (\Exception $e) 424 | { /*Dont do anything*/ 425 | } 426 | } 427 | } 428 | 429 | return $output; 430 | } 431 | 432 | /** 433 | * Decode Flate 434 | * 435 | * @param $data 436 | * 437 | * @return string 438 | */ 439 | private function decodeFlate($data) 440 | { 441 | return @gzuncompress($data); 442 | } 443 | 444 | /** 445 | * Get Object Options 446 | * 447 | * @param $object 448 | * 449 | * @return array 450 | */ 451 | private function getObjectOptions($object) 452 | { 453 | $options = array(); 454 | 455 | if (preg_match("#<<(.*)>>#ismU", $object, $options)) 456 | { 457 | $options = explode("/", $options[1]); 458 | 459 | @array_shift($options); 460 | 461 | $o = array(); 462 | 463 | for ($j = 0; $j < @count($options); $j++) 464 | { 465 | $options[$j] = preg_replace("#\s+#", " ", trim($options[$j])); 466 | 467 | if (strpos($options[$j], " ") !== false) 468 | { 469 | $parts = explode(" ", $options[$j]); 470 | $o[$parts[0]] = $parts[1]; 471 | } 472 | else 473 | { 474 | $o[$options[$j]] = true; 475 | } 476 | } 477 | 478 | $options = $o; 479 | 480 | unset($o); 481 | } 482 | 483 | return $options; 484 | } 485 | 486 | /** 487 | * Get Decode Stream. 488 | * 489 | * @param $stream 490 | * @param $options 491 | * 492 | * @return string 493 | */ 494 | private function getDecodedStream($stream, $options) 495 | { 496 | $data = ""; 497 | 498 | if (empty($options["Filter"])) 499 | { 500 | $data = $stream; 501 | } 502 | else 503 | { 504 | $length = !empty($options["Length"]) ? $options["Length"] : strlen($stream); 505 | $_stream = substr($stream, 0, $length); 506 | 507 | foreach ($options as $key => $value) 508 | { 509 | if ($key === "ASCIIHexDecode") 510 | { 511 | $_stream = $this->decodeAsciiHex($_stream); 512 | } 513 | elseif ($key === "ASCII85Decode") 514 | { 515 | $_stream = $this->decodeAscii85($_stream); 516 | } 517 | elseif ($key === "FlateDecode") 518 | { 519 | $_stream = $this->decodeFlate($_stream); 520 | } 521 | elseif ($key === "Crypt") 522 | { // TO DO 523 | } 524 | } 525 | $data = $_stream; 526 | } 527 | 528 | return $data; 529 | } 530 | 531 | /** 532 | * Get Dirty Texts 533 | * 534 | * @param array $texts 535 | * @param array $textContainers 536 | * 537 | * @return void 538 | */ 539 | private function getDirtyTexts(&$texts, $textContainers) 540 | { 541 | for ($j = 0; $j < count($textContainers); $j++) 542 | { 543 | if (preg_match_all("#\[(.*)\]\s*TJ[\n|\r| ]#ismU", $textContainers[$j], $parts)) 544 | { 545 | $texts = array_merge($texts, array(@implode('', $parts[1]))); 546 | } 547 | elseif (preg_match_all("#T[d|w|m|f]\s*(\(.*\))\s*Tj[\n|\r| ]#ismU", $textContainers[$j], $parts)) 548 | { 549 | $texts = array_merge($texts, array(@implode('', $parts[1]))); 550 | } 551 | elseif (preg_match_all("#T[d|w|m|f]\s*(\[.*\])\s*Tj[\n|\r| ]#ismU", $textContainers[$j], $parts)) 552 | { 553 | $texts = array_merge($texts, array(@implode('', $parts[1]))); 554 | } 555 | } 556 | } 557 | 558 | /** 559 | * Get Char Transformations 560 | * 561 | * @param $transformations 562 | * @param $stream 563 | * 564 | * @return void 565 | */ 566 | private function getCharTransformations(&$transformations, $stream) 567 | { 568 | preg_match_all("#([0-9]+)\s+beginbfchar(.*)endbfchar#ismU", $stream, $chars, PREG_SET_ORDER); 569 | preg_match_all("#([0-9]+)\s+beginbfrange(.*)endbfrange#ismU", $stream, $ranges, PREG_SET_ORDER); 570 | 571 | for ($j = 0; $j < count($chars); $j++) 572 | { 573 | $count = $chars[$j][1]; 574 | $current = explode("\n", trim($chars[$j][2])); 575 | 576 | for ($k = 0; $k < $count && $k < count($current); $k++) 577 | { 578 | if (preg_match("#<([0-9a-f]{2,4})>\s+<([0-9a-f]{4,512})>#is", trim($current[$k]), $map)) 579 | { 580 | $transformations[str_pad($map[1], 4, "0")] = $map[2]; 581 | } 582 | } 583 | } 584 | for ($j = 0; $j < count($ranges); $j++) 585 | { 586 | $count = $ranges[$j][1]; 587 | $current = explode("\n", trim($ranges[$j][2])); 588 | 589 | for ($k = 0; $k < $count && $k < count($current); $k++) 590 | { 591 | if (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+<([0-9a-f]{4})>#is", trim($current[$k]), $map)) 592 | { 593 | $from = hexdec($map[1]); 594 | $to = hexdec($map[2]); 595 | $_from = hexdec($map[3]); 596 | 597 | for ($m = $from, $n = 0; $m <= $to; $m++, $n++) 598 | { 599 | $transformations[sprintf("%04X", $m)] = sprintf("%04X", $_from + $n); 600 | } 601 | } 602 | elseif (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+\[(.*)\]#ismU", trim($current[$k]), $map)) 603 | { 604 | $from = hexdec($map[1]); 605 | $to = hexdec($map[2]); 606 | $parts = preg_split("#\s+#", trim($map[3])); 607 | 608 | for ($m = $from, $n = 0; $m <= $to && $n < count($parts); $m++, $n++) 609 | { 610 | $transformations[sprintf("%04X", $m)] = sprintf("%04X", hexdec($parts[$n])); 611 | } 612 | } 613 | } 614 | } 615 | } 616 | 617 | /** 618 | * Get Text Using Transformations 619 | * 620 | * @param $texts 621 | * @param $transformations 622 | * 623 | * @return string 624 | */ 625 | private function getTextUsingTransformations($texts, $transformations) 626 | { 627 | $document = ""; 628 | 629 | for ($i = 0; $i < count($texts); $i++) 630 | { 631 | $isHex = false; 632 | $isPlain = false; 633 | 634 | $hex = ""; 635 | $plain = ""; 636 | 637 | for ($j = 0; $j < strlen($texts[$i]); $j++) 638 | { 639 | $c = $texts[$i][$j]; 640 | switch ($c) 641 | { 642 | case "<": 643 | $hex = ""; 644 | $isHex = true; 645 | $isPlain = false; 646 | break; 647 | case ">": 648 | $hexs = str_split($hex, $this->multibyte); // 2 or 4 (UTF8 or ISO) 649 | for ($k = 0; $k < count($hexs); $k++) 650 | { 651 | 652 | $chex = str_pad($hexs[$k], 4, "0"); // Add tailing zero 653 | if (isset($transformations[$chex])) 654 | { 655 | $chex = $transformations[$chex]; 656 | } 657 | $document .= html_entity_decode("&#x" . $chex . ";"); 658 | } 659 | $isHex = false; 660 | break; 661 | case "(": 662 | $plain = ""; 663 | $isPlain = true; 664 | $isHex = false; 665 | break; 666 | case ")": 667 | $document .= $plain; 668 | $isPlain = false; 669 | break; 670 | case "\\": 671 | $c2 = $texts[$i][$j + 1]; 672 | if (in_array($c2, array("\\", "(", ")"))) 673 | { 674 | $plain .= $c2; 675 | } 676 | elseif ($c2 === "n") 677 | { 678 | $plain .= '\n'; 679 | } 680 | elseif ($c2 === "r") 681 | { 682 | $plain .= '\r'; 683 | } 684 | elseif ($c2 === "t") 685 | { 686 | $plain .= '\t'; 687 | } 688 | elseif ($c2 === "b") 689 | { 690 | $plain .= '\b'; 691 | } 692 | elseif ($c2 === "f") 693 | { 694 | $plain .= '\f'; 695 | } 696 | elseif ($c2 >= '0' && $c2 <= '9') 697 | { 698 | $oct = preg_replace("#[^0-9]#", "", substr($texts[$i], $j + 1, 3)); 699 | $j += strlen($oct) - 1; 700 | $plain .= html_entity_decode("&#" . octdec($oct) . ";", $this->convertquotes); 701 | } 702 | $j++; 703 | break; 704 | 705 | default: 706 | if ($isHex) 707 | { 708 | $hex .= $c; 709 | } 710 | elseif ($isPlain) 711 | { 712 | $plain .= $c; 713 | } 714 | break; 715 | } 716 | } 717 | 718 | $document .= "\n"; 719 | } 720 | 721 | return $document; 722 | } 723 | } 724 | -------------------------------------------------------------------------------- /vendor/asika/pdf2text/test/Pdf2textTest.php: -------------------------------------------------------------------------------- 1 | instance = new Pdf2text; 30 | } 31 | 32 | /** 33 | * testConvert2Test 34 | * 35 | * @return void 36 | */ 37 | public function testDecodePDF() 38 | { 39 | $output = $this->instance->decode(__DIR__ . '/test.pdf'); 40 | 41 | $text = <<assertEquals($output, $text); 51 | } 52 | 53 | /** 54 | * testDecodeContent 55 | * 56 | * @return void 57 | */ 58 | public function testDecodeContent() 59 | { 60 | $output = $this->instance->decodeContent(file_get_contents(__DIR__ . '/test.pdf')); 61 | 62 | $text = <<assertEquals($output, $text); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /vendor/asika/pdf2text/test/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/front/wp-redisearch/1fcea6cf0e1d6e334bc30fd7d79b992b1263f0fd/vendor/asika/pdf2text/test/test.pdf -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see http://www.php-fig.org/psr/psr-0/ 41 | * @see http://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | // PSR-4 46 | private $prefixLengthsPsr4 = array(); 47 | private $prefixDirsPsr4 = array(); 48 | private $fallbackDirsPsr4 = array(); 49 | 50 | // PSR-0 51 | private $prefixesPsr0 = array(); 52 | private $fallbackDirsPsr0 = array(); 53 | 54 | private $useIncludePath = false; 55 | private $classMap = array(); 56 | private $classMapAuthoritative = false; 57 | private $missingClasses = array(); 58 | private $apcuPrefix; 59 | 60 | public function getPrefixes() 61 | { 62 | if (!empty($this->prefixesPsr0)) { 63 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 64 | } 65 | 66 | return array(); 67 | } 68 | 69 | public function getPrefixesPsr4() 70 | { 71 | return $this->prefixDirsPsr4; 72 | } 73 | 74 | public function getFallbackDirs() 75 | { 76 | return $this->fallbackDirsPsr0; 77 | } 78 | 79 | public function getFallbackDirsPsr4() 80 | { 81 | return $this->fallbackDirsPsr4; 82 | } 83 | 84 | public function getClassMap() 85 | { 86 | return $this->classMap; 87 | } 88 | 89 | /** 90 | * @param array $classMap Class to filename map 91 | */ 92 | public function addClassMap(array $classMap) 93 | { 94 | if ($this->classMap) { 95 | $this->classMap = array_merge($this->classMap, $classMap); 96 | } else { 97 | $this->classMap = $classMap; 98 | } 99 | } 100 | 101 | /** 102 | * Registers a set of PSR-0 directories for a given prefix, either 103 | * appending or prepending to the ones previously set for this prefix. 104 | * 105 | * @param string $prefix The prefix 106 | * @param array|string $paths The PSR-0 root directories 107 | * @param bool $prepend Whether to prepend the directories 108 | */ 109 | public function add($prefix, $paths, $prepend = false) 110 | { 111 | if (!$prefix) { 112 | if ($prepend) { 113 | $this->fallbackDirsPsr0 = array_merge( 114 | (array) $paths, 115 | $this->fallbackDirsPsr0 116 | ); 117 | } else { 118 | $this->fallbackDirsPsr0 = array_merge( 119 | $this->fallbackDirsPsr0, 120 | (array) $paths 121 | ); 122 | } 123 | 124 | return; 125 | } 126 | 127 | $first = $prefix[0]; 128 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 129 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 130 | 131 | return; 132 | } 133 | if ($prepend) { 134 | $this->prefixesPsr0[$first][$prefix] = array_merge( 135 | (array) $paths, 136 | $this->prefixesPsr0[$first][$prefix] 137 | ); 138 | } else { 139 | $this->prefixesPsr0[$first][$prefix] = array_merge( 140 | $this->prefixesPsr0[$first][$prefix], 141 | (array) $paths 142 | ); 143 | } 144 | } 145 | 146 | /** 147 | * Registers a set of PSR-4 directories for a given namespace, either 148 | * appending or prepending to the ones previously set for this namespace. 149 | * 150 | * @param string $prefix The prefix/namespace, with trailing '\\' 151 | * @param array|string $paths The PSR-4 base directories 152 | * @param bool $prepend Whether to prepend the directories 153 | * 154 | * @throws \InvalidArgumentException 155 | */ 156 | public function addPsr4($prefix, $paths, $prepend = false) 157 | { 158 | if (!$prefix) { 159 | // Register directories for the root namespace. 160 | if ($prepend) { 161 | $this->fallbackDirsPsr4 = array_merge( 162 | (array) $paths, 163 | $this->fallbackDirsPsr4 164 | ); 165 | } else { 166 | $this->fallbackDirsPsr4 = array_merge( 167 | $this->fallbackDirsPsr4, 168 | (array) $paths 169 | ); 170 | } 171 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 172 | // Register directories for a new namespace. 173 | $length = strlen($prefix); 174 | if ('\\' !== $prefix[$length - 1]) { 175 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 176 | } 177 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 178 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 179 | } elseif ($prepend) { 180 | // Prepend directories for an already registered namespace. 181 | $this->prefixDirsPsr4[$prefix] = array_merge( 182 | (array) $paths, 183 | $this->prefixDirsPsr4[$prefix] 184 | ); 185 | } else { 186 | // Append directories for an already registered namespace. 187 | $this->prefixDirsPsr4[$prefix] = array_merge( 188 | $this->prefixDirsPsr4[$prefix], 189 | (array) $paths 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Registers a set of PSR-0 directories for a given prefix, 196 | * replacing any others previously set for this prefix. 197 | * 198 | * @param string $prefix The prefix 199 | * @param array|string $paths The PSR-0 base directories 200 | */ 201 | public function set($prefix, $paths) 202 | { 203 | if (!$prefix) { 204 | $this->fallbackDirsPsr0 = (array) $paths; 205 | } else { 206 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 207 | } 208 | } 209 | 210 | /** 211 | * Registers a set of PSR-4 directories for a given namespace, 212 | * replacing any others previously set for this namespace. 213 | * 214 | * @param string $prefix The prefix/namespace, with trailing '\\' 215 | * @param array|string $paths The PSR-4 base directories 216 | * 217 | * @throws \InvalidArgumentException 218 | */ 219 | public function setPsr4($prefix, $paths) 220 | { 221 | if (!$prefix) { 222 | $this->fallbackDirsPsr4 = (array) $paths; 223 | } else { 224 | $length = strlen($prefix); 225 | if ('\\' !== $prefix[$length - 1]) { 226 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 227 | } 228 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 229 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 230 | } 231 | } 232 | 233 | /** 234 | * Turns on searching the include path for class files. 235 | * 236 | * @param bool $useIncludePath 237 | */ 238 | public function setUseIncludePath($useIncludePath) 239 | { 240 | $this->useIncludePath = $useIncludePath; 241 | } 242 | 243 | /** 244 | * Can be used to check if the autoloader uses the include path to check 245 | * for classes. 246 | * 247 | * @return bool 248 | */ 249 | public function getUseIncludePath() 250 | { 251 | return $this->useIncludePath; 252 | } 253 | 254 | /** 255 | * Turns off searching the prefix and fallback directories for classes 256 | * that have not been registered with the class map. 257 | * 258 | * @param bool $classMapAuthoritative 259 | */ 260 | public function setClassMapAuthoritative($classMapAuthoritative) 261 | { 262 | $this->classMapAuthoritative = $classMapAuthoritative; 263 | } 264 | 265 | /** 266 | * Should class lookup fail if not found in the current class map? 267 | * 268 | * @return bool 269 | */ 270 | public function isClassMapAuthoritative() 271 | { 272 | return $this->classMapAuthoritative; 273 | } 274 | 275 | /** 276 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 277 | * 278 | * @param string|null $apcuPrefix 279 | */ 280 | public function setApcuPrefix($apcuPrefix) 281 | { 282 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 283 | } 284 | 285 | /** 286 | * The APCu prefix in use, or null if APCu caching is not enabled. 287 | * 288 | * @return string|null 289 | */ 290 | public function getApcuPrefix() 291 | { 292 | return $this->apcuPrefix; 293 | } 294 | 295 | /** 296 | * Registers this instance as an autoloader. 297 | * 298 | * @param bool $prepend Whether to prepend the autoloader or not 299 | */ 300 | public function register($prepend = false) 301 | { 302 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 303 | } 304 | 305 | /** 306 | * Unregisters this instance as an autoloader. 307 | */ 308 | public function unregister() 309 | { 310 | spl_autoload_unregister(array($this, 'loadClass')); 311 | } 312 | 313 | /** 314 | * Loads the given class or interface. 315 | * 316 | * @param string $class The name of the class 317 | * @return bool|null True if loaded, null otherwise 318 | */ 319 | public function loadClass($class) 320 | { 321 | if ($file = $this->findFile($class)) { 322 | includeFile($file); 323 | 324 | return true; 325 | } 326 | } 327 | 328 | /** 329 | * Finds the path to the file where the class is defined. 330 | * 331 | * @param string $class The name of the class 332 | * 333 | * @return string|false The path if found, false otherwise 334 | */ 335 | public function findFile($class) 336 | { 337 | // class map lookup 338 | if (isset($this->classMap[$class])) { 339 | return $this->classMap[$class]; 340 | } 341 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 342 | return false; 343 | } 344 | if (null !== $this->apcuPrefix) { 345 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 346 | if ($hit) { 347 | return $file; 348 | } 349 | } 350 | 351 | $file = $this->findFileWithExtension($class, '.php'); 352 | 353 | // Search for Hack files if we are running on HHVM 354 | if (false === $file && defined('HHVM_VERSION')) { 355 | $file = $this->findFileWithExtension($class, '.hh'); 356 | } 357 | 358 | if (null !== $this->apcuPrefix) { 359 | apcu_add($this->apcuPrefix.$class, $file); 360 | } 361 | 362 | if (false === $file) { 363 | // Remember that this class does not exist. 364 | $this->missingClasses[$class] = true; 365 | } 366 | 367 | return $file; 368 | } 369 | 370 | private function findFileWithExtension($class, $ext) 371 | { 372 | // PSR-4 lookup 373 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 374 | 375 | $first = $class[0]; 376 | if (isset($this->prefixLengthsPsr4[$first])) { 377 | $subPath = $class; 378 | while (false !== $lastPos = strrpos($subPath, '\\')) { 379 | $subPath = substr($subPath, 0, $lastPos); 380 | $search = $subPath . '\\'; 381 | if (isset($this->prefixDirsPsr4[$search])) { 382 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 383 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 384 | if (file_exists($file = $dir . $pathEnd)) { 385 | return $file; 386 | } 387 | } 388 | } 389 | } 390 | } 391 | 392 | // PSR-4 fallback dirs 393 | foreach ($this->fallbackDirsPsr4 as $dir) { 394 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 395 | return $file; 396 | } 397 | } 398 | 399 | // PSR-0 lookup 400 | if (false !== $pos = strrpos($class, '\\')) { 401 | // namespaced class name 402 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 403 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 404 | } else { 405 | // PEAR-like class name 406 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 407 | } 408 | 409 | if (isset($this->prefixesPsr0[$first])) { 410 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 411 | if (0 === strpos($class, $prefix)) { 412 | foreach ($dirs as $dir) { 413 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 414 | return $file; 415 | } 416 | } 417 | } 418 | } 419 | } 420 | 421 | // PSR-0 fallback dirs 422 | foreach ($this->fallbackDirsPsr0 as $dir) { 423 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 424 | return $file; 425 | } 426 | } 427 | 428 | // PSR-0 include paths. 429 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 430 | return $file; 431 | } 432 | 433 | return false; 434 | } 435 | } 436 | 437 | /** 438 | * Scope isolated include. 439 | * 440 | * Prevents access to $this/self from included files. 441 | */ 442 | function includeFile($file) 443 | { 444 | include $file; 445 | } 446 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/src'), 10 | 'Vaites\\ApacheTika\\' => array($vendorDir . '/vaites/php-apache-tika/src'), 11 | 'SevenFields\\' => array($vendorDir . '/foadyousefi/seven-fields/src'), 12 | 'Predis\\' => array($vendorDir . '/predis/predis/src'), 13 | 'FKRediSearch\\' => array($vendorDir . '/front/redisearch/src'), 14 | 'Asika\\' => array($vendorDir . '/asika/pdf2text/src'), 15 | ); 16 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); 30 | if ($useStaticLoader) { 31 | require_once __DIR__ . '/autoload_static.php'; 32 | 33 | call_user_func(\Composer\Autoload\ComposerStaticInitc54fefdfecefa7a76a4efbc673072543::getInitializer($loader)); 34 | } else { 35 | $map = require __DIR__ . '/autoload_namespaces.php'; 36 | foreach ($map as $namespace => $path) { 37 | $loader->set($namespace, $path); 38 | } 39 | 40 | $map = require __DIR__ . '/autoload_psr4.php'; 41 | foreach ($map as $namespace => $path) { 42 | $loader->setPsr4($namespace, $path); 43 | } 44 | 45 | $classMap = require __DIR__ . '/autoload_classmap.php'; 46 | if ($classMap) { 47 | $loader->addClassMap($classMap); 48 | } 49 | } 50 | 51 | $loader->register(true); 52 | 53 | return $loader; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "asika/pdf2text", 4 | "version": "dev-master", 5 | "version_normalized": "9999999-dev", 6 | "source": { 7 | "type": "git", 8 | "url": "https://github.com/asika32764/php-pdf-2-text.git", 9 | "reference": "a14ea95695a277e385dbc03caeddb91c5e10319f" 10 | }, 11 | "dist": { 12 | "type": "zip", 13 | "url": "https://api.github.com/repos/asika32764/php-pdf-2-text/zipball/a14ea95695a277e385dbc03caeddb91c5e10319f", 14 | "reference": "a14ea95695a277e385dbc03caeddb91c5e10319f", 15 | "shasum": "" 16 | }, 17 | "time": "2016-11-02T03:47:29+00:00", 18 | "type": "library", 19 | "installation-source": "source", 20 | "autoload": { 21 | "psr-4": { 22 | "Asika\\": "src" 23 | } 24 | }, 25 | "notification-url": "https://packagist.org/downloads/", 26 | "license": [ 27 | "GPL v2" 28 | ], 29 | "authors": [ 30 | { 31 | "name": "Simon Asika", 32 | "email": "asika32764@gmail.com" 33 | } 34 | ], 35 | "description": "Simple PHP PDF to text class." 36 | }, 37 | { 38 | "name": "foadyousefi/seven-fields", 39 | "version": "dev-master", 40 | "version_normalized": "9999999-dev", 41 | "source": { 42 | "type": "git", 43 | "url": "https://github.com/foadyousefi/seven-fields.git", 44 | "reference": "89e1431b0960657135ee44095d862afe48baeb62" 45 | }, 46 | "dist": { 47 | "type": "zip", 48 | "url": "https://api.github.com/repos/foadyousefi/seven-fields/zipball/89e1431b0960657135ee44095d862afe48baeb62", 49 | "reference": "89e1431b0960657135ee44095d862afe48baeb62", 50 | "shasum": "" 51 | }, 52 | "require": { 53 | "php": ">=5.3" 54 | }, 55 | "time": "2019-01-09T12:17:22+00:00", 56 | "type": "library", 57 | "installation-source": "source", 58 | "autoload": { 59 | "psr-4": { 60 | "SevenFields\\": "src/" 61 | } 62 | }, 63 | "notification-url": "https://packagist.org/downloads/", 64 | "license": [ 65 | "GPL-2.0" 66 | ], 67 | "authors": [ 68 | { 69 | "name": "Foad Yousefi", 70 | "email": "foadyousefi@gmail.com", 71 | "homepage": "https://7km.co/" 72 | } 73 | ], 74 | "description": "WordPress developer-friendly option pages with custom fields.", 75 | "homepage": "https://7km.co/" 76 | }, 77 | { 78 | "name": "front/redisearch", 79 | "version": "dev-master", 80 | "version_normalized": "9999999-dev", 81 | "source": { 82 | "type": "git", 83 | "url": "https://github.com/front/redisearch.git", 84 | "reference": "e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9" 85 | }, 86 | "dist": { 87 | "type": "zip", 88 | "url": "https://api.github.com/repos/front/redisearch/zipball/e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9", 89 | "reference": "e74aa4dd41f3bdbee1a501b2cedf679ae0331ed9", 90 | "shasum": "" 91 | }, 92 | "require": { 93 | "predis/predis": "^1.1" 94 | }, 95 | "time": "2020-10-13T12:33:36+00:00", 96 | "type": "library", 97 | "installation-source": "source", 98 | "autoload": { 99 | "psr-4": { 100 | "FKRediSearch\\": "src/" 101 | } 102 | }, 103 | "notification-url": "https://packagist.org/downloads/", 104 | "license": [ 105 | "MIT" 106 | ], 107 | "authors": [ 108 | { 109 | "name": "Foad Yousefi", 110 | "email": "foadyousefi@gmail.com", 111 | "homepage": "https://frontkom.no" 112 | }, 113 | { 114 | "name": "Fábio Neves", 115 | "email": "fabio@frontkom.com", 116 | "homepage": "https://frontkom.com" 117 | }, 118 | { 119 | "name": "Bartosz Jarzyna", 120 | "email": "bartosz@netkata.com", 121 | "homepage": "https://netkata.com" 122 | } 123 | ], 124 | "description": "CMS/Framework agnostic RediSearch client", 125 | "homepage": "http://github.com/front/redisearch", 126 | "keywords": [ 127 | "redis", 128 | "redisearch" 129 | ] 130 | }, 131 | { 132 | "name": "predis/predis", 133 | "version": "v1.1.x-dev", 134 | "version_normalized": "1.1.9999999.9999999-dev", 135 | "source": { 136 | "type": "git", 137 | "url": "https://github.com/predis/predis.git", 138 | "reference": "5f4b87080f4df0042a57d537137c1d2c17a2b79a" 139 | }, 140 | "dist": { 141 | "type": "zip", 142 | "url": "https://api.github.com/repos/predis/predis/zipball/5f4b87080f4df0042a57d537137c1d2c17a2b79a", 143 | "reference": "5f4b87080f4df0042a57d537137c1d2c17a2b79a", 144 | "shasum": "" 145 | }, 146 | "require": { 147 | "php": ">=5.3.9" 148 | }, 149 | "require-dev": { 150 | "cweagans/composer-patches": "^1.6", 151 | "phpunit/phpunit": "~4.8" 152 | }, 153 | "suggest": { 154 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 155 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 156 | }, 157 | "time": "2020-09-20T15:16:49+00:00", 158 | "type": "library", 159 | "extra": { 160 | "composer-exit-on-patch-failure": true, 161 | "patches": { 162 | "phpunit/phpunit-mock-objects": { 163 | "Fix PHP 7 and 8 compatibility": "./tests/phpunit_mock_objects.patch" 164 | }, 165 | "phpunit/phpunit": { 166 | "Fix PHP 7 compatibility": "./tests/phpunit_php7.patch", 167 | "Fix PHP 8 compatibility": "./tests/phpunit_php8.patch" 168 | } 169 | } 170 | }, 171 | "installation-source": "source", 172 | "autoload": { 173 | "psr-4": { 174 | "Predis\\": "src/" 175 | } 176 | }, 177 | "notification-url": "https://packagist.org/downloads/", 178 | "license": [ 179 | "MIT" 180 | ], 181 | "authors": [ 182 | { 183 | "name": "Daniele Alessandri", 184 | "email": "suppakilla@gmail.com", 185 | "homepage": "http://clorophilla.net", 186 | "role": "Creator & Maintainer" 187 | }, 188 | { 189 | "name": "Till Krüss", 190 | "homepage": "https://till.im", 191 | "role": "Maintainer" 192 | } 193 | ], 194 | "description": "Flexible and feature-complete Redis client for PHP and HHVM", 195 | "homepage": "http://github.com/predis/predis", 196 | "keywords": [ 197 | "nosql", 198 | "predis", 199 | "redis" 200 | ], 201 | "funding": [ 202 | { 203 | "url": "https://github.com/sponsors/tillkruss", 204 | "type": "github" 205 | } 206 | ] 207 | }, 208 | { 209 | "name": "vaites/php-apache-tika", 210 | "version": "dev-master", 211 | "version_normalized": "9999999-dev", 212 | "source": { 213 | "type": "git", 214 | "url": "https://github.com/vaites/php-apache-tika.git", 215 | "reference": "dcc27626ee7d0f572a828539a607d734094b9e73" 216 | }, 217 | "dist": { 218 | "type": "zip", 219 | "url": "https://api.github.com/repos/vaites/php-apache-tika/zipball/dcc27626ee7d0f572a828539a607d734094b9e73", 220 | "reference": "dcc27626ee7d0f572a828539a607d734094b9e73", 221 | "shasum": "" 222 | }, 223 | "require": { 224 | "ext-curl": "*", 225 | "php": ">=7.2.0" 226 | }, 227 | "require-dev": { 228 | "filp/whoops": "^2.7", 229 | "nunomaduro/phpinsights": "^1.14", 230 | "phpstan/phpstan": "^0.12.26", 231 | "phpunit/phpunit": "^8.0", 232 | "symfony/var-dumper": "^5.1" 233 | }, 234 | "time": "2020-10-19T20:23:24+00:00", 235 | "type": "library", 236 | "extra": { 237 | "supported-versions": [ 238 | "1.15", 239 | "1.16", 240 | "1.17", 241 | "1.18", 242 | "1.19", 243 | "1.19.1", 244 | "1.20", 245 | "1.21", 246 | "1.22", 247 | "1.23", 248 | "1.24", 249 | "1.24.1" 250 | ] 251 | }, 252 | "installation-source": "source", 253 | "autoload": { 254 | "psr-4": { 255 | "Vaites\\ApacheTika\\": "src/" 256 | } 257 | }, 258 | "notification-url": "https://packagist.org/downloads/", 259 | "license": [ 260 | "MIT" 261 | ], 262 | "authors": [ 263 | { 264 | "name": "David Martínez", 265 | "email": "contacto@davidmartinez.net" 266 | } 267 | ], 268 | "description": "Apache Tika bindings for PHP: extracts text from documents and images (with OCR), metadata and more...", 269 | "keywords": [ 270 | "OCR", 271 | "apache", 272 | "doc", 273 | "documents", 274 | "docx", 275 | "odt", 276 | "office", 277 | "pdf", 278 | "ppt", 279 | "pptx", 280 | "tika" 281 | ] 282 | } 283 | ] 284 | -------------------------------------------------------------------------------- /wp-redisearch.php: -------------------------------------------------------------------------------- 1 |